diff --git a/gradle.properties b/gradle.properties index d011161..4d0cb72 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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.0-SNAPSHOT +mod_version=1.2.7-SNAPSHOT # 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 diff --git a/src/main/java/io/lampnet/travelerssuitcase/Config.java b/src/main/java/io/lampnet/travelerssuitcase/Config.java index 490a2a6..245305a 100644 --- a/src/main/java/io/lampnet/travelerssuitcase/Config.java +++ b/src/main/java/io/lampnet/travelerssuitcase/Config.java @@ -19,7 +19,7 @@ public class Config { static final ForgeConfigSpec SPEC = BUILDER.build(); private static boolean validateItemName(final Object obj) { - return obj instanceof final String itemName && ForgeRegistries.ITEMS.containsKey(new ResourceLocation(itemName)); + return obj instanceof final String itemName && ForgeRegistries.ITEMS.containsKey(ResourceLocation.parse(itemName)); } @SubscribeEvent diff --git a/src/main/java/io/lampnet/travelerssuitcase/SuitcaseRegistryState.java b/src/main/java/io/lampnet/travelerssuitcase/SuitcaseRegistryState.java index e5ca29f..63f073b 100644 --- a/src/main/java/io/lampnet/travelerssuitcase/SuitcaseRegistryState.java +++ b/src/main/java/io/lampnet/travelerssuitcase/SuitcaseRegistryState.java @@ -1,21 +1,21 @@ package io.lampnet.travelerssuitcase; import io.lampnet.travelerssuitcase.block.entity.SuitcaseBlockEntity; +import net.minecraft.core.BlockPos; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; import net.minecraft.nbt.Tag; import net.minecraft.server.MinecraftServer; -import net.minecraft.world.level.saveddata.SavedData; -import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; import net.minecraft.world.level.Level; -import org.jetbrains.annotations.NotNull; +import net.minecraft.world.level.saveddata.SavedData; import java.util.HashMap; import java.util.Map; import java.util.Objects; public class SuitcaseRegistryState extends SavedData { - private static final String REGISTRY_KEY = "travelers_suitcase_registry"; + public static final String DATA_NAME = "travelers_suitcase_registry"; private final Map> registry = new HashMap<>(); public SuitcaseRegistryState() { @@ -23,62 +23,86 @@ public class SuitcaseRegistryState extends SavedData { } @Override - public @NotNull CompoundTag save(@NotNull CompoundTag nbt) { - ListTag keystonesList = new ListTag(); - for (Map.Entry> keystoneEntry : registry.entrySet()) { - CompoundTag keystoneNbt = new CompoundTag(); - keystoneNbt.putString("KeystoneName", keystoneEntry.getKey()); - ListTag playersList = getPlayersList(keystoneEntry); - keystoneNbt.put("Players", playersList); - keystonesList.add(keystoneNbt); + public CompoundTag save(CompoundTag nbt) { + CompoundTag top = new CompoundTag(); + for (Map.Entry> entry : registry.entrySet()) { + String keystone = entry.getKey(); + Map playerMap = entry.getValue(); + + ListTag playerList = new ListTag(); + for (Map.Entry e2 : playerMap.entrySet()) { + CompoundTag record = new CompoundTag(); + record.putString("UUID", e2.getKey()); + BlockPos pos = e2.getValue(); + record.putInt("X", pos.getX()); + record.putInt("Y", pos.getY()); + record.putInt("Z", pos.getZ()); + playerList.add(record); + } + top.put(keystone, playerList); } - nbt.put("SuitcaseRegistry", keystonesList); + nbt.put("RegistryEntries", top); return nbt; } - private static @NotNull ListTag getPlayersList(Map.Entry> keystoneEntry) { - ListTag playersList = new ListTag(); - for (Map.Entry playerEntry : keystoneEntry.getValue().entrySet()) { - CompoundTag playerNbt = new CompoundTag(); - playerNbt.putString("UUID", playerEntry.getKey()); - BlockPos pos = playerEntry.getValue(); - playerNbt.putInt("X", pos.getX()); - playerNbt.putInt("Y", pos.getY()); - playerNbt.putInt("Z", pos.getZ()); - - playersList.add(playerNbt); - } - return playersList; - } - public static SuitcaseRegistryState load(CompoundTag nbt) { - SuitcaseRegistryState state = new SuitcaseRegistryState(); - if (nbt.contains("SuitcaseRegistry")) { - ListTag keystonesList = nbt.getList("SuitcaseRegistry", Tag.TAG_COMPOUND); - for (int i = 0; i < keystonesList.size(); i++) { - CompoundTag keystoneNbt = keystonesList.getCompound(i); - String keystoneName = keystoneNbt.getString("KeystoneName"); - Map playersMap = new HashMap<>(); - ListTag playersList = keystoneNbt.getList("Players", Tag.TAG_COMPOUND); - for (int j = 0; j < playersList.size(); j++) { - CompoundTag playerNbt = playersList.getCompound(j); - String uuid = playerNbt.getString("UUID"); - BlockPos pos = new BlockPos( - playerNbt.getInt("X"), - playerNbt.getInt("Y"), - playerNbt.getInt("Z") - ); - playersMap.put(uuid, pos); + SuitcaseRegistryState data = new SuitcaseRegistryState(); + if (nbt.contains("RegistryEntries", Tag.TAG_COMPOUND)) { + CompoundTag top = nbt.getCompound("RegistryEntries"); + for (String keystone : top.getAllKeys()) { + ListTag playerList = top.getList(keystone, Tag.TAG_COMPOUND); + Map playerMap = new HashMap<>(); + for (int i = 0; i < playerList.size(); i++) { + CompoundTag rec = playerList.getCompound(i); + String uuid = rec.getString("UUID"); + int x = rec.getInt("X"); + int y = rec.getInt("Y"); + int z = rec.getInt("Z"); + playerMap.put(uuid, new BlockPos(x, y, z)); } - state.registry.put(keystoneName, playersMap); + data.registry.put(keystone, playerMap); } } - return state; + return data; + } + + public void syncFromStaticRegistry() { + registry.clear(); + SuitcaseBlockEntity.saveSuitcaseRegistryTo(registry); + setDirty(); + } + + public void syncToStaticRegistry() { + SuitcaseBlockEntity.initializeSuitcaseRegistry(registry); + } + + public static void onServerStart(MinecraftServer server) { + Level overworld = server.getLevel(Level.OVERWORLD); + if (overworld == null) return; + + SuitcaseRegistryState data = Objects.requireNonNull(((ServerLevel) overworld).getDataStorage()).computeIfAbsent( + SuitcaseRegistryState::load, + SuitcaseRegistryState::new, + DATA_NAME + ); + data.syncToStaticRegistry(); + } + + public static void onRegistryChanged(MinecraftServer server) { + Level overworld = server.getLevel(Level.OVERWORLD); + if (overworld == null) return; + + SuitcaseRegistryState data = Objects.requireNonNull(((ServerLevel) overworld).getDataStorage()).computeIfAbsent( + SuitcaseRegistryState::load, + SuitcaseRegistryState::new, + DATA_NAME + ); + data.syncFromStaticRegistry(); } public static SuitcaseRegistryState getState(MinecraftServer server) { - return Objects.requireNonNull(server.getLevel(Level.OVERWORLD)).getDataStorage() - .computeIfAbsent(SuitcaseRegistryState::load, SuitcaseRegistryState::new, REGISTRY_KEY); + return Objects.requireNonNull(((ServerLevel) server.getLevel(Level.OVERWORLD)).getDataStorage()) + .computeIfAbsent(SuitcaseRegistryState::load, SuitcaseRegistryState::new, DATA_NAME); } public Map> getRegistry() { diff --git a/src/main/java/io/lampnet/travelerssuitcase/TravelersSuitcase.java b/src/main/java/io/lampnet/travelerssuitcase/TravelersSuitcase.java index f15586e..41b1faa 100644 --- a/src/main/java/io/lampnet/travelerssuitcase/TravelersSuitcase.java +++ b/src/main/java/io/lampnet/travelerssuitcase/TravelersSuitcase.java @@ -1,22 +1,102 @@ 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; +import io.lampnet.travelerssuitcase.block.SuitcaseBlock; import io.lampnet.travelerssuitcase.block.entity.ModBlockEntities; +import io.lampnet.travelerssuitcase.block.entity.SuitcaseBlockEntity; +import io.lampnet.travelerssuitcase.criterion.EnterPocketDimensionCriterion; +import io.lampnet.travelerssuitcase.data.MobEntryData; +import io.lampnet.travelerssuitcase.data.PlayerEntryData; +import io.lampnet.travelerssuitcase.item.KeystoneItem; import io.lampnet.travelerssuitcase.item.ModItemGroups; import io.lampnet.travelerssuitcase.item.ModItems; -import io.lampnet.travelerssuitcase.block.entity.SuitcaseBlockEntity; -import io.lampnet.travelerssuitcase.SuitcaseRegistryState; +import io.lampnet.travelerssuitcase.world.Fantasy; +import io.lampnet.travelerssuitcase.world.FantasyInitializer; +import io.lampnet.travelerssuitcase.world.PortalChunkGenerator; +import io.lampnet.travelerssuitcase.world.RuntimeWorldConfig; +import net.minecraft.advancements.CriteriaTriggers; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.monster.Enemy; +import net.minecraft.world.entity.animal.Wolf; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.RegisterCommandsEvent; +import net.minecraftforge.event.entity.player.PlayerInteractEvent; +import net.minecraftforge.event.server.ServerStartingEvent; import net.minecraftforge.eventbus.api.IEventBus; +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 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 io.lampnet.travelerssuitcase.world.RuntimeWorldHandle; @Mod(TravelersSuitcase.MODID) public class TravelersSuitcase { public static final String MODID = "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> entityBlacklist = new HashSet<>(); + + // Public accessor methods for the configuration + public static boolean getCanCaptureHostile() { + return canCaptureHostile; + } + + public static void setCanCaptureHostile(boolean value) { + canCaptureHostile = value; + } + + public static Set> getEntityBlacklist() { + return new HashSet<>(entityBlacklist); + } + + public static void addToBlacklist(EntityType entityType) { + entityBlacklist.add(entityType); + } + + public static boolean removeFromBlacklist(EntityType entityType) { + return entityBlacklist.remove(entityType); + } + + public static boolean isBlacklisted(EntityType entityType) { + return entityBlacklist.contains(entityType); + } + + public static void clearBlacklist() { + entityBlacklist.clear(); + } public TravelersSuitcase() { IEventBus modEventBus = FMLJavaModLoadingContext.get().getModEventBus(); @@ -26,55 +106,392 @@ public class TravelersSuitcase { ModItemGroups.register(modEventBus); ModBlockEntities.register(modEventBus); - MinecraftForge.EVENT_BUS.addListener(this::onWorldLoad); - MinecraftForge.EVENT_BUS.addListener(this::onServerStarting); - MinecraftForge.EVENT_BUS.addListener(this::onServerStopping); + modEventBus.addListener(this::commonSetup); - LOGGER.info("Initializing " + MODID); + MinecraftForge.EVENT_BUS.register(this); + MinecraftForge.EVENT_BUS.register(TravelersSuitcase.class); // Register static methods } - private void onWorldLoad(net.minecraftforge.event.level.LevelEvent.Load event) { - if (event.getLevel() instanceof net.minecraft.server.level.ServerLevel world) { - if (world.dimension().location().getNamespace().equals(MODID)) { - String dimensionName = world.dimension().location().getPath(); - java.nio.file.Path structureMarkerPath = world.getServer().getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT) - .resolve("data") - .resolve(MODID) - .resolve("pending_structures") - .resolve(dimensionName + ".txt"); - if (java.nio.file.Files.exists(structureMarkerPath)) { - try { - net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate template = world.getServer().getStructureManager() - .get(new net.minecraft.resources.ResourceLocation(MODID, "pocket_island_01")) - .orElse(null); - if (template != null) { - net.minecraft.core.BlockPos pos = new net.minecraft.core.BlockPos(0, 64, 0); - template.placeInWorld( - world, - pos, - pos, - new net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings(), - world.getRandom(), - net.minecraft.world.level.block.Block.UPDATE_ALL - ); - java.nio.file.Files.delete(structureMarkerPath); - } - } catch (java.io.IOException e) { - LOGGER.error("Failed to place structure", e); - } - } + 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()); + + Path registryFile = event.getServer().getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT) + .resolve("data") + .resolve(MODID) + .resolve("dimension_registry") + .resolve("registry.txt"); + + if (!Files.exists(registryFile)) { + return; + } + var biomeRegistry = event.getServer().registryAccess().registryOrThrow(Registries.BIOME); + + long seed = event.getServer().overworld().getSeed(); + try { + for (String dimName : Files.readAllLines(registryFile)) { + ResourceLocation worldId = ResourceLocation.fromNamespaceAndPath(MODID, dimName); + + var voidGen = new PortalChunkGenerator(biomeRegistry); + + var cfg = new RuntimeWorldConfig() + .setGenerator(voidGen) + .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); } + } catch (IOException e) { + TravelersSuitcase.LOGGER.error("Failed to reload pocket dimensions", e); } } - private void onServerStarting(net.minecraftforge.event.server.ServerStartingEvent event) { - SuitcaseRegistryState state = SuitcaseRegistryState.getState(event.getServer()); - SuitcaseBlockEntity.initializeSuitcaseRegistry(state.getRegistry()); + @SubscribeEvent + public static void onRegisterCommands(RegisterCommandsEvent event) { + LiteralArgumentBuilder builder = Commands.literal("travelerssuitcase") + .then(Commands.literal("setMobEntry") + .executes(ctx -> { + var src = ctx.getSource(); + var world = src.getLevel(); + var id = world.dimension().location(); + if (!id.getNamespace().equals(MODID) + || !id.getPath().startsWith("pocket_dimension_")) { + src.sendFailure(Component.literal("§cNot in a pocket dimension")); + return 0; + } + var pos = src.getPosition(); + var yaw = src.getEntity().getYRot(); + var pitch = src.getEntity().getXRot(); + + MobEntryData.get(world).setEntry(pos, yaw, pitch); + src.sendSuccess(() -> Component.literal( + String.format("§aMob entry set to %.2f, %.2f, %.2f", pos.x(), pos.y(), pos.z()) + ), false); + return 1; + }) + ) + .then(Commands.literal("setPlayerEntry") + .executes(ctx -> { + var src = ctx.getSource(); + var world = src.getLevel(); + var id = world.dimension().location(); + if (!id.getNamespace().equals(MODID) + || !id.getPath().startsWith("pocket_dimension_")) { + src.sendFailure(Component.literal("§cNot in a pocket dimension")); + return 0; + } + var pos = src.getPosition(); + var yaw = src.getEntity().getYRot(); + var pitch = src.getEntity().getXRot(); + + PlayerEntryData.get(world).setEntry(pos, yaw, pitch); + src.sendSuccess(() -> Component.literal( + String.format("§aPlayer entry set to %.2f, %.2f, %.2f", pos.x(), pos.y(), pos.z()) + ), false); + return 1; + })) + .then(Commands.literal("resetPlayerEntry") + .then(Commands.argument("dimension", StringArgumentType.word()) + .executes(ctx -> { + var src = ctx.getSource(); + String dimSuffix = StringArgumentType.getString(ctx, "dimension"); + + ResourceLocation dimId = ResourceLocation.fromNamespaceAndPath(MODID, "pocket_dimension_" + dimSuffix); + ResourceKey worldKey = ResourceKey.create(Registries.DIMENSION, dimId); + ServerLevel targetWorld = src.getServer().getLevel(worldKey); + + if (targetWorld == null) { + src.sendFailure(Component.literal("§cPocket dimension '" + dimSuffix + "' not found")); + return 0; + } + + // Reset player entry to default position + PlayerEntryData playerData = PlayerEntryData.get(targetWorld); + playerData.setEntry(new Vec3(17.5, 97.0, 9.5), 0f, 0f); + + src.sendSuccess(() -> Component.literal( + "§aPlayer entry reset for pocket dimension '" + dimSuffix + "'" + ), false); + return 1; + }) + ) + ) + .then(Commands.literal("listDimensions") + .requires(src -> src.hasPermission(2)) + .executes(ctx -> { + var src = ctx.getSource(); + src.sendSuccess(() -> Component.literal("§aPocket Dimensions Loaded:"), false); + boolean foundAny = false; + for (ServerLevel world : src.getServer().getAllLevels()) { + var id = world.dimension().location(); + String namespace = id.getNamespace(); + String path = id.getPath(); + String prefix = "pocket_dimension_"; + + if (MODID.equals(namespace) && path.startsWith(prefix)) { + String suffix = path.substring(prefix.length()); + src.sendSuccess(() -> Component.literal(" " + suffix), false); + foundAny = true; + } + } + if (!foundAny) { + src.sendSuccess(() -> Component.literal("§cNo pocket dimensions found."), false); + } + return 1; + }) + ) + .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 -> { + ctx.getSource().sendSuccess(() -> Component.literal( + "§7Hostile mob capture is currently: " + (canCaptureHostile ? "§aEnabled" : "§cDisabled") + ), false); + return 1; + }) + ) + .then(Commands.literal("mobBlacklist") + .requires(src -> src.hasPermission(2)) + .then(Commands.literal("add") + .then(Commands.argument("entity", StringArgumentType.string()) + .executes(ctx -> { + var src = ctx.getSource(); + String entityString = StringArgumentType.getString(ctx, "entity"); + try { + ResourceLocation entityId = ResourceLocation.parse(entityString); + EntityType entityType = BuiltInRegistries.ENTITY_TYPE.get(entityId); + if (entityType == EntityType.PIG && !entityString.equals("minecraft:pig")) { // Default fallback + src.sendFailure(Component.literal("§cUnknown entity type: " + entityString)); + return 0; + } + if (entityType == EntityType.PLAYER) { + 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) { + src.sendFailure(Component.literal("§cInvalid entity identifier: " + entityString)); + return 0; + } + }) + ) + ) + .then(Commands.literal("remove") + .then(Commands.argument("entity", StringArgumentType.string()) + .executes(ctx -> { + var src = ctx.getSource(); + String entityString = StringArgumentType.getString(ctx, "entity"); + try { + var entityId = ResourceLocation.parse(entityString); + var entityType = BuiltInRegistries.ENTITY_TYPE.get(entityId); + if (entityType == EntityType.PIG && !entityString.equals("minecraft:pig")) { + 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); + } + return 1; + } catch (Exception e) { + src.sendFailure(Component.literal("§cInvalid entity identifier: " + entityString)); + return 0; + } + }) + ) + ) + .then(Commands.literal("list") + .executes(ctx -> { + var src = ctx.getSource(); + if (entityBlacklist.isEmpty()) { + src.sendSuccess(() -> Component.literal("§7No entities are blacklisted"), false); + } else { + src.sendSuccess(() -> Component.literal("§aBlacklisted entities:"), false); + entityBlacklist.forEach(entityType -> { + var id = BuiltInRegistries.ENTITY_TYPE.getKey(entityType); + src.sendSuccess(() -> Component.literal(" " + id), false); + }); + } + return 1; + }) + ) + .then(Commands.literal("clear") + .executes(ctx -> { + var src = ctx.getSource(); + int count = entityBlacklist.size(); + entityBlacklist.clear(); + src.sendSuccess(() -> Component.literal("§aCleared blacklist (removed " + count + " entities)"), false); + return 1; + }) + ) + ); + event.getDispatcher().register(builder); } - private void onServerStopping(net.minecraftforge.event.server.ServerStoppingEvent event) { - SuitcaseRegistryState state = SuitcaseRegistryState.getState(event.getServer()); - SuitcaseBlockEntity.saveSuitcaseRegistryTo(state.getRegistry()); - state.setDirty(); + @SubscribeEvent + public void onUseEntity(PlayerInteractEvent.EntityInteract event) { + Level world = event.getLevel(); + if (world.isClientSide) return; + var player = event.getEntity(); + var hand = event.getHand(); + var entity = event.getTarget(); + var stack = player.getItemInHand(hand); + + // Suitcase mob teleport + if (stack.getItem() instanceof BlockItem bi && bi.getBlock() instanceof SuitcaseBlock) { + CompoundTag beNbt = BlockItem.getBlockEntityData(stack); + if (beNbt == null || !beNbt.contains("BoundKeystone")) { + 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); + event.setCanceled(true); + return; + } + if (beNbt.getBoolean("Locked")) { + 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); + event.setCanceled(true); + return; + } + String keystone = beNbt.getString("BoundKeystone"); + ResourceLocation dimId = ResourceLocation.fromNamespaceAndPath(MODID, "pocket_dimension_" + keystone); + ResourceKey dimKey = ResourceKey.create(Registries.DIMENSION, dimId); + ServerLevel targetWorld = world.getServer().getLevel(dimKey); + if (targetWorld == null) { + player.displayClientMessage(Component.literal("§cPocket dimension not found"), true); + event.setCancellationResult(InteractionResult.FAIL); + event.setCanceled(true); + return; + } + if (!(entity instanceof LivingEntity mob)) { + return; + } + if (entityBlacklist.contains(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); + event.setCanceled(true); + return; + } + boolean isHostile = mob instanceof Enemy || (mob instanceof Wolf wolf && wolf.isAngryAtAllPlayers(world)); + if (!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); + event.setCanceled(true); + return; + } + MobEntryData data = MobEntryData.get(targetWorld); + Vec3 dest = data.getEntryPos(); + float yaw = data.getEntryYaw(); + float pitch = data.getEntryPitch(); + mob.changeDimension(targetWorld, new net.minecraftforge.common.util.ITeleporter() { + @Override + public Entity placeEntity(Entity entity, ServerLevel currentWorld, ServerLevel destWorld, float yaw, java.util.function.Function repositionEntity) { + Entity e = repositionEntity.apply(false); + e.teleportTo(dest.x, dest.y, dest.z); + return e; + } + }); + world.playSound(null, player.getX(), player.getY(), player.getZ(), SoundEvents.BUNDLE_DROP_CONTENTS, SoundSource.PLAYERS, 2.0f, 1.0f); + world.playSound(null, player.getX(), player.getY(), player.getZ(), SoundEvents.ITEM_PICKUP, SoundSource.PLAYERS, 0.5f, 1.0f); + event.setCancellationResult(InteractionResult.SUCCESS); + event.setCanceled(true); + return; + } + + // Key rescue mob + if (stack.getItem() instanceof KeystoneItem) { + ResourceLocation dimId = world.dimension().location(); + String namespace = dimId.getNamespace(); + String path = dimId.getPath(); + String prefix = "pocket_dimension_"; + if (!namespace.equals(MODID) || !path.startsWith(prefix)) { + return; + } + String keystoneName = path.substring(prefix.length()); + if (!(entity instanceof LivingEntity mob)) { + return; + } + String playerUuid = player.getUUID().toString(); + BlockPos suitcasePos = SuitcaseBlockEntity.findSuitcasePosition(keystoneName, playerUuid); + if (suitcasePos == null) { + player.displayClientMessage(Component.literal("§cNo suitcase found"), true); + event.setCancellationResult(InteractionResult.FAIL); + event.setCanceled(true); + return; + } + ServerLevel overworld = world.getServer().getLevel(Level.OVERWORLD); + if (overworld == null) { + player.displayClientMessage(Component.literal("§cOverworld is not loaded"), true); + event.setCancellationResult(InteractionResult.FAIL); + event.setCanceled(true); + return; + } + Vec3 exitPos = new Vec3(suitcasePos.getX() + 0.5, suitcasePos.getY() + 0.5, suitcasePos.getZ() + 0.5); + mob.changeDimension(overworld, new net.minecraftforge.common.util.ITeleporter() { + @Override + public Entity placeEntity(Entity entity, ServerLevel currentWorld, ServerLevel destWorld, float yaw, java.util.function.Function repositionEntity) { + Entity e = repositionEntity.apply(false); + e.teleportTo(exitPos.x, exitPos.y, exitPos.z); + return e; + } + }); + overworld.playSound(null, exitPos.x, exitPos.y, exitPos.z, SoundEvents.BUNDLE_DROP_CONTENTS, SoundSource.PLAYERS, 2.0f, 1.0f); + world.playSound(null, player.getX(), player.getY(), player.getZ(), SoundEvents.BUNDLE_DROP_CONTENTS, SoundSource.PLAYERS, 2.0f, 1.0f); + event.setCancellationResult(InteractionResult.SUCCESS); + event.setCanceled(true); + } + } + + @SubscribeEvent + public void onUseItem(PlayerInteractEvent.RightClickItem event) { + Level world = event.getLevel(); + var player = event.getEntity(); + var hand = event.getHand(); + var stack = player.getItemInHand(hand); + + if (world.isClientSide || hand != InteractionHand.MAIN_HAND) return; + + if (!(world instanceof ServerLevel sw)) return; + var id = sw.dimension().location(); + if (!id.getNamespace().equals(MODID) || !id.getPath().startsWith("pocket_dimension_")) return; + + if (stack.getItem() == Items.BONE) { + Vec3 pos = player.position(); + float yaw = player.getYRot(); + float pitch = player.getXRot(); + PlayerEntryData.get(sw).setEntry(pos, yaw, pitch); + player.sendSystemMessage(Component.literal(String.format("§aPlayer entry location set to %.1f, %.1f, %.1f", pos.x, pos.y, pos.z))); + event.setCancellationResult(InteractionResult.SUCCESS); + event.setCanceled(true); + } else if (stack.getItem() == Items.LEAD) { + Vec3 pos = player.position(); + float yaw = player.getYRot(); + float pitch = player.getXRot(); + MobEntryData.get(sw).setEntry(pos, yaw, pitch); + player.sendSystemMessage(Component.literal(String.format("§aMob entry location set to %.1f, %.1f, %.1f", pos.x, pos.y, pos.z))); + event.setCancellationResult(InteractionResult.SUCCESS); + event.setCanceled(true); + } } } \ No newline at end of file diff --git a/src/main/java/io/lampnet/travelerssuitcase/block/ModBlocks.java b/src/main/java/io/lampnet/travelerssuitcase/block/ModBlocks.java index 223797a..2cd9c09 100644 --- a/src/main/java/io/lampnet/travelerssuitcase/block/ModBlocks.java +++ b/src/main/java/io/lampnet/travelerssuitcase/block/ModBlocks.java @@ -70,7 +70,7 @@ public class ModBlocks { .sound(SoundType.LODESTONE) .noOcclusion() .lightLevel(getPortalLuminance()) - .strength(-1f))); + .strength(5.0f))); private static RegistryObject registerBlock(String name, Supplier blockSupplier) { RegistryObject block = BLOCKS.register(name, blockSupplier); diff --git a/src/main/java/io/lampnet/travelerssuitcase/block/PocketPortalBlock.java b/src/main/java/io/lampnet/travelerssuitcase/block/PocketPortalBlock.java index b0b0417..08b540b 100644 --- a/src/main/java/io/lampnet/travelerssuitcase/block/PocketPortalBlock.java +++ b/src/main/java/io/lampnet/travelerssuitcase/block/PocketPortalBlock.java @@ -28,10 +28,12 @@ import net.minecraft.world.level.block.state.BlockBehaviour; import net.minecraft.nbt.Tag; import org.jetbrains.annotations.NotNull; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import net.minecraft.world.level.storage.loot.LootParams; public class PocketPortalBlock extends Block { private static final Map LAST_KNOWN_POSITIONS = new HashMap<>(); @@ -57,6 +59,12 @@ public class PocketPortalBlock extends Block { super(properties); } + @Override + public List getDrops(BlockState state, LootParams.Builder builder) { + return Collections.singletonList(new ItemStack(this)); + } + + public static void storePlayerPosition(ServerPlayer player) { LAST_KNOWN_POSITIONS.put( player.getUUID().toString(), @@ -125,7 +133,7 @@ public class PocketPortalBlock extends Block { updateItemLore(stack, remainingPlayers); } } - SuitcaseBlockEntity.removeSuitcaseEntry(keystoneName, player.getUUID().toString()); + SuitcaseBlockEntity.removeSuitcaseEntry(keystoneName, player.getUUID().toString(), player.getServer()); } @Override @@ -162,13 +170,13 @@ public class PocketPortalBlock extends Block { // Fallback: Take them to spawn if (!teleported) { - player.displayClientMessage(Component.literal("§cCouldn't find your return point. Taking you to spawn.").withStyle(ChatFormatting.RED), false); + player.displayClientMessage(Component.literal("§c...").withStyle(ChatFormatting.RED), false); teleportToPosition(world, player, overworld, overworld.getSharedSpawnPos().getX() + 0.5, overworld.getSharedSpawnPos().getY() + 1.0, overworld.getSharedSpawnPos().getZ() + 0.5, 0, 0); } - SuitcaseBlockEntity.removeSuitcaseEntry(keystoneName, player.getUUID().toString()); + SuitcaseBlockEntity.removeSuitcaseEntry(keystoneName, player.getUUID().toString(), world.getServer()); LAST_KNOWN_POSITIONS.remove(player.getUUID().toString()); } } @@ -278,7 +286,9 @@ public class PocketPortalBlock extends Block { private void teleportToPosition(Level world, ServerPlayer player, ServerLevel targetWorld, double x, double y, double z, float yaw, float pitch) { - player.teleportTo(targetWorld, x, y, z, yaw, pitch); + if (!world.isClientSide) { + player.teleportTo(targetWorld, x, y, z, yaw, pitch); + } } private void updateItemLore(ItemStack stack, int playerCount) { @@ -304,5 +314,16 @@ public class PocketPortalBlock extends Block { } } } + if (playerCount > 0) { + Component lore = Component.literal(playerCount + " player" + (playerCount > 1 ? "s" : "") + " inside").withStyle(ChatFormatting.GRAY); + CompoundTag displayTag = stack.getOrCreateTagElement("display"); + ListTag loreList = new ListTag(); + loreList.add(StringTag.valueOf(Component.Serializer.toJson(lore))); + displayTag.put("Lore", loreList); + } else { + if (stack.hasTag() && stack.getTag().contains("display")) { + stack.getTag().getCompound("display").remove("Lore"); + } + } } } \ No newline at end of file diff --git a/src/main/java/io/lampnet/travelerssuitcase/block/SuitcaseBlock.java b/src/main/java/io/lampnet/travelerssuitcase/block/SuitcaseBlock.java index b9b74b3..b66ff49 100644 --- a/src/main/java/io/lampnet/travelerssuitcase/block/SuitcaseBlock.java +++ b/src/main/java/io/lampnet/travelerssuitcase/block/SuitcaseBlock.java @@ -3,6 +3,7 @@ package io.lampnet.travelerssuitcase.block; import io.lampnet.travelerssuitcase.TravelersSuitcase; import io.lampnet.travelerssuitcase.block.entity.ModBlockEntities; import io.lampnet.travelerssuitcase.block.entity.SuitcaseBlockEntity; +import io.lampnet.travelerssuitcase.data.PlayerEntryData; import io.lampnet.travelerssuitcase.item.KeystoneItem; import net.minecraft.world.level.block.BaseEntityBlock; import net.minecraft.world.level.block.Block; @@ -48,6 +49,10 @@ import net.minecraft.world.level.Level; import net.minecraft.world.Containers; import net.minecraft.world.item.DyeColor; +import net.minecraft.network.protocol.game.ClientboundStopSoundPacket; +import net.minecraft.sounds.SoundSource; +import net.minecraft.sounds.SoundEvents; + import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -243,20 +248,37 @@ public class SuitcaseBlock extends BaseEntityBlock { return; } String dimensionName = "pocket_dimension_" + keystoneName; - ResourceLocation dimensionRL = new ResourceLocation(TravelersSuitcase.MODID, dimensionName); + ResourceLocation dimensionRL = ResourceLocation.fromNamespaceAndPath(TravelersSuitcase.MODID, dimensionName); ResourceKey dimensionKey = ResourceKey.create(Registries.DIMENSION, dimensionRL); ServerLevel targetWorld = Objects.requireNonNull(world.getServer()).getLevel(dimensionKey); if (targetWorld != null) { + boolean wasFirstTime = suitcase.isFirstTimeEntering(player); suitcase.playerEntered(player); + if (wasFirstTime) { + TravelersSuitcase.ENTER_POCKET_DIMENSION.trigger(player); + } + player.stopRiding(); player.setDeltaMovement(Vec3.ZERO); player.fallDistance = 0f; - player.teleportTo(targetWorld, 17.5, 97, 9.5, player.getYRot(), player.getXRot()); - world.playSound(null, pos, + + PlayerEntryData ped = PlayerEntryData.get(targetWorld); + Vec3 dest = ped.getEntryPos(); + + float yaw = ped.getEntryYaw(); + float pitch = player.getXRot(); + + player.teleportTo(targetWorld, dest.x, dest.y, dest.z, yaw, pitch); + + player.connection.send(new ClientboundStopSoundPacket(null, null)); + + world.playSound( + null, + pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5, SoundEvents.BUNDLE_DROP_CONTENTS, - SoundSource.PLAYERS, 2.0f, 1.0f); - } else { - TravelersSuitcase.LOGGER.warn("Target dimension {} not found for keystone {}", dimensionRL, keystoneName); + SoundSource.PLAYERS, + 2.0f, 1.0f + ); } } } diff --git a/src/main/java/io/lampnet/travelerssuitcase/block/entity/SuitcaseBlockEntity.java b/src/main/java/io/lampnet/travelerssuitcase/block/entity/SuitcaseBlockEntity.java index ae1026b..3168bab 100644 --- a/src/main/java/io/lampnet/travelerssuitcase/block/entity/SuitcaseBlockEntity.java +++ b/src/main/java/io/lampnet/travelerssuitcase/block/entity/SuitcaseBlockEntity.java @@ -16,6 +16,9 @@ import org.jetbrains.annotations.Nullable; import java.util.*; +import io.lampnet.travelerssuitcase.SuitcaseRegistryState; +import net.minecraft.server.MinecraftServer; + public class SuitcaseBlockEntity extends BlockEntity { private String boundKeystoneName; private boolean isLocked = false; @@ -70,6 +73,11 @@ public class SuitcaseBlockEntity extends BlockEntity { return dimensionLocked; } + private static final java.util.Set PLAYERS_WHO_ENTERED = new java.util.HashSet<>(); + public boolean isFirstTimeEntering(ServerPlayer player) { + return !PLAYERS_WHO_ENTERED.contains(player.getUUID()); + } + public void playerEntered(ServerPlayer player) { enteredPlayers.removeIf(data -> data.uuid.equals(player.getUUID().toString())); EnteredPlayerData data = new EnteredPlayerData( @@ -79,6 +87,9 @@ public class SuitcaseBlockEntity extends BlockEntity { this.worldPosition ); enteredPlayers.add(data); + + PLAYERS_WHO_ENTERED.add(player.getUUID()); + if (boundKeystoneName != null) { Map suitcases = SUITCASE_REGISTRY.computeIfAbsent( boundKeystoneName, k -> new HashMap<>() @@ -87,6 +98,11 @@ public class SuitcaseBlockEntity extends BlockEntity { } PocketPortalBlock.storePlayerPosition(player); setChangedAndNotify(); + + MinecraftServer server = player.getServer(); + if (server != null) { + SuitcaseRegistryState.onRegistryChanged(server); + } } public EnteredPlayerData getExitPosition(String playerUuid) { @@ -182,13 +198,16 @@ public class SuitcaseBlockEntity extends BlockEntity { return (suitcases != null) ? suitcases.get(playerUuid) : null; } - public static void removeSuitcaseEntry(String keystoneName, String playerUuid) { + public static void removeSuitcaseEntry(String keystoneName, String playerUuid, MinecraftServer server) { Map suitcases = SUITCASE_REGISTRY.get(keystoneName); if (suitcases != null) { suitcases.remove(playerUuid); if (suitcases.isEmpty()) { SUITCASE_REGISTRY.remove(keystoneName); } + if (server != null) { + SuitcaseRegistryState.onRegistryChanged(server); + } } } diff --git a/src/main/java/io/lampnet/travelerssuitcase/criterion/EnterPocketDimensionCriterion.java b/src/main/java/io/lampnet/travelerssuitcase/criterion/EnterPocketDimensionCriterion.java new file mode 100644 index 0000000..abe37ab --- /dev/null +++ b/src/main/java/io/lampnet/travelerssuitcase/criterion/EnterPocketDimensionCriterion.java @@ -0,0 +1,37 @@ +package io.lampnet.travelerssuitcase.criterion; + +import com.google.gson.JsonObject; +import io.lampnet.travelerssuitcase.TravelersSuitcase; +import net.minecraft.advancements.critereon.AbstractCriterionTriggerInstance; +import net.minecraft.advancements.critereon.ContextAwarePredicate; +import net.minecraft.advancements.critereon.SimpleCriterionTrigger; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; + +public class EnterPocketDimensionCriterion extends SimpleCriterionTrigger { + static final ResourceLocation ID = ResourceLocation.fromNamespaceAndPath(TravelersSuitcase.MODID, "enter_pocket_dimension"); + + @Override + public ResourceLocation getId() { + return ID; + } + + @Override + public TriggerInstance createInstance(JsonObject jsonObject, ContextAwarePredicate contextAwarePredicate, net.minecraft.advancements.critereon.DeserializationContext deserializationContext) { + return new TriggerInstance(contextAwarePredicate); + } + + public void trigger(ServerPlayer player) { + this.trigger(player, (triggerInstance) -> true); + } + + public static class TriggerInstance extends AbstractCriterionTriggerInstance { + public TriggerInstance(ContextAwarePredicate player) { + super(EnterPocketDimensionCriterion.ID, player); + } + + public JsonObject serializeToJson(net.minecraft.advancements.critereon.SerializationContext serializationContext) { + return super.serializeToJson(serializationContext); + } + } +} \ No newline at end of file diff --git a/src/main/java/io/lampnet/travelerssuitcase/data/MobEntryData.java b/src/main/java/io/lampnet/travelerssuitcase/data/MobEntryData.java new file mode 100644 index 0000000..97c1a1a --- /dev/null +++ b/src/main/java/io/lampnet/travelerssuitcase/data/MobEntryData.java @@ -0,0 +1,55 @@ +package io.lampnet.travelerssuitcase.data; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.saveddata.SavedData; +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; + + public MobEntryData() { + super(); + this.entryPos = new Vec3(35.5, 85, 16.5); + this.entryYaw = 0f; + this.entryPitch = 0f; + } + + public static MobEntryData load(CompoundTag nbt) { + MobEntryData data = new MobEntryData(); + double x = nbt.getDouble("entryX"); + double y = nbt.getDouble("entryY"); + double z = nbt.getDouble("entryZ"); + data.entryPos = new Vec3(x, y, z); + data.entryYaw = nbt.getFloat("entryYaw"); + data.entryPitch = nbt.getFloat("entryPitch"); + return data; + } + + public static MobEntryData get(ServerLevel world) { + return world.getDataStorage().computeIfAbsent(MobEntryData::load, MobEntryData::new, DATA_KEY); + } + + @Override + public CompoundTag save(CompoundTag nbt) { + nbt.putDouble("entryX", entryPos.x); + nbt.putDouble("entryY", entryPos.y); + nbt.putDouble("entryZ", entryPos.z); + nbt.putFloat( "entryYaw", entryYaw); + nbt.putFloat( "entryPitch", entryPitch); + return nbt; + } + + public void setEntry(Vec3 pos, float yaw, float pitch) { + this.entryPos = pos; + this.entryYaw = yaw; + this.entryPitch = pitch; + this.setDirty(); + } + + public Vec3 getEntryPos() { return entryPos; } + public float getEntryYaw() { return entryYaw; } + public float getEntryPitch() { return entryPitch; } +} \ No newline at end of file diff --git a/src/main/java/io/lampnet/travelerssuitcase/data/PlayerEntryData.java b/src/main/java/io/lampnet/travelerssuitcase/data/PlayerEntryData.java new file mode 100644 index 0000000..d2057f2 --- /dev/null +++ b/src/main/java/io/lampnet/travelerssuitcase/data/PlayerEntryData.java @@ -0,0 +1,57 @@ +package io.lampnet.travelerssuitcase.data; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.saveddata.SavedData; +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; + + public PlayerEntryData() { + super(); + this.entryPos = new Vec3(17.5, 97, 9.5); + this.entryYaw = 0f; + this.entryPitch = 0f; + } + + public static PlayerEntryData load(CompoundTag nbt) { + PlayerEntryData data = new PlayerEntryData(); + data.entryPos = new Vec3( + nbt.getDouble("px"), + nbt.getDouble("py"), + nbt.getDouble("pz") + ); + data.entryYaw = nbt.getFloat("pyaw"); + data.entryPitch = nbt.getFloat("ppitch"); + return data; + } + + public static PlayerEntryData get(ServerLevel world) { + return world.getDataStorage().computeIfAbsent(PlayerEntryData::load, PlayerEntryData::new, DATA_KEY); + } + + @Override + public CompoundTag save(CompoundTag nbt) { + nbt.putDouble("px", entryPos.x); + nbt.putDouble("py", entryPos.y); + nbt.putDouble("pz", entryPos.z); + nbt.putFloat( "pyaw", entryYaw); + nbt.putFloat( "ppitch", entryPitch); + return nbt; + } + + public void setEntry(Vec3 pos, float yaw, float pitch) { + this.entryPos = pos; + this.entryYaw = yaw; + this.entryPitch = pitch; + this.setDirty(); + } + + public Vec3 getEntryPos() { return entryPos; } + public float getEntryYaw() { return entryYaw; } + public float getEntryPitch() { return entryPitch; } +} \ No newline at end of file diff --git a/src/main/java/io/lampnet/travelerssuitcase/item/KeystoneItem.java b/src/main/java/io/lampnet/travelerssuitcase/item/KeystoneItem.java index f47afc8..e821e54 100644 --- a/src/main/java/io/lampnet/travelerssuitcase/item/KeystoneItem.java +++ b/src/main/java/io/lampnet/travelerssuitcase/item/KeystoneItem.java @@ -1,26 +1,42 @@ package io.lampnet.travelerssuitcase.item; import io.lampnet.travelerssuitcase.TravelersSuitcase; +import io.lampnet.travelerssuitcase.world.Fantasy; + +import io.lampnet.travelerssuitcase.world.FantasyWorldAccess; +import io.lampnet.travelerssuitcase.world.PortalChunkGenerator; +import io.lampnet.travelerssuitcase.world.RuntimeWorldConfig; +import io.lampnet.travelerssuitcase.world.RuntimeWorldHandle; import net.minecraft.ChatFormatting; -import net.minecraft.world.item.TooltipFlag; -import net.minecraft.world.item.enchantment.Enchantments; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResultHolder; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.server.MinecraftServer; -import net.minecraft.sounds.SoundSource; -import net.minecraft.sounds.SoundEvents; -import net.minecraft.network.chat.Component; -import net.minecraft.world.InteractionHand; -import net.minecraft.world.InteractionResultHolder; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.item.enchantment.Enchantments; import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.ChunkStatus; +import net.minecraft.server.level.TicketType; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; import net.minecraft.world.level.storage.LevelResource; -import net.minecraft.resources.ResourceLocation; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import net.minecraft.world.item.ItemStack.TooltipPart; import java.io.IOException; import java.nio.file.Files; @@ -39,35 +55,143 @@ public class KeystoneItem extends Item { @Override public @NotNull InteractionResultHolder use(@NotNull Level world, Player player, @NotNull InteractionHand hand) { ItemStack stack = player.getItemInHand(hand); - String keystoneName = stack.hasCustomHoverName() ? stack.getHoverName().getString().toLowerCase() : ""; - String defaultName = "item.travelerssuitcase.keystone"; - if (!stack.hasCustomHoverName() || keystoneName.isEmpty() || keystoneName.equals(Component.translatable(defaultName).getString().toLowerCase())) { - return InteractionResultHolder.pass(stack); - } - if (world.isClientSide) { - return InteractionResultHolder.success(stack); - } + + // If already enchanted, do nothing if (stack.isEnchanted()) { return InteractionResultHolder.pass(stack); } - String dimensionName = "pocket_dimension_" + keystoneName.replaceAll("[^a-z0-9_]", ""); - createDimension(world.getServer(), dimensionName); - stack.enchant(Enchantments.BINDING_CURSE, 1); - stack.hideTooltipPart(TooltipPart.ENCHANTMENTS); - stack.hideTooltipPart(TooltipPart.MODIFIERS); - CompoundTag nbt = stack.getOrCreateTag(); - nbt.putInt("RepairCost", 32767); - world.playSound(null, player.getX(), player.getY(), player.getZ(), - SoundEvents.AMETHYST_CLUSTER_FALL, SoundSource.PLAYERS, 2.0F, 2.0F); + + // 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); } + private void createOrLoadPersistentDimension(MinecraftServer server, String dimensionName) { + ResourceLocation worldId = ResourceLocation.fromNamespaceAndPath(TravelersSuitcase.MODID, dimensionName); + + Path worldSavePath = server.getWorldPath(LevelResource.ROOT) + .resolve("dimensions") + .resolve(TravelersSuitcase.MODID) + .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); + + 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: {}", + dimensionName, ((FantasyWorldAccess) handle.asWorld()).fantasy$shouldTick()); + + registerDimension(server, dimensionName); + + if (!dimensionExists) { + ServerLevel world = handle.asWorld(); + // Schedule structure placement for next tick to ensure world is fully initialized + server.execute(() -> placeStructureDelayed(server, world, dimensionName)); + } + } + + private void placeStructureDelayed(MinecraftServer server, ServerLevel world, String dimensionName) { + try { + StructureTemplate template = server.getStructureManager() + .get(ResourceLocation.fromNamespaceAndPath(TravelersSuitcase.MODID, "pocket_island_01")) + .orElse(null); + + 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, + pos, + new StructurePlaceSettings() + .setMirror(Mirror.NONE) + .setRotation(Rotation.NONE) + .setIgnoreEntities(false), + world.getRandom(), + Block.UPDATE_NEIGHBORS // Only update neighbors, don't replace existing blocks + ); + + // Force a save to ensure the forced chunk data is properly loaded for ticking + world.save(null, true, false); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void registerDimension(MinecraftServer server, String dimensionName) { + Path registryDir = server.getWorldPath(LevelResource.ROOT) + .resolve("data") + .resolve(TravelersSuitcase.MODID) + .resolve("dimension_registry"); + + try { + Files.createDirectories(registryDir); + Path registryFile = registryDir.resolve("registry.txt"); + + // load existing lines + Set 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); + } + } catch (IOException e) { + TravelersSuitcase.LOGGER.error("Failed to write dimension registry", e); + } + } + public static boolean isValidKeystone(ItemStack stack) { String keystoneName = stack.hasCustomHoverName() ? stack.getHoverName().getString().toLowerCase() : ""; String defaultNameKey = "item.travelerssuitcase.keystone"; return stack.hasCustomHoverName() && !keystoneName.isEmpty() && - !keystoneName.equals(Component.translatable(defaultNameKey).getString().toLowerCase()) && - stack.isEnchanted(); + !keystoneName.equals(Component.translatable(defaultNameKey).getString().toLowerCase()); } @Override @@ -84,104 +208,36 @@ public class KeystoneItem extends Item { public void inventoryTick(ItemStack stack, @NotNull Level world, @NotNull Entity entity, int slot, boolean selected) { String keystoneName = stack.hasCustomHoverName() ? stack.getHoverName().getString().toLowerCase() : ""; String defaultNameKey = "item.travelerssuitcase.keystone"; - if (!stack.hasCustomHoverName() || keystoneName.isEmpty() || - keystoneName.equals(Component.translatable(defaultNameKey).getString().toLowerCase())) { + boolean hasValidName = isValidKeystone(stack); + + if (!hasValidName) { CompoundTag nbt = stack.getOrCreateTag(); nbt.putInt("CustomModelData", 1); } else if (stack.getTag() != null && stack.getTag().contains("CustomModelData")) { stack.getTag().remove("CustomModelData"); } - } - private boolean createDimension(MinecraftServer server, String dimensionName) { - if (server == null) { - TravelersSuitcase.LOGGER.error("Failed to create dimension: " + dimensionName + " (MinecraftServer instance is null)"); - return false; - } - try { - Path datapackPath = server.getWorldPath(LevelResource.ROOT) - .resolve("datapacks") - .resolve("travelerssuitcase"); - Path dimensionPath = datapackPath - .resolve("data") - .resolve("travelerssuitcase") - .resolve("dimension"); - Files.createDirectories(dimensionPath); - createPackMcmeta(datapackPath); - Path dimensionFile = dimensionPath.resolve(dimensionName + ".json"); - boolean dimensionExists = Files.exists(dimensionFile); + // 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); - ResourceLocation dimensionKeyLocation = new ResourceLocation("travelerssuitcase", dimensionName); - boolean isDimensionRegistered = server.levelKeys().stream() - .anyMatch(key -> key.location().equals(dimensionKeyLocation)); - - Path dimensionRegistryPath = server.getWorldPath(LevelResource.ROOT) - .resolve("data") - .resolve("travelerssuitcase") - .resolve("dimension_registry"); - Files.createDirectories(dimensionRegistryPath); - Path dimensionRegistryFile = dimensionRegistryPath.resolve("registry.txt"); - Set registeredDimensionsInFile; - if (Files.exists(dimensionRegistryFile)) { - registeredDimensionsInFile = new HashSet<>(Files.readAllLines(dimensionRegistryFile)); - } else { - registeredDimensionsInFile = new HashSet<>(); + stack.enchant(Enchantments.BINDING_CURSE, 1); + 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); } - boolean isDimensionInRegistryFile = registeredDimensionsInFile.contains(dimensionName); - - if (!dimensionExists) { - String dimensionJson = """ - { - "type": "travelerssuitcase:pocket_dimension_type", - "generator": { - "type": "minecraft:flat", - "settings": { - "biome": "travelerssuitcase:pocket_islands", - "layers": [ - { - "block": "travelerssuitcase:portal", - "height": 1 - } - ] - } - } - } - """; - Files.writeString(dimensionFile, dimensionJson); - if (!isDimensionInRegistryFile) { - registeredDimensionsInFile.add(dimensionName); - Files.write(dimensionRegistryFile, registeredDimensionsInFile); - } - } - - if (!isDimensionInRegistryFile && !isDimensionRegistered) { - Path structureMarkerPath = server.getWorldPath(LevelResource.ROOT) - .resolve("data") - .resolve("travelerssuitcase") - .resolve("pending_structures"); - Files.createDirectories(structureMarkerPath); - Files.writeString(structureMarkerPath.resolve(dimensionName + ".txt"), "pending"); - return true; - } - return false; - } catch (IOException e) { - TravelersSuitcase.LOGGER.error("Failed to create dimension: {}", dimensionName, e); - return false; } - } - private void createPackMcmeta(@NotNull Path datapackPath) throws IOException { - Path packMcmeta = datapackPath.resolve("pack.mcmeta"); - if (!Files.exists(packMcmeta)) { - String content = """ - { - "pack": { - "pack_format": 15, - "description": "travelerssuitcase Dimensions" - } - } - """; - Files.writeString(packMcmeta, content); + if (!world.isClientSide() && stack.hasTag() && stack.getTag().contains("Enchantments")) { + CompoundTag nbt = stack.getOrCreateTag(); + if (nbt.getInt("RepairCost") < 32767) { + nbt.putInt("RepairCost", 32767); + } } } } \ No newline at end of file diff --git a/src/main/java/io/lampnet/travelerssuitcase/mixin/LevelStemMixin.java b/src/main/java/io/lampnet/travelerssuitcase/mixin/LevelStemMixin.java new file mode 100644 index 0000000..eae5bf1 --- /dev/null +++ b/src/main/java/io/lampnet/travelerssuitcase/mixin/LevelStemMixin.java @@ -0,0 +1,34 @@ +package io.lampnet.travelerssuitcase.mixin; + +import io.lampnet.travelerssuitcase.world.FantasyDimensionOptions; +import net.minecraft.world.level.dimension.LevelStem; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(LevelStem.class) +public class LevelStemMixin implements FantasyDimensionOptions { + @Unique + private boolean fantasy$save = true; + @Unique + private boolean fantasy$saveProperties = true; + + @Override + public void fantasy$setSave(boolean value) { + this.fantasy$save = value; + } + + @Override + public boolean fantasy$getSave() { + return this.fantasy$save; + } + + @Override + public void fantasy$setSaveProperties(boolean value) { + this.fantasy$saveProperties = value; + } + + @Override + public boolean fantasy$getSaveProperties() { + return this.fantasy$saveProperties; + } +} \ No newline at end of file diff --git a/src/main/java/io/lampnet/travelerssuitcase/mixin/MappedRegistryMixin.java b/src/main/java/io/lampnet/travelerssuitcase/mixin/MappedRegistryMixin.java new file mode 100644 index 0000000..963543c --- /dev/null +++ b/src/main/java/io/lampnet/travelerssuitcase/mixin/MappedRegistryMixin.java @@ -0,0 +1,46 @@ +package io.lampnet.travelerssuitcase.mixin; + +import io.lampnet.travelerssuitcase.world.RemoveFromRegistry; +import net.minecraft.core.MappedRegistry; +import net.minecraft.resources.ResourceLocation; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(MappedRegistry.class) +public abstract class MappedRegistryMixin implements RemoveFromRegistry { + @Shadow + private boolean frozen; + + @Unique + private boolean fantasy$originalFrozenState = false; + + @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; + } + + @Override + public boolean fantasy$isFrozen() { + return this.frozen; + } +} \ No newline at end of file diff --git a/src/main/java/io/lampnet/travelerssuitcase/mixin/MinecraftServerAccess.java b/src/main/java/io/lampnet/travelerssuitcase/mixin/MinecraftServerAccess.java new file mode 100644 index 0000000..0368b6b --- /dev/null +++ b/src/main/java/io/lampnet/travelerssuitcase/mixin/MinecraftServerAccess.java @@ -0,0 +1,20 @@ +package io.lampnet.travelerssuitcase.mixin; + +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.LevelStorageSource; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import java.util.Map; + +@Mixin(MinecraftServer.class) +public interface MinecraftServerAccess { + @Accessor("levels") + Map, ServerLevel> getWorlds(); + + @Accessor("storageSource") + LevelStorageSource.LevelStorageAccess getSession(); +} \ No newline at end of file diff --git a/src/main/java/io/lampnet/travelerssuitcase/mixin/ServerChunkManagerMixin.java b/src/main/java/io/lampnet/travelerssuitcase/mixin/ServerChunkManagerMixin.java new file mode 100644 index 0000000..32c07dc --- /dev/null +++ b/src/main/java/io/lampnet/travelerssuitcase/mixin/ServerChunkManagerMixin.java @@ -0,0 +1,30 @@ +package io.lampnet.travelerssuitcase.mixin; + +import io.lampnet.travelerssuitcase.world.FantasyWorldAccess; +import io.lampnet.travelerssuitcase.world.RuntimeWorld; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.ServerLevel; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(ServerChunkCache.class) +public class ServerChunkManagerMixin { + @Shadow + @Final + private ServerLevel level; + + @Inject(method = "runDistanceManagerUpdates", at = @At("HEAD"), cancellable = true) + private void onRunDistanceManagerUpdates(CallbackInfoReturnable cir) { + // Only apply special chunk processing logic to RuntimeWorld instances (fantasy dimensions) + if (this.level instanceof RuntimeWorld) { + if (!((FantasyWorldAccess) this.level).fantasy$shouldTick()) { + cir.setReturnValue(false); + } + } + // Regular worlds (overworld, nether, end) process chunks normally without interference + } +} \ No newline at end of file diff --git a/src/main/java/io/lampnet/travelerssuitcase/mixin/ServerWorldMixin.java b/src/main/java/io/lampnet/travelerssuitcase/mixin/ServerWorldMixin.java new file mode 100644 index 0000000..008fa4f --- /dev/null +++ b/src/main/java/io/lampnet/travelerssuitcase/mixin/ServerWorldMixin.java @@ -0,0 +1,58 @@ +package io.lampnet.travelerssuitcase.mixin; + +import io.lampnet.travelerssuitcase.world.FantasyWorldAccess; +import io.lampnet.travelerssuitcase.world.RuntimeWorld; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.List; +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 + + @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 + public abstract List players(); + + @Shadow + public abstract net.minecraft.server.level.ServerChunkCache getChunkSource(); + + @Override + public void fantasy$setTickWhenEmpty(boolean tickWhenEmpty) { + this.fantasy$tickWhenEmpty = tickWhenEmpty; + } + + @Override + public boolean fantasy$shouldTick() { + // For RuntimeWorld instances, use the configured behavior + if ((Object) this instanceof RuntimeWorld) { + return this.fantasy$tickWhenEmpty || !this.isWorldEmpty(); + } + // For regular worlds, always tick (normal behavior) + return true; + } + + @Unique + private boolean isWorldEmpty() { + return this.players().isEmpty() && this.getChunkSource().getLoadedChunksCount() <= 0; + } +} \ No newline at end of file diff --git a/src/main/java/io/lampnet/travelerssuitcase/util/FilteredRegistry.java b/src/main/java/io/lampnet/travelerssuitcase/util/FilteredRegistry.java new file mode 100644 index 0000000..8ef5618 --- /dev/null +++ b/src/main/java/io/lampnet/travelerssuitcase/util/FilteredRegistry.java @@ -0,0 +1,144 @@ +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 extends MappedRegistry { + private final Registry source; + private final Predicate check; + + public FilteredRegistry(Registry source, Predicate check) { + super(source.key(), source.registryLifecycle(), false); + this.source = source; + this.check = check; + } + + public Registry getSource() { + return this.source; + } + + @Nullable + @Override + public ResourceLocation getKey(T value) { + return check.test(value) ? this.source.getKey(value) : null; + } + + @Override + public Optional> 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 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 keySet() { + Set 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, T>> entrySet() { + Set, T>> set = new HashSet<>(); + for (Map.Entry, 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 key) { + T value = this.source.get(key); + return value != null && check.test(value); + } + + @Override + public @NotNull Iterator 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 register(ResourceKey resourceKey, T object, Lifecycle lifecycle) { + throw new UnsupportedOperationException("Cannot register to a filtered registry"); + } + + public Holder.Reference registerOrNah(ResourceKey resourceKey, T object, Lifecycle lifecycle) { + throw new UnsupportedOperationException("Cannot register to a filtered registry"); + } + + @Override + public Holder.Reference registerMapping(int i, ResourceKey resourceKey, T object, Lifecycle lifecycle) { + throw new UnsupportedOperationException("Cannot register to a filtered registry"); + } + + @Override + public Holder.Reference getHolderOrThrow(ResourceKey resourceKey) { + return this.source.getHolderOrThrow(resourceKey); + } + + @Override + public Optional> getHolder(ResourceKey resourceKey) { + return this.source.getHolder(resourceKey); + } + + @Override + public Stream> holders() { + return this.source.holders().filter(h -> check.test(h.value())); + } +} \ No newline at end of file diff --git a/src/main/java/io/lampnet/travelerssuitcase/util/GameRuleStore.java b/src/main/java/io/lampnet/travelerssuitcase/util/GameRuleStore.java new file mode 100644 index 0000000..cd76349 --- /dev/null +++ b/src/main/java/io/lampnet/travelerssuitcase/util/GameRuleStore.java @@ -0,0 +1,37 @@ +package io.lampnet.travelerssuitcase.util; + +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.GameRules; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; + +public final class GameRuleStore { + private final Map, Boolean> booleanRules = new HashMap<>(); + private final Map, Integer> intRules = new HashMap<>(); + + public void set(GameRules.Key key, boolean value) { + this.booleanRules.put(key, value); + } + + public void set(GameRules.Key key, int value) { + 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, Boolean> entry : this.booleanRules.entrySet()) { + GameRules.BooleanValue rule = rules.getRule(entry.getKey()); + rule.set(entry.getValue(), server); + } + + for (Map.Entry, Integer> entry : this.intRules.entrySet()) { + GameRules.IntegerValue rule = rules.getRule(entry.getKey()); + rule.set(entry.getValue(), server); + } + } +} \ No newline at end of file diff --git a/src/main/java/io/lampnet/travelerssuitcase/util/SafeIterator.java b/src/main/java/io/lampnet/travelerssuitcase/util/SafeIterator.java new file mode 100644 index 0000000..4446537 --- /dev/null +++ b/src/main/java/io/lampnet/travelerssuitcase/util/SafeIterator.java @@ -0,0 +1,23 @@ +package io.lampnet.travelerssuitcase.util; + +import java.util.Collection; +import java.util.Iterator; + +public final class SafeIterator implements Iterator { + private final Object[] values; + private int index = 0; + + public SafeIterator(Collection 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++]; + } +} \ No newline at end of file diff --git a/src/main/java/io/lampnet/travelerssuitcase/util/VoidChunkGenerator.java b/src/main/java/io/lampnet/travelerssuitcase/util/VoidChunkGenerator.java new file mode 100644 index 0000000..aa9c226 --- /dev/null +++ b/src/main/java/io/lampnet/travelerssuitcase/util/VoidChunkGenerator.java @@ -0,0 +1,90 @@ +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 CODEC = Codec.unit(VoidChunkGenerator::new); + + public VoidChunkGenerator(Registry biomeRegistry) { + super(new FixedBiomeSource(biomeRegistry.getHolderOrThrow(Biomes.THE_VOID))); + } + + public VoidChunkGenerator() { + super(new FixedBiomeSource(null)); + } + + @Override + protected @NotNull Codec 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 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 list, RandomState randomState, BlockPos blockPos) { + } + + @Override + public void buildSurface(WorldGenRegion worldGenRegion, StructureManager structureManager, RandomState randomState, + ChunkAccess chunkAccess) { + // No surface to build for void chunks + } +} \ No newline at end of file diff --git a/src/main/java/io/lampnet/travelerssuitcase/util/VoidWorldProgressListener.java b/src/main/java/io/lampnet/travelerssuitcase/util/VoidWorldProgressListener.java new file mode 100644 index 0000000..6469156 --- /dev/null +++ b/src/main/java/io/lampnet/travelerssuitcase/util/VoidWorldProgressListener.java @@ -0,0 +1,33 @@ +package io.lampnet.travelerssuitcase.util; + +import net.minecraft.server.level.progress.ChunkProgressListener; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.ChunkStatus; +import org.jetbrains.annotations.Nullable; + +public class VoidWorldProgressListener implements ChunkProgressListener { + public static final VoidWorldProgressListener INSTANCE = new VoidWorldProgressListener(); + + private VoidWorldProgressListener() { + } + + @Override + public void updateSpawnPos(ChunkPos spawnPos) { + // Do nothing + } + + @Override + public void onStatusChange(ChunkPos pos, @Nullable ChunkStatus status) { + // Do nothing + } + + @Override + public void start() { + // Do nothing + } + + @Override + public void stop() { + // Do nothing + } +} \ No newline at end of file diff --git a/src/main/java/io/lampnet/travelerssuitcase/world/Fantasy.java b/src/main/java/io/lampnet/travelerssuitcase/world/Fantasy.java new file mode 100644 index 0000000..be30aa9 --- /dev/null +++ b/src/main/java/io/lampnet/travelerssuitcase/world/Fantasy.java @@ -0,0 +1,198 @@ +package io.lampnet.travelerssuitcase.world; + +import com.google.common.base.Preconditions; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import io.lampnet.travelerssuitcase.mixin.MinecraftServerAccess; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.dimension.LevelStem; +import net.minecraft.world.level.storage.LevelStorageSource; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.event.server.ServerStoppingEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +public final class Fantasy { + public static final Logger LOGGER = LogManager.getLogger(Fantasy.class); + public static final String ID = "fantasy"; + public static final ResourceKey DEFAULT_DIM_TYPE = ResourceKey.create(Registries.LEVEL_STEM, ResourceLocation.fromNamespaceAndPath(Fantasy.ID, "default")); + + private static Fantasy instance; + + private final MinecraftServer server; + private final MinecraftServerAccess serverAccess; + + private final RuntimeWorldManager worldManager; + + private final Set deletionQueue = new ReferenceOpenHashSet<>(); + private final Set unloadingQueue = new ReferenceOpenHashSet<>(); + + static { + // We need to register the event listener on the bus. + MinecraftForge.EVENT_BUS.register(Fantasy.class); + } + + private Fantasy(MinecraftServer server) { + this.server = server; + this.serverAccess = (MinecraftServerAccess) server; + this.worldManager = new RuntimeWorldManager(server); + } + + public static Fantasy get(MinecraftServer server) { + Preconditions.checkState(server.isSameThread(), "cannot create worlds from off-thread!"); + + if (instance == null || instance.server != server) { + instance = new Fantasy(server); + } + + return instance; + } + + @SubscribeEvent + public static void onServerTick(TickEvent.ServerTickEvent event) { + if (event.phase == TickEvent.Phase.START && instance != null) { + instance.tick(); + } + } + + @SubscribeEvent + public static void onServerStopping(ServerStoppingEvent event) { + if(instance != null) { + instance.onServerStopping(); + } + } + + private void tick() { + Set deletionQueue = this.deletionQueue; + if (!deletionQueue.isEmpty()) { + deletionQueue.removeIf(this::tickDeleteWorld); + } + + Set unloadingQueue = this.unloadingQueue; + if (!unloadingQueue.isEmpty()) { + unloadingQueue.removeIf(this::tickUnloadWorld); + } + } + + public RuntimeWorldHandle openTemporaryWorld(RuntimeWorldConfig config) { + RuntimeWorld world = this.addTemporaryWorld(config); + return new RuntimeWorldHandle(this, world); + } + + public RuntimeWorldHandle getOrOpenPersistentWorld(ResourceLocation key, RuntimeWorldConfig config) { + ResourceKey worldKey = ResourceKey.create(Registries.DIMENSION, key); + + ServerLevel world = this.server.getLevel(worldKey); + if (world == null) { + world = this.addPersistentWorld(key, config); + } else { + this.deletionQueue.remove(world); + } + + return new RuntimeWorldHandle(this, world); + } + + private RuntimeWorld addPersistentWorld(ResourceLocation key, RuntimeWorldConfig config) { + ResourceKey worldKey = ResourceKey.create(Registries.DIMENSION, key); + return this.worldManager.add(worldKey, config, RuntimeWorld.Style.PERSISTENT); + } + + private RuntimeWorld addTemporaryWorld(RuntimeWorldConfig config) { + ResourceKey worldKey = ResourceKey.create(Registries.DIMENSION, generateTemporaryWorldKey()); + + try { + LevelStorageSource.LevelStorageAccess session = this.serverAccess.getSession(); + FileUtils.forceDeleteOnExit(session.getDimensionPath(worldKey).toFile()); + } catch (IOException ignored) { + } + + return this.worldManager.add(worldKey, config, RuntimeWorld.Style.TEMPORARY); + } + + void enqueueWorldDeletion(ServerLevel world) { + this.server.execute(() -> this.deletionQueue.add(world)); + } + + void enqueueWorldUnloading(ServerLevel world) { + this.server.execute(() -> this.unloadingQueue.add(world)); + } + + private boolean tickDeleteWorld(ServerLevel world) { + if (this.isWorldUnloaded(world)) { + this.worldManager.delete(world); + return true; + } else { + this.kickPlayers(world); + return false; + } + } + + private boolean tickUnloadWorld(ServerLevel world) { + if (this.isWorldUnloaded(world)) { + this.worldManager.unload(world); + return true; + } else { + this.kickPlayers(world); + return false; + } + } + + private void kickPlayers(ServerLevel world) { + if (world.players().isEmpty()) { + return; + } + + ServerLevel overworld = this.server.overworld(); + BlockPos spawnPos = overworld.getSharedSpawnPos(); + float spawnAngle = overworld.getSharedSpawnAngle(); + + List players = new ArrayList<>(world.players()); + for (ServerPlayer player : players) { + player.teleportTo(overworld, spawnPos.getX() + 0.5, spawnPos.getY(), spawnPos.getZ() + 0.5, spawnAngle, 0.0F); + } + } + + private boolean isWorldUnloaded(ServerLevel world) { + return world.players().isEmpty() && world.getChunkSource().getLoadedChunksCount() <= 0; + } + + private void onServerStopping() { + List temporaryWorlds = this.collectTemporaryWorlds(); + for (RuntimeWorld temporary : temporaryWorlds) { + this.kickPlayers(temporary); + this.worldManager.delete(temporary); + } + } + + private List collectTemporaryWorlds() { + List temporaryWorlds = new ArrayList<>(); + for (ServerLevel world : this.server.getAllLevels()) { + if (world instanceof RuntimeWorld runtimeWorld) { + if (runtimeWorld.style == RuntimeWorld.Style.TEMPORARY) { + temporaryWorlds.add(runtimeWorld); + } + } + } + return temporaryWorlds; + } + + private static ResourceLocation generateTemporaryWorldKey() { + String key = RandomStringUtils.random(16, "abcdefghijklmnopqrstuvwxyz0123456789"); + return ResourceLocation.fromNamespaceAndPath(Fantasy.ID, key); + } +} \ No newline at end of file diff --git a/src/main/java/io/lampnet/travelerssuitcase/world/FantasyDimensionOptions.java b/src/main/java/io/lampnet/travelerssuitcase/world/FantasyDimensionOptions.java new file mode 100644 index 0000000..372f888 --- /dev/null +++ b/src/main/java/io/lampnet/travelerssuitcase/world/FantasyDimensionOptions.java @@ -0,0 +1,17 @@ +package io.lampnet.travelerssuitcase.world; + +import net.minecraft.world.level.dimension.LevelStem; +import org.jetbrains.annotations.ApiStatus; + +import java.util.function.Predicate; + +@ApiStatus.Internal +public interface FantasyDimensionOptions { + Predicate SAVE_PREDICATE = (e) -> ((FantasyDimensionOptions) (Object) e).fantasy$getSave(); + Predicate SAVE_PROPERTIES_PREDICATE = (e) -> ((FantasyDimensionOptions) (Object) e).fantasy$getSaveProperties(); + + void fantasy$setSave(boolean value); + boolean fantasy$getSave(); + void fantasy$setSaveProperties(boolean value); + boolean fantasy$getSaveProperties(); +} \ No newline at end of file diff --git a/src/main/java/io/lampnet/travelerssuitcase/world/FantasyInitializer.java b/src/main/java/io/lampnet/travelerssuitcase/world/FantasyInitializer.java new file mode 100644 index 0000000..5de6e8a --- /dev/null +++ b/src/main/java/io/lampnet/travelerssuitcase/world/FantasyInitializer.java @@ -0,0 +1,15 @@ +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; + +public final class FantasyInitializer { + 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); + } +} \ No newline at end of file diff --git a/src/main/java/io/lampnet/travelerssuitcase/world/FantasyWorldAccess.java b/src/main/java/io/lampnet/travelerssuitcase/world/FantasyWorldAccess.java new file mode 100644 index 0000000..9a51f2c --- /dev/null +++ b/src/main/java/io/lampnet/travelerssuitcase/world/FantasyWorldAccess.java @@ -0,0 +1,10 @@ +package io.lampnet.travelerssuitcase.world; + +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public interface FantasyWorldAccess { + void fantasy$setTickWhenEmpty(boolean tickWhenEmpty); + + boolean fantasy$shouldTick(); +} \ No newline at end of file diff --git a/src/main/java/io/lampnet/travelerssuitcase/world/PortalChunkGenerator.java b/src/main/java/io/lampnet/travelerssuitcase/world/PortalChunkGenerator.java new file mode 100644 index 0000000..347fb89 --- /dev/null +++ b/src/main/java/io/lampnet/travelerssuitcase/world/PortalChunkGenerator.java @@ -0,0 +1,105 @@ +package io.lampnet.travelerssuitcase.world; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +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.BiomeSource; +import net.minecraft.world.level.biome.FixedBiomeSource; +import net.minecraft.world.level.block.Blocks; +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 net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +public class PortalChunkGenerator extends ChunkGenerator { + public static final Codec CODEC = RecordCodecBuilder.create(instance -> + instance.group( + BiomeSource.CODEC.fieldOf("biome_source").forGetter(ChunkGenerator::getBiomeSource) + ).apply(instance, PortalChunkGenerator::new) + ); + + public PortalChunkGenerator(BiomeSource biomeSource) { + super(biomeSource); + } + + public PortalChunkGenerator(Registry biomeRegistry) { + super(new FixedBiomeSource(biomeRegistry.getHolderOrThrow( + ResourceKey.create(Registries.BIOME, ResourceLocation.fromNamespaceAndPath("travelerssuitcase", "pocket_islands")) + ))); + } + + @Override + protected Codec codec() { + return CODEC; + } + + @Override + public void applyCarvers(WorldGenRegion level, long seed, RandomState randomState, BiomeManager biomeManager, StructureManager structureManager, ChunkAccess chunk, GenerationStep.Carving step) { + // Empty implementation - no carvers needed + } + + @Override + public void buildSurface(WorldGenRegion level, StructureManager structureManager, RandomState randomState, ChunkAccess chunk) { + // Empty implementation - no surface building needed + } + + @Override + public void spawnOriginalMobs(WorldGenRegion level) { + // Empty implementation - no mobs + } + + @Override + public int getGenDepth() { + return 384; + } + + @Override + public CompletableFuture fillFromNoise(Executor executor, Blender blender, RandomState randomState, StructureManager structureManager, ChunkAccess chunk) { + return CompletableFuture.completedFuture(chunk); + } + + @Override + public int getSeaLevel() { + return 63; + } + + @Override + public int getMinY() { + return -64; + } + + @Override + public int getBaseHeight(int x, int z, Heightmap.Types type, LevelHeightAccessor level, RandomState randomState) { + return 0; + } + + @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(); + } + return new NoiseColumn(level.getMinBuildHeight(), column); + } + + @Override + public void addDebugScreenInfo(java.util.List info, RandomState randomState, BlockPos pos) { + // Empty implementation + } +} \ No newline at end of file diff --git a/src/main/java/io/lampnet/travelerssuitcase/world/RemoveFromRegistry.java b/src/main/java/io/lampnet/travelerssuitcase/world/RemoveFromRegistry.java new file mode 100644 index 0000000..dba06b4 --- /dev/null +++ b/src/main/java/io/lampnet/travelerssuitcase/world/RemoveFromRegistry.java @@ -0,0 +1,26 @@ +package io.lampnet.travelerssuitcase.world; + +import net.minecraft.core.MappedRegistry; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public interface RemoveFromRegistry { + @SuppressWarnings("unchecked") + static boolean remove(MappedRegistry registry, ResourceLocation key) { + return ((RemoveFromRegistry) registry).fantasy$remove(key); + } + + @SuppressWarnings("unchecked") + static boolean remove(MappedRegistry registry, T value) { + return ((RemoveFromRegistry) registry).fantasy$remove(value); + } + + boolean fantasy$remove(T value); + + boolean fantasy$remove(ResourceLocation key); + + void fantasy$setFrozen(boolean value); + + boolean fantasy$isFrozen(); +} \ No newline at end of file diff --git a/src/main/java/io/lampnet/travelerssuitcase/world/RuntimeWorld.java b/src/main/java/io/lampnet/travelerssuitcase/world/RuntimeWorld.java new file mode 100644 index 0000000..2023dcb --- /dev/null +++ b/src/main/java/io/lampnet/travelerssuitcase/world/RuntimeWorld.java @@ -0,0 +1,68 @@ +package io.lampnet.travelerssuitcase.world; + +import com.google.common.collect.ImmutableList; +import io.lampnet.travelerssuitcase.mixin.MinecraftServerAccess; +import io.lampnet.travelerssuitcase.util.VoidWorldProgressListener; +import net.minecraft.Util; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.ProgressListener; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.biome.BiomeManager; +import net.minecraft.world.level.dimension.LevelStem; +import org.jetbrains.annotations.Nullable; + +public class RuntimeWorld extends ServerLevel { + final Style style; + private boolean flat; + + protected RuntimeWorld(MinecraftServer server, ResourceKey registryKey, RuntimeWorldConfig config, Style style) { + super( + server, + Util.backgroundExecutor(), + ((MinecraftServerAccess) server).getSession(), + new RuntimeWorldProperties(server.getWorldData().overworldData(), config), + registryKey, + new LevelStem( + server.registryAccess().registryOrThrow(net.minecraft.core.registries.Registries.DIMENSION_TYPE) + .getHolderOrThrow(net.minecraft.world.level.dimension.BuiltinDimensionTypes.OVERWORLD), + config.getGenerator() + ), + VoidWorldProgressListener.INSTANCE, + false, + BiomeManager.obfuscateSeed(config.getSeed()), + ImmutableList.of(), + config.shouldTickTime(), + null + ); + this.style = style; + this.flat = config.isFlat().equals(RuntimeWorldConfig.TriState.TRUE); + } + + @Override + public long getSeed() { + return super.getSeed(); + } + + @Override + public void save(@Nullable ProgressListener progressListener, boolean flush, boolean enabled) { + if (this.style == Style.PERSISTENT || !flush) { + super.save(progressListener, flush, enabled); + } + } + + @Override + public boolean isFlat() { + return this.flat; + } + + public enum Style { + PERSISTENT, + TEMPORARY + } + + public interface Constructor { + RuntimeWorld createWorld(MinecraftServer server, ResourceKey registryKey, RuntimeWorldConfig config, Style style); + } +} \ No newline at end of file diff --git a/src/main/java/io/lampnet/travelerssuitcase/world/RuntimeWorldConfig.java b/src/main/java/io/lampnet/travelerssuitcase/world/RuntimeWorldConfig.java new file mode 100644 index 0000000..a8cbffe --- /dev/null +++ b/src/main/java/io/lampnet/travelerssuitcase/world/RuntimeWorldConfig.java @@ -0,0 +1,199 @@ +package io.lampnet.travelerssuitcase.world; + +import com.google.common.base.Preconditions; +import io.lampnet.travelerssuitcase.util.GameRuleStore; +import net.minecraft.core.Holder; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.Difficulty; +import net.minecraft.world.level.GameRules; +import net.minecraft.world.level.dimension.LevelStem; +import net.minecraft.world.level.chunk.ChunkGenerator; +import org.jetbrains.annotations.Nullable; + +public final class RuntimeWorldConfig { + private long seed = 0; + private ResourceKey dimensionTypeKey = Fantasy.DEFAULT_DIM_TYPE; + private Holder dimensionType; + private ChunkGenerator generator = null; + private boolean shouldTickTime = false; + private long timeOfDay = 6000; + private Difficulty difficulty = Difficulty.NORMAL; + 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 TriState flat = TriState.DEFAULT; + + // Simple TriState implementation + public enum TriState { + DEFAULT, + TRUE, + FALSE; + + public static TriState of(boolean value) { + return value ? TRUE : FALSE; + } + } + + public RuntimeWorldConfig setSeed(long seed) { + this.seed = seed; + return this; + } + + public RuntimeWorldConfig setWorldConstructor(RuntimeWorld.Constructor constructor) { + this.worldConstructor = constructor; + return this; + } + + public RuntimeWorldConfig setDimensionType(Holder dimensionType) { + this.dimensionType = dimensionType; + this.dimensionTypeKey = null; + return this; + } + + public RuntimeWorldConfig setDimensionType(ResourceKey dimensionTypeKey) { + this.dimensionTypeKey = dimensionTypeKey; + this.dimensionType = null; + return this; + } + + public RuntimeWorldConfig setGenerator(ChunkGenerator generator) { + this.generator = generator; + return this; + } + + public RuntimeWorldConfig setShouldTickTime(boolean shouldTickTime) { + this.shouldTickTime = shouldTickTime; + return this; + } + + public RuntimeWorldConfig setTimeOfDay(long timeOfDay) { + this.timeOfDay = timeOfDay; + return this; + } + + public RuntimeWorldConfig setDifficulty(Difficulty difficulty) { + this.difficulty = difficulty; + return this; + } + + public RuntimeWorldConfig setGameRule(GameRules.Key key, boolean value) { + this.gameRules.set(key, value); + return this; + } + + public RuntimeWorldConfig setGameRule(GameRules.Key key, int value) { + this.gameRules.set(key, value); + return this; + } + + public RuntimeWorldConfig setSunnyTime(int sunnyTime) { + this.sunnyTime = sunnyTime; + return this; + } + + public RuntimeWorldConfig setRaining(int rainTime) { + this.raining = rainTime > 0; + this.rainTime = rainTime; + return this; + } + + public RuntimeWorldConfig setRaining(boolean raining) { + this.raining = raining; + return this; + } + + public RuntimeWorldConfig setThundering(int thunderTime) { + this.thundering = thunderTime > 0; + this.thunderTime = thunderTime; + return this; + } + + public RuntimeWorldConfig setThundering(boolean thundering) { + this.thundering = thundering; + return this; + } + + public RuntimeWorldConfig setFlat(TriState state) { + this.flat = state; + return this; + } + + public RuntimeWorldConfig setFlat(boolean state) { + return this.setFlat(TriState.of(state)); + } + + public long getSeed() { + return this.seed; + } + + public LevelStem createDimensionOptions(MinecraftServer server) { + var dimensionTypeHolder = server.registryAccess().registryOrThrow(net.minecraft.core.registries.Registries.DIMENSION_TYPE) + .getHolderOrThrow(net.minecraft.world.level.dimension.BuiltinDimensionTypes.OVERWORLD); + return new LevelStem(dimensionTypeHolder, this.generator); + } + + private Holder resolveDimensionType(MinecraftServer server) { + var dimensionType = this.dimensionType; + if (dimensionType == null) { + dimensionType = server.registryAccess().registryOrThrow(Registries.LEVEL_STEM).getHolder(this.dimensionTypeKey).orElse(null); + Preconditions.checkNotNull(dimensionType, "invalid dimension type " + this.dimensionTypeKey); + } + return dimensionType; + } + + @Nullable + public ChunkGenerator getGenerator() { + return this.generator; + } + + public RuntimeWorld.Constructor getWorldConstructor() { + return this.worldConstructor; + } + + public boolean shouldTickTime() { + return this.shouldTickTime; + } + + public long getTimeOfDay() { + return this.timeOfDay; + } + + public Difficulty getDifficulty() { + return this.difficulty; + } + + public GameRuleStore getGameRules() { + return this.gameRules; + } + + public int getSunnyTime() { + return this.sunnyTime; + } + + public int getRainTime() { + return this.rainTime; + } + + public int getThunderTime() { + return this.thunderTime; + } + + public boolean isRaining() { + return this.raining; + } + + public boolean isThundering() { + return this.thundering; + } + + public TriState isFlat() { + return this.flat; + } +} \ No newline at end of file diff --git a/src/main/java/io/lampnet/travelerssuitcase/world/RuntimeWorldHandle.java b/src/main/java/io/lampnet/travelerssuitcase/world/RuntimeWorldHandle.java new file mode 100644 index 0000000..c2d0a05 --- /dev/null +++ b/src/main/java/io/lampnet/travelerssuitcase/world/RuntimeWorldHandle.java @@ -0,0 +1,37 @@ +package io.lampnet.travelerssuitcase.world; + +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.Level; + +public final class RuntimeWorldHandle { + private final Fantasy fantasy; + private final ServerLevel world; + + RuntimeWorldHandle(Fantasy fantasy, ServerLevel world) { + this.fantasy = fantasy; + this.world = world; + } + + public void setTickWhenEmpty(boolean tickWhenEmpty) { + ((FantasyWorldAccess) this.world).fantasy$setTickWhenEmpty(tickWhenEmpty); + } + + public void delete() { + if (this.world instanceof RuntimeWorld runtimeWorld) { + if (runtimeWorld.style == RuntimeWorld.Style.PERSISTENT) { + this.fantasy.enqueueWorldDeletion(this.world); + } else { + this.fantasy.enqueueWorldUnloading(this.world); + } + } + } + + public ServerLevel asWorld() { + return this.world; + } + + public ResourceKey getRegistryKey() { + return this.world.dimension(); + } +} \ No newline at end of file diff --git a/src/main/java/io/lampnet/travelerssuitcase/world/RuntimeWorldManager.java b/src/main/java/io/lampnet/travelerssuitcase/world/RuntimeWorldManager.java new file mode 100644 index 0000000..93dab1e --- /dev/null +++ b/src/main/java/io/lampnet/travelerssuitcase/world/RuntimeWorldManager.java @@ -0,0 +1,115 @@ +package io.lampnet.travelerssuitcase.world; + +import com.mojang.serialization.Lifecycle; +import io.lampnet.travelerssuitcase.mixin.MinecraftServerAccess; +import net.minecraft.core.MappedRegistry; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.ProgressListener; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.dimension.LevelStem; +import net.minecraft.world.level.storage.LevelStorageSource; +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.IOException; + +final class RuntimeWorldManager { + private final MinecraftServer server; + private final MinecraftServerAccess serverAccess; + + RuntimeWorldManager(MinecraftServer server) { + this.server = server; + this.serverAccess = (MinecraftServerAccess) server; + } + + RuntimeWorld add(ResourceKey worldKey, RuntimeWorldConfig config, RuntimeWorld.Style style) { + LevelStem options = config.createDimensionOptions(this.server); + + if (style == RuntimeWorld.Style.TEMPORARY) { + ((FantasyDimensionOptions) (Object) options).fantasy$setSave(false); + } + ((FantasyDimensionOptions) (Object) options).fantasy$setSaveProperties(false); + + MappedRegistry dimensionsRegistry = getDimensionsRegistry(this.server); + boolean isFrozen = ((RemoveFromRegistry) dimensionsRegistry).fantasy$isFrozen(); + ((RemoveFromRegistry) dimensionsRegistry).fantasy$setFrozen(false); + + var key = ResourceKey.create(net.minecraft.core.registries.Registries.LEVEL_STEM, worldKey.location()); + if(!dimensionsRegistry.containsKey(key)) { + dimensionsRegistry.register(key, options, Lifecycle.stable()); + } + ((RemoveFromRegistry) dimensionsRegistry).fantasy$setFrozen(isFrozen); + + RuntimeWorld world = config.getWorldConstructor().createWorld(this.server, worldKey, config, style); + + this.serverAccess.getWorlds().put(world.dimension(), world); + + net.minecraftforge.common.MinecraftForge.EVENT_BUS.post(new net.minecraftforge.event.level.LevelEvent.Load(world)); + world.tick(() -> true); + + return world; + } + + void delete(ServerLevel world) { + ResourceKey dimensionKey = world.dimension(); + + if (this.serverAccess.getWorlds().remove(dimensionKey, world)) { + net.minecraftforge.common.MinecraftForge.EVENT_BUS.post(new net.minecraftforge.event.level.LevelEvent.Unload(world)); + + MappedRegistry dimensionsRegistry = getDimensionsRegistry(this.server); + RemoveFromRegistry.remove(dimensionsRegistry, dimensionKey.location()); + + LevelStorageSource.LevelStorageAccess session = this.serverAccess.getSession(); + File worldDirectory = session.getDimensionPath(dimensionKey).toFile(); + if (worldDirectory.exists()) { + try { + FileUtils.deleteDirectory(worldDirectory); + } catch (IOException e) { + Fantasy.LOGGER.warn("Failed to delete world directory", e); + try { + FileUtils.forceDeleteOnExit(worldDirectory); + } catch (IOException ignored) { + } + } + } + } + } + + void unload(ServerLevel world) { + ResourceKey dimensionKey = world.dimension(); + + if (this.serverAccess.getWorlds().remove(dimensionKey, world)) { + world.save(new ProgressListener() { + @Override + public void progressStartNoAbort(Component component) {} + + @Override + public void progressStart(Component component) {} + + @Override + public void progressStage(Component component) {} + + @Override + public void progressStagePercentage(int i) {} + + @Override + public void stop() { + net.minecraftforge.common.MinecraftForge.EVENT_BUS.post(new net.minecraftforge.event.level.LevelEvent.Unload(world)); + + MappedRegistry dimensionsRegistry = getDimensionsRegistry(RuntimeWorldManager.this.server); + RemoveFromRegistry.remove(dimensionsRegistry, dimensionKey.location()); + } + }, true, false); + } + } + + private static MappedRegistry getDimensionsRegistry(MinecraftServer server) { + Registry registry = server.registryAccess().registryOrThrow(Registries.LEVEL_STEM); + return (MappedRegistry) registry; + } +} \ No newline at end of file diff --git a/src/main/java/io/lampnet/travelerssuitcase/world/RuntimeWorldProperties.java b/src/main/java/io/lampnet/travelerssuitcase/world/RuntimeWorldProperties.java new file mode 100644 index 0000000..59dd027 --- /dev/null +++ b/src/main/java/io/lampnet/travelerssuitcase/world/RuntimeWorldProperties.java @@ -0,0 +1,222 @@ +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; +import net.minecraft.world.level.GameType; +import net.minecraft.world.level.border.WorldBorder; +import net.minecraft.world.level.storage.ServerLevelData; +import net.minecraft.world.level.timers.TimerQueue; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +public class RuntimeWorldProperties implements ServerLevelData { + private final ServerLevelData parent; + final RuntimeWorldConfig config; + + public RuntimeWorldProperties(ServerLevelData parent, RuntimeWorldConfig config) { + this.parent = parent; + this.config = config; + } + + @Override + public @NotNull String getLevelName() { + return "Runtime Pocket Dimension"; + } + + @Override + public void setThundering(boolean thundering) { + this.config.setThundering(thundering); + } + + @Override + public int getRainTime() { + return this.config.getRainTime(); + } + + @Override + public void setRainTime(int rainTime) { + this.config.setRaining(rainTime); + } + + @Override + public int getThunderTime() { + return this.config.getThunderTime(); + } + + @Override + public void setThunderTime(int thunderTime) { + this.config.setThundering(thunderTime); + } + + @Override + public boolean isThundering() { + return this.config.isThundering(); + } + + @Override + public boolean isRaining() { + return this.config.isRaining(); + } + + @Override + public void setRaining(boolean raining) { + this.config.setRaining(raining); + } + + @Override + public int getClearWeatherTime() { + return this.config.getSunnyTime(); + } + + @Override + public void setClearWeatherTime(int time) { + this.config.setSunnyTime(time); + } + + @Override + public int getWanderingTraderSpawnDelay() { + return parent.getWanderingTraderSpawnDelay(); + } + + @Override + public void setWanderingTraderSpawnDelay(int delay) { + parent.setWanderingTraderSpawnDelay(delay); + } + + @Override + public int getWanderingTraderSpawnChance() { + return parent.getWanderingTraderSpawnChance(); + } + + @Override + public void setWanderingTraderSpawnChance(int chance) { + parent.setWanderingTraderSpawnChance(chance); + } + + @Override + public UUID getWanderingTraderId() { + return parent.getWanderingTraderId(); + } + + @Override + public void setWanderingTraderId(UUID id) { + parent.setWanderingTraderId(id); + } + + @Override + public @NotNull GameType getGameType() { + return parent.getGameType(); + } + + @Override + public void setGameType(@NotNull GameType type) {} + + @Override + public boolean isHardcore() { + return parent.isHardcore(); + } + + @Override + public boolean getAllowCommands() { + return parent.getAllowCommands(); + } + + @Override + public boolean isInitialized() { + return true; + } + + @Override + public void setInitialized(boolean initialized) {} + + @Override + public @NotNull GameRules getGameRules() { + GameRules rules = new GameRules(); + this.config.getGameRules().applyTo(rules, null); + return rules; + } + + @Override + public @NotNull WorldBorder.Settings getWorldBorder() { + return parent.getWorldBorder(); + } + + @Override + public void setWorldBorder(@NotNull WorldBorder.Settings settings) {} + + @Override + public @NotNull Difficulty getDifficulty() { + return this.config.getDifficulty(); + } + + @Override + public boolean isDifficultyLocked() { + return parent.isDifficultyLocked(); + } + + @Override + public @NotNull TimerQueue getScheduledEvents() { + return new TimerQueue<>(null); + } + + @Override + public int getXSpawn() { + return 0; + } + + @Override + public int getYSpawn() { + return 64; + } + + @Override + public int getZSpawn() { + return 0; + } + + @Override + public float getSpawnAngle() { + return 0; + } + + @Override + public void setXSpawn(int x) {} + + @Override + public void setYSpawn(int y) {} + + @Override + public void setZSpawn(int z) {} + + @Override + public void setSpawnAngle(float angle) {} + + @Override + public long getGameTime() { + return parent.getGameTime(); + } + + @Override + public long getDayTime() { + return config.getTimeOfDay(); + } + + @Override + public void setGameTime(long time) {} + + @Override + public void setDayTime(long time) { + if (config.shouldTickTime()) { + config.setTimeOfDay(time); + } + } + + @Override + public void fillCrashReportCategory(@NotNull CrashReportCategory category, net.minecraft.world.level.LevelHeightAccessor context) { + parent.fillCrashReportCategory(category, context); + } +} \ No newline at end of file diff --git a/src/main/resources/data/travelerssuitcase/advancements/smaller_on_the_outside.json b/src/main/resources/data/travelerssuitcase/advancements/smaller_on_the_outside.json new file mode 100644 index 0000000..3aec126 --- /dev/null +++ b/src/main/resources/data/travelerssuitcase/advancements/smaller_on_the_outside.json @@ -0,0 +1,28 @@ +{ + "parent": "minecraft:adventure/root", + "display": { + "icon": { + "item": "travelerssuitcase:keystone" + }, + "title": { + "text": "Smaller on the Outside" + }, + "description": { + "text": "Enter a pocket dimension." + }, + "frame": "task", + "show_toast": true, + "announce_to_chat": true, + "hidden": false + }, + "criteria": { + "enter_pocket_dimension": { + "trigger": "travelerssuitcase:enter_pocket_dimension" + } + }, + "requirements": [ + [ + "enter_pocket_dimension" + ] + ] +} \ No newline at end of file diff --git a/src/main/resources/data/travelerssuitcase/dimension/pocket_dimension_.json b/src/main/resources/data/travelerssuitcase/dimension/pocket_dimension_.json deleted file mode 100644 index efdb077..0000000 --- a/src/main/resources/data/travelerssuitcase/dimension/pocket_dimension_.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "type": "travelerssuitcase:pocket_dimension_type", - "generator": { - "type": "minecraft:flat", - "settings": { - "biome": "travelerssuitcase:pocket_islands", - "layers": [ - { - "block": "travelerssuitcase:portal", - "height": 1 - } - ], - "structure_overrides": [] - } - } -} \ No newline at end of file diff --git a/src/main/resources/data/travelerssuitcase/structures/pocket_island_01.nbt b/src/main/resources/data/travelerssuitcase/structures/pocket_island_01.nbt index bdbfe1c..6e4021b 100644 Binary files a/src/main/resources/data/travelerssuitcase/structures/pocket_island_01.nbt and b/src/main/resources/data/travelerssuitcase/structures/pocket_island_01.nbt differ diff --git a/src/main/resources/travelerssuitcase.mixins.json b/src/main/resources/travelerssuitcase.mixins.json index f348105..e2a87a9 100644 --- a/src/main/resources/travelerssuitcase.mixins.json +++ b/src/main/resources/travelerssuitcase.mixins.json @@ -5,6 +5,11 @@ "compatibilityLevel": "JAVA_8", "refmap": "travelerssuitcase.refmap.json", "mixins": [ + "MinecraftServerAccess", + "LevelStemMixin", + "MappedRegistryMixin", + "ServerWorldMixin", + "ServerChunkManagerMixin" ], "client": [ ],