This commit is contained in:
candle 2025-08-03 20:51:43 -04:00
parent 57f0f340a9
commit 120ab466aa
22 changed files with 286 additions and 539 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ run/
run-data/
.idea/
.gradle/
.c*

View File

@ -38,7 +38,7 @@ mod_name=TravelersSuitcase
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
mod_license=MIT
# The mod version. See https://semver.org/
mod_version=1.2.7-SNAPSHOT
mod_version=1.2.7-SNAPSHOTv2
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
# This should match the base package used for the mod sources.
# See https://maven.apache.org/guides/mini/guide-naming-conventions.html

View File

@ -1,7 +1,8 @@
package io.lampnet.travelerssuitcase;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.Item;
import net.minecraft.world.entity.EntityType;
import net.minecraftforge.common.ForgeConfigSpec;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
@ -10,20 +11,210 @@ import net.minecraftforge.registries.ForgeRegistries;
import java.util.List;
import java.util.Set;
import java.util.HashSet;
import java.util.stream.Collectors;
@Mod.EventBusSubscriber(modid = TravelersSuitcase.MODID, bus = Mod.EventBusSubscriber.Bus.MOD)
public class Config {
private static final ForgeConfigSpec.Builder BUILDER = new ForgeConfigSpec.Builder();
static final ForgeConfigSpec SPEC = BUILDER.build();
public static final ForgeConfigSpec.BooleanValue CAN_CAPTURE_HOSTILE;
public static final ForgeConfigSpec.ConfigValue<List<? extends String>> ENTITY_BLACKLIST;
private static boolean validateItemName(final Object obj) {
return obj instanceof final String itemName && ForgeRegistries.ITEMS.containsKey(ResourceLocation.parse(itemName));
public static final ForgeConfigSpec.LongValue DEFAULT_TIME_OF_DAY;
public static final ForgeConfigSpec.BooleanValue SHOULD_TICK_TIME;
public static final ForgeConfigSpec.BooleanValue TICK_WHEN_EMPTY;
public static final ForgeConfigSpec.IntValue DEFAULT_SUNNY_TIME;
public static final ForgeConfigSpec.BooleanValue DEFAULT_RAINING;
public static final ForgeConfigSpec.IntValue DEFAULT_RAIN_TIME;
public static final ForgeConfigSpec.BooleanValue DEFAULT_THUNDERING;
public static final ForgeConfigSpec.IntValue DEFAULT_THUNDER_TIME;
public static final ForgeConfigSpec.IntValue DIMENSION_HEIGHT;
public static final ForgeConfigSpec.IntValue DIMENSION_MIN_Y;
public static final ForgeConfigSpec.DoubleValue COORDINATE_SCALE;
public static final ForgeConfigSpec.DoubleValue AMBIENT_LIGHT;
static final ForgeConfigSpec SPEC;
static {
BUILDER.comment("Entity Configuration").push("entities");
CAN_CAPTURE_HOSTILE = BUILDER
.comment("Whether hostile entities can be captured in suitcases")
.define("canCaptureHostile", false);
ENTITY_BLACKLIST = BUILDER
.comment("List of entity types that cannot be captured (e.g., minecraft:ender_dragon)")
.defineListAllowEmpty("blacklistedEntities",
List.of("minecraft:ender_dragon", "minecraft:wither"),
Config::validateEntityName);
BUILDER.pop();
BUILDER.comment("Pocket Dimension Configuration").push("pocket_dimensions");
DEFAULT_TIME_OF_DAY = BUILDER
.comment("Default time of day in pocket dimensions (0-24000, where 6000 is noon)")
.defineInRange("defaultTimeOfDay", 6000L, 0L, 24000L);
SHOULD_TICK_TIME = BUILDER
.comment("Whether time should advance in pocket dimensions")
.define("shouldTickTime", true);
TICK_WHEN_EMPTY = BUILDER
.comment("Whether pocket dimensions should tick when no players are present")
.define("tickWhenEmpty", true);
BUILDER.pop();
BUILDER.comment("Weather Configuration").push("weather");
DEFAULT_SUNNY_TIME = BUILDER
.comment("Default duration of sunny weather in pocket dimensions (ticks)")
.defineInRange("defaultSunnyTime", Integer.MAX_VALUE, 0, Integer.MAX_VALUE);
DEFAULT_RAINING = BUILDER
.comment("Whether pocket dimensions should start with rain")
.define("defaultRaining", false);
DEFAULT_RAIN_TIME = BUILDER
.comment("Default rain duration in pocket dimensions (ticks)")
.defineInRange("defaultRainTime", 0, 0, Integer.MAX_VALUE);
DEFAULT_THUNDERING = BUILDER
.comment("Whether pocket dimensions should start with thunder")
.define("defaultThundering", false);
DEFAULT_THUNDER_TIME = BUILDER
.comment("Default thunder duration in pocket dimensions (ticks)")
.defineInRange("defaultThunderTime", 0, 0, Integer.MAX_VALUE);
BUILDER.pop();
BUILDER.comment("Dimension Properties").push("dimension_properties");
DIMENSION_HEIGHT = BUILDER
.comment("Height of pocket dimensions (must be multiple of 16)")
.defineInRange("dimensionHeight", 384, 64, 4064);
DIMENSION_MIN_Y = BUILDER
.comment("Minimum Y level for pocket dimensions")
.defineInRange("dimensionMinY", -64, -2032, 2016);
COORDINATE_SCALE = BUILDER
.comment("Coordinate scale for pocket dimensions")
.defineInRange("coordinateScale", 1.0, 0.00001, 30000000.0);
AMBIENT_LIGHT = BUILDER
.comment("Ambient light level in pocket dimensions (0.0 - 1.0)")
.defineInRange("ambientLight", 0.0, 0.0, 1.0);
BUILDER.pop();
SPEC = BUILDER.build();
}
private static boolean validateEntityName(final Object obj) {
if (!(obj instanceof String entityName)) {
return false;
}
try {
ResourceLocation location = ResourceLocation.parse(entityName);
return ForgeRegistries.ENTITY_TYPES.containsKey(location);
} catch (Exception e) {
return false;
}
}
private static Set<EntityType<?>> cachedEntityBlacklist = new HashSet<>();
@SubscribeEvent
static void onLoad(final ModConfigEvent event) {
if (event.getConfig().getSpec() == SPEC) {
updateCachedValues();
TravelersSuitcase.LOGGER.info("Configuration loaded");
}
}
}
private static void updateCachedValues() {
cachedEntityBlacklist = ENTITY_BLACKLIST.get().stream()
.map(entityName -> {
try {
ResourceLocation location = ResourceLocation.parse(entityName);
return ForgeRegistries.ENTITY_TYPES.getValue(location);
} catch (Exception e) {
TravelersSuitcase.LOGGER.warn("Invalid entity name in blacklist: {}", entityName);
return null;
}
})
.filter(entityType -> entityType != null)
.collect(Collectors.toSet());
}
public static boolean canCaptureHostile() {
return CAN_CAPTURE_HOSTILE.get();
}
public static Set<EntityType<?>> getEntityBlacklist() {
return new HashSet<>(cachedEntityBlacklist);
}
public static boolean isEntityBlacklisted(EntityType<?> entityType) {
return cachedEntityBlacklist.contains(entityType);
}
public static long getDefaultTimeOfDay() {
return DEFAULT_TIME_OF_DAY.get();
}
public static boolean shouldTickTime() {
return SHOULD_TICK_TIME.get();
}
public static boolean tickWhenEmpty() {
return TICK_WHEN_EMPTY.get();
}
public static int getDefaultSunnyTime() {
return DEFAULT_SUNNY_TIME.get();
}
public static boolean isDefaultRaining() {
return DEFAULT_RAINING.get();
}
public static int getDefaultRainTime() {
return DEFAULT_RAIN_TIME.get();
}
public static boolean isDefaultThundering() {
return DEFAULT_THUNDERING.get();
}
public static int getDefaultThunderTime() {
return DEFAULT_THUNDER_TIME.get();
}
public static int getDimensionHeight() {
return DIMENSION_HEIGHT.get();
}
public static int getDimensionMinY() {
return DIMENSION_MIN_Y.get();
}
public static double getCoordinateScale() {
return COORDINATE_SCALE.get();
}
public static double getAmbientLight() {
return AMBIENT_LIGHT.get();
}
}

View File

@ -1,6 +1,6 @@
package io.lampnet.travelerssuitcase;
import com.mojang.brigadier.arguments.BoolArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import io.lampnet.travelerssuitcase.block.ModBlocks;
@ -48,16 +48,18 @@ import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import net.minecraftforge.fml.ModLoadingContext;
import net.minecraftforge.fml.config.ModConfig;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.Set;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.entity.Entity;
import net.minecraft.server.MinecraftServer;
import io.lampnet.travelerssuitcase.world.RuntimeWorldHandle;
@ -67,36 +69,16 @@ public class TravelersSuitcase {
public static final Logger LOGGER = LogManager.getLogger(MODID);
public static final EnterPocketDimensionCriterion ENTER_POCKET_DIMENSION = CriteriaTriggers.register(new EnterPocketDimensionCriterion());
private static boolean canCaptureHostile = false;
private static final Set<EntityType<?>> entityBlacklist = new HashSet<>();
// Public accessor methods for the configuration
public static boolean getCanCaptureHostile() {
return canCaptureHostile;
}
public static void setCanCaptureHostile(boolean value) {
canCaptureHostile = value;
return Config.canCaptureHostile();
}
public static Set<EntityType<?>> getEntityBlacklist() {
return new HashSet<>(entityBlacklist);
}
public static void addToBlacklist(EntityType<?> entityType) {
entityBlacklist.add(entityType);
}
public static boolean removeFromBlacklist(EntityType<?> entityType) {
return entityBlacklist.remove(entityType);
return Config.getEntityBlacklist();
}
public static boolean isBlacklisted(EntityType<?> entityType) {
return entityBlacklist.contains(entityType);
}
public static void clearBlacklist() {
entityBlacklist.clear();
return Config.isEntityBlacklisted(entityType);
}
public TravelersSuitcase() {
@ -106,29 +88,24 @@ public class TravelersSuitcase {
ModBlocks.register(modEventBus);
ModItemGroups.register(modEventBus);
ModBlockEntities.register(modEventBus);
FantasyInitializer.register(modEventBus);
// Register configuration
ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, Config.SPEC);
modEventBus.addListener(this::commonSetup);
MinecraftForge.EVENT_BUS.register(this);
MinecraftForge.EVENT_BUS.register(TravelersSuitcase.class); // Register static methods
MinecraftForge.EVENT_BUS.register(TravelersSuitcase.class);
}
private void commonSetup(final FMLCommonSetupEvent event) {
// Register chunk generators during setup phase
new FantasyInitializer().onInitialize();
}
@SubscribeEvent
public static void onServerStarting(ServerStartingEvent event) {
SuitcaseRegistryState.onServerStart(event.getServer());
// Load configuration
try {
loadConfig(event.getServer());
LOGGER.info("Configuration loaded successfully");
} catch (IOException e) {
LOGGER.warn("Failed to load configuration, using defaults", e);
}
Path registryFile = event.getServer().getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT)
.resolve("data")
@ -153,8 +130,7 @@ public class TravelersSuitcase {
.setSeed(seed);
RuntimeWorldHandle handle = Fantasy.get(event.getServer()).getOrOpenPersistentWorld(worldId, cfg);
// Ensure the pocket dimension ticks even when empty to prevent entity tick issues
handle.setTickWhenEmpty(true);
handle.setTickWhenEmpty(Config.tickWhenEmpty());
}
} catch (IOException e) {
TravelersSuitcase.LOGGER.error("Failed to reload pocket dimensions", e);
@ -266,7 +242,6 @@ public class TravelersSuitcase {
return 0;
}
// Reset player entry to default position
PlayerEntryData playerData = PlayerEntryData.get(targetWorld);
playerData.resetToDefault();
@ -292,7 +267,6 @@ public class TravelersSuitcase {
return 0;
}
// Reset mob entry to default position
MobEntryData mobData = MobEntryData.get(targetWorld);
mobData.setEntry(new Vec3(35.5, 85.0, 16.5), 0f, 0f);
@ -329,19 +303,11 @@ public class TravelersSuitcase {
)
.then(Commands.literal("canCaptureHostile")
.requires(src -> src.hasPermission(2))
.then(Commands.argument("value", BoolArgumentType.bool())
.executes(ctx -> {
boolean value = BoolArgumentType.getBool(ctx, "value");
canCaptureHostile = value;
ctx.getSource().sendSuccess(() -> Component.literal(
value ? "§aHostile mob capture enabled" : "§cHostile mob capture disabled"
), false);
return 1;
})
)
.executes(ctx -> {
.executes(ctx -> {
boolean enabled = Config.canCaptureHostile();
ctx.getSource().sendSuccess(() -> Component.literal(
"§7Hostile mob capture is currently: " + (canCaptureHostile ? "§aEnabled" : "§cDisabled")
"§7Hostile mob capture is currently: " + (enabled ? "§aEnabled" : "§cDisabled") +
"\n§7(Configure in config file)"
), false);
return 1;
})
@ -356,7 +322,7 @@ public class TravelersSuitcase {
try {
ResourceLocation entityId = ResourceLocation.parse(entityString);
EntityType<?> entityType = BuiltInRegistries.ENTITY_TYPE.get(entityId);
if (entityType == EntityType.PIG && !entityString.equals("minecraft:pig")) { // Default fallback
if (entityType == EntityType.PIG && !entityString.equals("minecraft:pig")) {
src.sendFailure(Component.literal("§cUnknown entity type: " + entityString));
return 0;
}
@ -364,7 +330,6 @@ public class TravelersSuitcase {
src.sendFailure(Component.literal("§cCannot blacklist players"));
return 0;
}
entityBlacklist.add(entityType);
src.sendSuccess(() -> Component.literal("§aAdded " + entityId + " to blacklist"), false);
return 1;
} catch (Exception e) {
@ -386,11 +351,7 @@ public class TravelersSuitcase {
src.sendFailure(Component.literal("§cUnknown entity type: " + entityString));
return 0;
}
if (entityBlacklist.remove(entityType)) {
src.sendSuccess(() -> Component.literal("§aRemoved " + entityId + " from blacklist"), false);
} else {
src.sendSuccess(() -> Component.literal("§7" + entityId + " was not in blacklist"), false);
}
src.sendSuccess(() -> Component.literal("§eEntity blacklist is managed through configuration files"), false);
return 1;
} catch (Exception e) {
src.sendFailure(Component.literal("§cInvalid entity identifier: " + entityString));
@ -402,11 +363,12 @@ public class TravelersSuitcase {
.then(Commands.literal("list")
.executes(ctx -> {
var src = ctx.getSource();
if (entityBlacklist.isEmpty()) {
Set<EntityType<?>> blacklist = Config.getEntityBlacklist();
if (blacklist.isEmpty()) {
src.sendSuccess(() -> Component.literal("§7No entities are blacklisted"), false);
} else {
src.sendSuccess(() -> Component.literal("§aBlacklisted entities:"), false);
entityBlacklist.forEach(entityType -> {
blacklist.forEach(entityType -> {
var id = BuiltInRegistries.ENTITY_TYPE.getKey(entityType);
src.sendSuccess(() -> Component.literal(" " + id), false);
});
@ -415,127 +377,17 @@ public class TravelersSuitcase {
})
)
.then(Commands.literal("clear")
.executes(ctx -> {
.executes(ctx -> {
var src = ctx.getSource();
int count = entityBlacklist.size();
entityBlacklist.clear();
src.sendSuccess(() -> Component.literal("§aCleared blacklist (removed " + count + " entities)"), false);
src.sendSuccess(() -> Component.literal("§eEntity blacklist is managed through configuration files"), false);
return 1;
})
)
)
.then(Commands.literal("config")
.requires(src -> src.hasPermission(2))
.then(Commands.literal("save")
.executes(ctx -> {
var src = ctx.getSource();
try {
saveConfig(src.getServer());
src.sendSuccess(() -> Component.literal("§aConfiguration saved successfully"), false);
return 1;
} catch (Exception e) {
src.sendFailure(Component.literal("§cFailed to save configuration: " + e.getMessage()));
return 0;
}
})
)
.then(Commands.literal("load")
.executes(ctx -> {
var src = ctx.getSource();
try {
loadConfig(src.getServer());
src.sendSuccess(() -> Component.literal("§aConfiguration loaded successfully"), false);
return 1;
} catch (Exception e) {
src.sendFailure(Component.literal("§cFailed to load configuration: " + e.getMessage()));
return 0;
}
})
)
.then(Commands.literal("reload")
.executes(ctx -> {
var src = ctx.getSource();
try {
loadConfig(src.getServer());
src.sendSuccess(() -> Component.literal("§aConfiguration reloaded successfully"), false);
return 1;
} catch (Exception e) {
src.sendFailure(Component.literal("§cFailed to reload configuration: " + e.getMessage()));
return 0;
}
})
)
);
event.getDispatcher().register(builder);
}
private static void saveConfig(MinecraftServer server) throws IOException {
Path configDir = server.getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT)
.resolve("data")
.resolve(MODID);
Files.createDirectories(configDir);
Path configFile = configDir.resolve("config.properties");
StringBuilder content = new StringBuilder();
content.append("canCaptureHostile=").append(canCaptureHostile).append("\n");
content.append("blacklistedEntities=");
boolean first = true;
for (EntityType<?> entityType : entityBlacklist) {
if (!first) {
content.append(",");
}
content.append(BuiltInRegistries.ENTITY_TYPE.getKey(entityType));
first = false;
}
content.append("\n");
Files.writeString(configFile, content.toString());
}
private static void loadConfig(MinecraftServer server) throws IOException {
Path configFile = server.getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT)
.resolve("data")
.resolve(MODID)
.resolve("config.properties");
if (!Files.exists(configFile)) {
return;
}
entityBlacklist.clear();
for (String line : Files.readAllLines(configFile)) {
String[] parts = line.split("=", 2);
if (parts.length != 2) continue;
String key = parts[0].trim();
String value = parts[1].trim();
switch (key) {
case "canCaptureHostile":
canCaptureHostile = Boolean.parseBoolean(value);
break;
case "blacklistedEntities":
if (!value.isEmpty()) {
for (String entityId : value.split(",")) {
try {
ResourceLocation id = ResourceLocation.parse(entityId.trim());
EntityType<?> entityType = BuiltInRegistries.ENTITY_TYPE.get(id);
if (entityType != EntityType.PIG || entityId.trim().equals("minecraft:pig")) {
entityBlacklist.add(entityType);
}
} catch (Exception e) {
LOGGER.warn("Failed to parse entity ID from config: " + entityId, e);
}
}
}
break;
}
}
}
@SubscribeEvent
public void onUseEntity(PlayerInteractEvent.EntityInteract event) {
@ -576,7 +428,7 @@ public class TravelersSuitcase {
if (!(entity instanceof LivingEntity mob)) {
return;
}
if (entityBlacklist.contains(mob.getType())) {
if (Config.isEntityBlacklisted(mob.getType())) {
player.displayClientMessage(Component.literal("§c☒"), true);
world.playSound(null, player.getX(), player.getY(), player.getZ(), SoundEvents.ZOMBIE_ATTACK_WOODEN_DOOR, SoundSource.PLAYERS, 0.3f, 1.5f);
event.setCancellationResult(InteractionResult.FAIL);
@ -584,7 +436,7 @@ public class TravelersSuitcase {
return;
}
boolean isHostile = mob instanceof Enemy || (mob instanceof Wolf wolf && wolf.isAngryAtAllPlayers(world));
if (!canCaptureHostile && isHostile) {
if (!Config.canCaptureHostile() && isHostile) {
player.displayClientMessage(Component.literal("§c☒"), true);
world.playSound(null, player.getX(), player.getY(), player.getZ(), SoundEvents.ZOMBIE_ATTACK_WOODEN_DOOR, SoundSource.PLAYERS, 0.3f, 1.5f);
event.setCancellationResult(InteractionResult.FAIL);
@ -607,8 +459,7 @@ public class TravelersSuitcase {
event.setCanceled(true);
return;
}
// Key rescue mob
if (stack.getItem() instanceof KeystoneItem) {
ResourceLocation dimId = world.dimension().location();
String namespace = dimId.getNamespace();

View File

@ -312,7 +312,7 @@ public class SuitcaseBlock extends BaseEntityBlock {
CompoundTag display = stack.getOrCreateTagElement("display");
ListTag lore = new ListTag();
if (!suitcase.getEnteredPlayers().isEmpty()) {
Component warningText = Component.literal("§c⚠ Contains " + suitcase.getEnteredPlayers().size() + " Traveler's!")
Component warningText = Component.literal("§c⚠ Contains " + suitcase.getEnteredPlayers().size() + " Travelers!")
.withStyle(ChatFormatting.RED);
lore.add(StringTag.valueOf(Component.Serializer.toJson(warningText)));
}

View File

@ -187,13 +187,11 @@ public class SuitcaseBlockEntity extends BlockEntity {
@Override
public @NotNull CompoundTag getUpdateTag() {
CompoundTag nbt = new CompoundTag();
// Only sync essential data to client, not player entry data which could cause position conflicts
if (boundKeystoneName != null) {
nbt.putString("BoundKeystone", boundKeystoneName);
}
nbt.putBoolean("Locked", isLocked);
nbt.putBoolean("DimensionLocked", dimensionLocked);
// Deliberately exclude EnteredPlayers from client sync to prevent coordinate conflicts
return nbt;
}

View File

@ -7,8 +7,8 @@ import net.minecraft.world.phys.Vec3;
public class MobEntryData extends SavedData {
private static final String DATA_KEY = "pocket_entry_data";
private Vec3 entryPos = Vec3.ZERO;
private float entryYaw = 0f, entryPitch = 0f;
private Vec3 entryPos;
private float entryYaw, entryPitch;
public MobEntryData() {
super();

View File

@ -8,8 +8,8 @@ import net.minecraft.world.phys.Vec3;
public class PlayerEntryData extends SavedData {
private static final String DATA_KEY = "pocket_player_entry";
private Vec3 entryPos = Vec3.ZERO;
private float entryYaw = 0f, entryPitch = 0f;
private Vec3 entryPos;
private float entryYaw, entryPitch;
private boolean isManuallySet = false;
public PlayerEntryData() {

View File

@ -56,22 +56,14 @@ public class KeystoneItem extends Item {
public @NotNull InteractionResultHolder<ItemStack> use(@NotNull Level world, Player player, @NotNull InteractionHand hand) {
ItemStack stack = player.getItemInHand(hand);
// If already enchanted, do nothing
if (stack.isEnchanted()) {
return InteractionResultHolder.pass(stack);
}
// If no valid custom name, do nothing
if (!isValidKeystone(stack)) {
return InteractionResultHolder.pass(stack);
}
// On client side, just return success
if (world.isClientSide) {
return InteractionResultHolder.success(stack);
}
// The actual enchantment and dimension creation now happens in inventoryTick
return InteractionResultHolder.success(stack);
}
@ -84,18 +76,15 @@ public class KeystoneItem extends Item {
.resolve(dimensionName);
boolean dimensionExists = Files.exists(worldSavePath);
// Get biome registry - if this fails, the server isn't ready yet
var biomeRegistry = server.registryAccess().registryOrThrow(Registries.BIOME);
RuntimeWorldConfig config = new RuntimeWorldConfig()
.setGenerator(new PortalChunkGenerator(biomeRegistry))
.setSeed(server.overworld().getSeed())
.setShouldTickTime(true);
.setSeed(server.overworld().getSeed());
RuntimeWorldHandle handle = Fantasy.get(server)
.getOrOpenPersistentWorld(worldId, config);
// Ensure the pocket dimension ticks even when empty to prevent entity tick issues
handle.setTickWhenEmpty(true);
TravelersSuitcase.LOGGER.info("Created/loaded pocket dimension '{}', ticking enabled: {}",
@ -105,7 +94,6 @@ public class KeystoneItem extends Item {
if (!dimensionExists) {
ServerLevel world = handle.asWorld();
// Schedule structure placement for next tick to ensure world is fully initialized
server.execute(() -> placeStructureDelayed(server, world, dimensionName));
}
}
@ -119,29 +107,10 @@ public class KeystoneItem extends Item {
if (template != null) {
BlockPos pos = new BlockPos(0, 64, 0);
// Ensure the chunk is fully loaded and ready
ChunkPos chunkPos = new ChunkPos(pos);
world.getChunkSource().addRegionTicket(TicketType.UNKNOWN, chunkPos, 2, chunkPos);
// Force chunk to be fully loaded with all features
world.getChunk(pos.getX() >> 4, pos.getZ() >> 4, ChunkStatus.FULL, true);
// Place blocks first without entities to ensure support blocks exist
template.placeInWorld(
world,
pos,
pos,
new StructurePlaceSettings()
.setMirror(Mirror.NONE)
.setRotation(Rotation.NONE)
.setIgnoreEntities(true), // Skip entities first
world.getRandom(),
Block.UPDATE_ALL
);
world.setChunkForced(pos.getX() >> 4, pos.getZ() >> 4, true);
// Now place the structure again with entities, blocks won't replace since they're already there
template.placeInWorld(
world,
pos,
@ -151,10 +120,11 @@ public class KeystoneItem extends Item {
.setRotation(Rotation.NONE)
.setIgnoreEntities(false),
world.getRandom(),
Block.UPDATE_NEIGHBORS // Only update neighbors, don't replace existing blocks
Block.UPDATE_ALL
);
world.setChunkForced(pos.getX() >> 4, pos.getZ() >> 4, true);
// Force a save to ensure the forced chunk data is properly loaded for ticking
world.save(null, true, false);
}
} catch (Exception e) {
@ -172,13 +142,11 @@ public class KeystoneItem extends Item {
Files.createDirectories(registryDir);
Path registryFile = registryDir.resolve("registry.txt");
// load existing lines
Set<String> dims = new HashSet<>();
if (Files.exists(registryFile)) {
dims.addAll(Files.readAllLines(registryFile));
}
// add + save only if new
if (dims.add(dimensionName)) {
Files.write(registryFile, dims);
}
@ -217,7 +185,6 @@ public class KeystoneItem extends Item {
stack.getTag().remove("CustomModelData");
}
// Auto-enchant and create dimension when item gets a valid custom name
if (!world.isClientSide() && hasValidName && !stack.isEnchanted()) {
String dimensionName = "pocket_dimension_" + keystoneName.replaceAll("[^a-z0-9_]", "");
createOrLoadPersistentDimension(world.getServer(), dimensionName);
@ -226,17 +193,19 @@ public class KeystoneItem extends Item {
CompoundTag nbt = stack.getOrCreateTag();
nbt.putInt("RepairCost", 32767);
// Play sound effect if entity is a player
if (entity instanceof Player player) {
world.playSound(null, player.getX(), player.getY(), player.getZ(),
SoundEvents.AMETHYST_CLUSTER_FALL, SoundSource.PLAYERS, 2.0F, 2.0F);
}
}
if (!world.isClientSide() && stack.hasTag() && stack.getTag().contains("Enchantments")) {
CompoundTag nbt = stack.getOrCreateTag();
if (nbt.getInt("RepairCost") < 32767) {
nbt.putInt("RepairCost", 32767);
if (!world.isClientSide() && stack.hasTag()) {
assert stack.getTag() != null;
if (stack.getTag().contains("Enchantments")) {
CompoundTag nbt = stack.getOrCreateTag();
if (nbt.getInt("RepairCost") < 32767) {
nbt.putInt("RepairCost", 32767);
}
}
}
}

View File

@ -17,25 +17,21 @@ public abstract class MappedRegistryMixin<T> implements RemoveFromRegistry<T> {
@Override
public boolean fantasy$remove(T value) {
// Implementation depends on access to private fields
// For now, return false as removal is complex
return false;
}
@Override
public boolean fantasy$remove(ResourceLocation key) {
// Implementation depends on access to private fields
// For now, return false as removal is complex
return false;
}
@Override
public void fantasy$setFrozen(boolean value) {
// Store the original frozen state so we can restore it later
if (!value && this.frozen) {
this.fantasy$originalFrozenState = true;
}
// Actually modify the registry's frozen state
this.frozen = value;
}

View File

@ -15,21 +15,17 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
public class ServerChunkManagerMixin {
@Shadow
@Final
private ServerLevel level;
public ServerLevel level;
@Inject(method = "runDistanceManagerUpdates", at = @At("HEAD"), cancellable = true)
private void onRunDistanceManagerUpdates(CallbackInfoReturnable<Boolean> cir) {
// Only apply special chunk processing logic to RuntimeWorld instances (fantasy dimensions)
if (this.level instanceof RuntimeWorld) {
FantasyWorldAccess worldAccess = (FantasyWorldAccess) this.level;
// Always allow distance manager updates if chunks are loaded (prevents breaking entity initialization)
// Only block when world is truly empty AND configured not to tick when empty
if (this.level.getChunkSource().getLoadedChunksCount() > 0 || worldAccess.fantasy$shouldTick()) {
// Allow distance manager to run - this is critical for entity systems
return;
}
cir.setReturnValue(false);
}
// Regular worlds (overworld, nether, end) process chunks normally without interference
}
}

View File

@ -17,17 +17,15 @@ import java.util.function.BooleanSupplier;
@Mixin(ServerLevel.class)
public abstract class ServerWorldMixin implements FantasyWorldAccess {
@Unique
private boolean fantasy$tickWhenEmpty = false; // Default to false, will be set to true for fantasy worlds
private boolean fantasy$tickWhenEmpty = false;
@Inject(method = "tick", at = @At("HEAD"), cancellable = true)
private void onTick(BooleanSupplier shouldKeepTicking, CallbackInfo ci) {
// Only apply special tick logic to RuntimeWorld instances (fantasy dimensions)
if ((Object) this instanceof RuntimeWorld) {
if (!this.fantasy$shouldTick()) {
ci.cancel();
}
}
// Regular worlds (overworld, nether, end) tick normally without interference
}
@Shadow
@ -43,16 +41,14 @@ public abstract class ServerWorldMixin implements FantasyWorldAccess {
@Override
public boolean fantasy$shouldTick() {
// For RuntimeWorld instances, use the configured behavior
if ((Object) this instanceof RuntimeWorld) {
return this.fantasy$tickWhenEmpty || !this.isWorldEmpty();
return this.fantasy$tickWhenEmpty || !this.travelerssuitcase$isWorldEmpty();
}
// For regular worlds, always tick (normal behavior)
return true;
}
@Unique
private boolean isWorldEmpty() {
private boolean travelerssuitcase$isWorldEmpty() {
return this.players().isEmpty() && this.getChunkSource().getLoadedChunksCount() <= 0;
}
}

View File

@ -1,144 +0,0 @@
package io.lampnet.travelerssuitcase.util;
import com.google.common.collect.Iterators;
import com.mojang.serialization.Lifecycle;
import net.minecraft.core.Holder;
import net.minecraft.core.MappedRegistry;
import net.minecraft.core.Registry;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import org.jetbrains.annotations.NotNull;
import javax.annotation.Nullable;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Stream;
public class FilteredRegistry<T> extends MappedRegistry<T> {
private final Registry<T> source;
private final Predicate<T> check;
public FilteredRegistry(Registry<T> source, Predicate<T> check) {
super(source.key(), source.registryLifecycle(), false);
this.source = source;
this.check = check;
}
public Registry<T> getSource() {
return this.source;
}
@Nullable
@Override
public ResourceLocation getKey(T value) {
return check.test(value) ? this.source.getKey(value) : null;
}
@Override
public Optional<ResourceKey<T>> getResourceKey(T entry) {
return check.test(entry) ? this.source.getResourceKey(entry) : Optional.empty();
}
@Override
public int getId(@Nullable T value) {
return check.test(value) ? this.source.getId(value) : -1;
}
public T get(int index) {
return this.source.byId(index);
}
@Override
public int size() {
return (int) this.source.stream().filter(check).count();
}
@Nullable
@Override
public T get(@Nullable ResourceKey<T> key) {
T value = this.source.get(key);
return (value != null && check.test(value)) ? value : null;
}
@Nullable
@Override
public T get(@Nullable ResourceLocation id) {
T value = this.source.get(id);
return (value != null && check.test(value)) ? value : null;
}
@Override
public Lifecycle lifecycle(T entry) {
return this.source.lifecycle(entry);
}
@Override
public @NotNull Set<ResourceLocation> keySet() {
Set<ResourceLocation> keys = new HashSet<>();
this.source.keySet().forEach(key -> {
T value = this.source.get(key);
if(value != null && check.test(value)) {
keys.add(key);
}
});
return keys;
}
@Override
public @NotNull Set<Map.Entry<ResourceKey<T>, T>> entrySet() {
Set<Map.Entry<ResourceKey<T>, T>> set = new HashSet<>();
for (Map.Entry<ResourceKey<T>, T> e : this.source.entrySet()) {
if (this.check.test(e.getValue())) {
set.add(e);
}
}
return set;
}
@Override
public boolean containsKey(ResourceLocation id) {
T value = this.source.get(id);
return value != null && check.test(value);
}
@Override
public boolean containsKey(ResourceKey<T> key) {
T value = this.source.get(key);
return value != null && check.test(value);
}
@Override
public @NotNull Iterator<T> iterator() {
return Iterators.filter(this.source.iterator(), e -> this.check.test(e));
}
// Methods below are for WritableRegistry, we don't support writing to a filtered registry
@Override
public Holder.Reference<T> register(ResourceKey<T> resourceKey, T object, Lifecycle lifecycle) {
throw new UnsupportedOperationException("Cannot register to a filtered registry");
}
public Holder.Reference<T> registerOrNah(ResourceKey<T> resourceKey, T object, Lifecycle lifecycle) {
throw new UnsupportedOperationException("Cannot register to a filtered registry");
}
@Override
public Holder.Reference<T> registerMapping(int i, ResourceKey<T> resourceKey, T object, Lifecycle lifecycle) {
throw new UnsupportedOperationException("Cannot register to a filtered registry");
}
@Override
public Holder.Reference<T> getHolderOrThrow(ResourceKey<T> resourceKey) {
return this.source.getHolderOrThrow(resourceKey);
}
@Override
public Optional<Holder.Reference<T>> getHolder(ResourceKey<T> resourceKey) {
return this.source.getHolder(resourceKey);
}
@Override
public Stream<Holder.Reference<T>> holders() {
return this.source.holders().filter(h -> check.test(h.value()));
}
}

View File

@ -19,10 +19,6 @@ public final class GameRuleStore {
this.intRules.put(key, value);
}
public boolean contains(GameRules.Key<?> key) {
return this.booleanRules.containsKey(key) || this.intRules.containsKey(key);
}
public void applyTo(GameRules rules, @Nullable MinecraftServer server) {
for (Map.Entry<GameRules.Key<GameRules.BooleanValue>, Boolean> entry : this.booleanRules.entrySet()) {
GameRules.BooleanValue rule = rules.getRule(entry.getKey());

View File

@ -1,23 +0,0 @@
package io.lampnet.travelerssuitcase.util;
import java.util.Collection;
import java.util.Iterator;
public final class SafeIterator<T> implements Iterator<T> {
private final Object[] values;
private int index = 0;
public SafeIterator(Collection<T> source) {
this.values = source.toArray();
}
@Override
public boolean hasNext() {
return this.values.length > this.index;
}
@Override
public T next() {
return (T) this.values[this.index++];
}
}

View File

@ -1,90 +0,0 @@
package io.lampnet.travelerssuitcase.util;
import com.mojang.serialization.Codec;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Registry;
import net.minecraft.server.level.WorldGenRegion;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeManager;
import net.minecraft.world.level.biome.Biomes;
import net.minecraft.world.level.biome.FixedBiomeSource;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.blending.Blender;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
public class VoidChunkGenerator extends ChunkGenerator {
public static final Codec<VoidChunkGenerator> CODEC = Codec.unit(VoidChunkGenerator::new);
public VoidChunkGenerator(Registry<Biome> biomeRegistry) {
super(new FixedBiomeSource(biomeRegistry.getHolderOrThrow(Biomes.THE_VOID)));
}
public VoidChunkGenerator() {
super(new FixedBiomeSource(null));
}
@Override
protected @NotNull Codec<? extends ChunkGenerator> codec() {
return CODEC;
}
@Override
public void applyCarvers(WorldGenRegion worldGenRegion, long seed, RandomState randomState, BiomeManager biomeManager, StructureManager structureManager, ChunkAccess chunkAccess, GenerationStep.Carving carving) {
}
@Override
public void spawnOriginalMobs(WorldGenRegion worldGenRegion) {
}
@Override
public int getGenDepth() {
return 384;
}
@Override
public @NotNull CompletableFuture<ChunkAccess> fillFromNoise(Executor executor, Blender blender, RandomState randomState, StructureManager structureManager, ChunkAccess chunkAccess) {
return CompletableFuture.completedFuture(chunkAccess);
}
@Override
public int getSeaLevel() {
return 0;
}
@Override
public int getMinY() {
return 0;
}
@Override
public int getBaseHeight(int i, int j, Heightmap.Types types, LevelHeightAccessor levelHeightAccessor, RandomState randomState) {
return 0;
}
@Override
public @NotNull NoiseColumn getBaseColumn(int i, int j, LevelHeightAccessor levelHeightAccessor, RandomState randomState) {
return new NoiseColumn(0, new BlockState[0]);
}
@Override
public void addDebugScreenInfo(List<String> list, RandomState randomState, BlockPos blockPos) {
}
@Override
public void buildSurface(WorldGenRegion worldGenRegion, StructureManager structureManager, RandomState randomState,
ChunkAccess chunkAccess) {
// No surface to build for void chunks
}
}

View File

@ -43,7 +43,6 @@ public final class Fantasy {
private final Set<ServerLevel> unloadingQueue = new ReferenceOpenHashSet<>();
static {
// We need to register the event listener on the bus.
MinecraftForge.EVENT_BUS.register(Fantasy.class);
}

View File

@ -1,15 +1,25 @@
package io.lampnet.travelerssuitcase.world;
import io.lampnet.travelerssuitcase.util.VoidChunkGenerator;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.ResourceLocation;
import static io.lampnet.travelerssuitcase.TravelersSuitcase.MODID;
import com.mojang.serialization.Codec;
import io.lampnet.travelerssuitcase.TravelersSuitcase;
import net.minecraft.core.registries.Registries;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraftforge.eventbus.api.IEventBus;
import net.minecraftforge.registries.DeferredRegister;
import net.minecraftforge.registries.RegistryObject;
public final class FantasyInitializer {
public static final DeferredRegister<Codec<? extends ChunkGenerator>> CHUNK_GENERATORS =
DeferredRegister.create(Registries.CHUNK_GENERATOR, TravelersSuitcase.MODID);
public static final RegistryObject<Codec<PortalChunkGenerator>> PORTAL_GENERATOR =
CHUNK_GENERATORS.register("portal", () -> PortalChunkGenerator.CODEC);
public static void register(IEventBus eventBus) {
CHUNK_GENERATORS.register(eventBus);
}
public void onInitialize() {
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, ResourceLocation.fromNamespaceAndPath(MODID, "void"), VoidChunkGenerator.CODEC);
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, ResourceLocation.fromNamespaceAndPath(MODID, "portal"), PortalChunkGenerator.CODEC);
// Registration now handled by DeferredRegister
}
}

View File

@ -24,6 +24,7 @@ import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import java.util.Arrays;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
@ -92,9 +93,7 @@ public class PortalChunkGenerator extends ChunkGenerator {
@Override
public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor level, RandomState randomState) {
BlockState[] column = new BlockState[level.getHeight()];
for (int i = 0; i < column.length; i++) {
column[i] = Blocks.AIR.defaultBlockState();
}
Arrays.fill(column, Blocks.AIR.defaultBlockState());
return new NoiseColumn(level.getMinBuildHeight(), column);
}

View File

@ -22,7 +22,7 @@ public class RuntimeWorld extends ServerLevel {
server,
Util.backgroundExecutor(),
((MinecraftServerAccess) server).getSession(),
new RuntimeWorldProperties(server.getWorldData().overworldData(), config),
new RuntimeWorldProperties(server.getWorldData().overworldData(), config, server),
registryKey,
new LevelStem(
server.registryAccess().registryOrThrow(net.minecraft.core.registries.Registries.DIMENSION_TYPE)

View File

@ -1,6 +1,7 @@
package io.lampnet.travelerssuitcase.world;
import com.google.common.base.Preconditions;
import io.lampnet.travelerssuitcase.Config;
import io.lampnet.travelerssuitcase.util.GameRuleStore;
import net.minecraft.core.Holder;
import net.minecraft.core.registries.Registries;
@ -17,20 +18,19 @@ public final class RuntimeWorldConfig {
private ResourceKey<LevelStem> dimensionTypeKey = Fantasy.DEFAULT_DIM_TYPE;
private Holder<LevelStem> dimensionType;
private ChunkGenerator generator = null;
private boolean shouldTickTime = false;
private long timeOfDay = 6000;
private Difficulty difficulty = Difficulty.NORMAL;
private boolean shouldTickTime = Config.shouldTickTime();
private long timeOfDay = Config.getDefaultTimeOfDay();
private Difficulty difficulty = null; // Will use server difficulty
private final GameRuleStore gameRules = new GameRuleStore();
private RuntimeWorld.Constructor worldConstructor = RuntimeWorld::new;
private int sunnyTime = Integer.MAX_VALUE;
private boolean raining;
private int rainTime;
private boolean thundering;
private int thunderTime;
private int sunnyTime = Config.getDefaultSunnyTime();
private boolean raining = Config.isDefaultRaining();
private int rainTime = Config.getDefaultRainTime();
private boolean thundering = Config.isDefaultThundering();
private int thunderTime = Config.getDefaultThunderTime();
private TriState flat = TriState.DEFAULT;
// Simple TriState implementation
public enum TriState {
DEFAULT,
TRUE,
@ -165,8 +165,8 @@ public final class RuntimeWorldConfig {
return this.timeOfDay;
}
public Difficulty getDifficulty() {
return this.difficulty;
public Difficulty getDifficulty(MinecraftServer server) {
return this.difficulty != null ? this.difficulty : server.getWorldData().getDifficulty();
}
public GameRuleStore getGameRules() {

View File

@ -1,7 +1,7 @@
package io.lampnet.travelerssuitcase.world;
import net.minecraft.CrashReportCategory;
import net.minecraft.core.BlockPos;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.Difficulty;
import net.minecraft.world.level.GameRules;
@ -16,10 +16,12 @@ import java.util.UUID;
public class RuntimeWorldProperties implements ServerLevelData {
private final ServerLevelData parent;
final RuntimeWorldConfig config;
private final MinecraftServer server;
public RuntimeWorldProperties(ServerLevelData parent, RuntimeWorldConfig config) {
public RuntimeWorldProperties(ServerLevelData parent, RuntimeWorldConfig config, MinecraftServer server) {
this.parent = parent;
this.config = config;
this.server = server;
}
@Override
@ -150,7 +152,7 @@ public class RuntimeWorldProperties implements ServerLevelData {
@Override
public @NotNull Difficulty getDifficulty() {
return this.config.getDifficulty();
return this.config.getDifficulty(this.server);
}
@Override