almost 1.2.7 parity, no updates in initial dimension currently

This commit is contained in:
candle 2025-07-06 17:14:37 -04:00
parent eb90d2f6ec
commit 54c9024615
37 changed files with 2492 additions and 240 deletions

View File

@ -38,7 +38,7 @@ mod_name=TravelersSuitcase
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. # The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
mod_license=MIT mod_license=MIT
# The mod version. See https://semver.org/ # 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. # 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. # This should match the base package used for the mod sources.
# See https://maven.apache.org/guides/mini/guide-naming-conventions.html # See https://maven.apache.org/guides/mini/guide-naming-conventions.html

View File

@ -19,7 +19,7 @@ public class Config {
static final ForgeConfigSpec SPEC = BUILDER.build(); static final ForgeConfigSpec SPEC = BUILDER.build();
private static boolean validateItemName(final Object obj) { 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 @SubscribeEvent

View File

@ -1,21 +1,21 @@
package io.lampnet.travelerssuitcase; package io.lampnet.travelerssuitcase;
import io.lampnet.travelerssuitcase.block.entity.SuitcaseBlockEntity; import io.lampnet.travelerssuitcase.block.entity.SuitcaseBlockEntity;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag; import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.Tag; import net.minecraft.nbt.Tag;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.saveddata.SavedData; import net.minecraft.server.level.ServerLevel;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.Level; 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.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
public class SuitcaseRegistryState extends SavedData { 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<String, Map<String, BlockPos>> registry = new HashMap<>(); private final Map<String, Map<String, BlockPos>> registry = new HashMap<>();
public SuitcaseRegistryState() { public SuitcaseRegistryState() {
@ -23,62 +23,86 @@ public class SuitcaseRegistryState extends SavedData {
} }
@Override @Override
public @NotNull CompoundTag save(@NotNull CompoundTag nbt) { public CompoundTag save(CompoundTag nbt) {
ListTag keystonesList = new ListTag(); CompoundTag top = new CompoundTag();
for (Map.Entry<String, Map<String, BlockPos>> keystoneEntry : registry.entrySet()) { for (Map.Entry<String, Map<String, BlockPos>> entry : registry.entrySet()) {
CompoundTag keystoneNbt = new CompoundTag(); String keystone = entry.getKey();
keystoneNbt.putString("KeystoneName", keystoneEntry.getKey()); Map<String, BlockPos> playerMap = entry.getValue();
ListTag playersList = getPlayersList(keystoneEntry);
keystoneNbt.put("Players", playersList); ListTag playerList = new ListTag();
keystonesList.add(keystoneNbt); for (Map.Entry<String, BlockPos> 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);
} }
nbt.put("SuitcaseRegistry", keystonesList); top.put(keystone, playerList);
}
nbt.put("RegistryEntries", top);
return nbt; return nbt;
} }
private static @NotNull ListTag getPlayersList(Map.Entry<String, Map<String, BlockPos>> keystoneEntry) {
ListTag playersList = new ListTag();
for (Map.Entry<String, BlockPos> 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) { public static SuitcaseRegistryState load(CompoundTag nbt) {
SuitcaseRegistryState state = new SuitcaseRegistryState(); SuitcaseRegistryState data = new SuitcaseRegistryState();
if (nbt.contains("SuitcaseRegistry")) { if (nbt.contains("RegistryEntries", Tag.TAG_COMPOUND)) {
ListTag keystonesList = nbt.getList("SuitcaseRegistry", Tag.TAG_COMPOUND); CompoundTag top = nbt.getCompound("RegistryEntries");
for (int i = 0; i < keystonesList.size(); i++) { for (String keystone : top.getAllKeys()) {
CompoundTag keystoneNbt = keystonesList.getCompound(i); ListTag playerList = top.getList(keystone, Tag.TAG_COMPOUND);
String keystoneName = keystoneNbt.getString("KeystoneName"); Map<String, BlockPos> playerMap = new HashMap<>();
Map<String, BlockPos> playersMap = new HashMap<>(); for (int i = 0; i < playerList.size(); i++) {
ListTag playersList = keystoneNbt.getList("Players", Tag.TAG_COMPOUND); CompoundTag rec = playerList.getCompound(i);
for (int j = 0; j < playersList.size(); j++) { String uuid = rec.getString("UUID");
CompoundTag playerNbt = playersList.getCompound(j); int x = rec.getInt("X");
String uuid = playerNbt.getString("UUID"); int y = rec.getInt("Y");
BlockPos pos = new BlockPos( int z = rec.getInt("Z");
playerNbt.getInt("X"), playerMap.put(uuid, new BlockPos(x, y, z));
playerNbt.getInt("Y"), }
playerNbt.getInt("Z") data.registry.put(keystone, playerMap);
}
}
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
); );
playersMap.put(uuid, pos); data.syncToStaticRegistry();
} }
state.registry.put(keystoneName, playersMap);
} public static void onRegistryChanged(MinecraftServer server) {
} Level overworld = server.getLevel(Level.OVERWORLD);
return state; 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) { public static SuitcaseRegistryState getState(MinecraftServer server) {
return Objects.requireNonNull(server.getLevel(Level.OVERWORLD)).getDataStorage() return Objects.requireNonNull(((ServerLevel) server.getLevel(Level.OVERWORLD)).getDataStorage())
.computeIfAbsent(SuitcaseRegistryState::load, SuitcaseRegistryState::new, REGISTRY_KEY); .computeIfAbsent(SuitcaseRegistryState::load, SuitcaseRegistryState::new, DATA_NAME);
} }
public Map<String, Map<String, BlockPos>> getRegistry() { public Map<String, Map<String, BlockPos>> getRegistry() {

View File

@ -1,22 +1,102 @@
package io.lampnet.travelerssuitcase; 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.ModBlocks;
import io.lampnet.travelerssuitcase.block.SuitcaseBlock;
import io.lampnet.travelerssuitcase.block.entity.ModBlockEntities; 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.ModItemGroups;
import io.lampnet.travelerssuitcase.item.ModItems; import io.lampnet.travelerssuitcase.item.ModItems;
import io.lampnet.travelerssuitcase.block.entity.SuitcaseBlockEntity; import io.lampnet.travelerssuitcase.world.Fantasy;
import io.lampnet.travelerssuitcase.SuitcaseRegistryState; 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.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.IEventBus;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; 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) @Mod(TravelersSuitcase.MODID)
public class TravelersSuitcase { public class TravelersSuitcase {
public static final String MODID = "travelerssuitcase"; public static final String MODID = "travelerssuitcase";
public static final Logger LOGGER = LogManager.getLogger(MODID); public static final Logger LOGGER = LogManager.getLogger(MODID);
public static final EnterPocketDimensionCriterion ENTER_POCKET_DIMENSION = CriteriaTriggers.register(new EnterPocketDimensionCriterion());
private static boolean canCaptureHostile = false;
private static final Set<EntityType<?>> entityBlacklist = new HashSet<>();
// Public accessor methods for the configuration
public static boolean getCanCaptureHostile() {
return canCaptureHostile;
}
public static void setCanCaptureHostile(boolean value) {
canCaptureHostile = value;
}
public static Set<EntityType<?>> getEntityBlacklist() {
return new HashSet<>(entityBlacklist);
}
public static void addToBlacklist(EntityType<?> entityType) {
entityBlacklist.add(entityType);
}
public static boolean removeFromBlacklist(EntityType<?> entityType) {
return entityBlacklist.remove(entityType);
}
public static boolean isBlacklisted(EntityType<?> entityType) {
return entityBlacklist.contains(entityType);
}
public static void clearBlacklist() {
entityBlacklist.clear();
}
public TravelersSuitcase() { public TravelersSuitcase() {
IEventBus modEventBus = FMLJavaModLoadingContext.get().getModEventBus(); IEventBus modEventBus = FMLJavaModLoadingContext.get().getModEventBus();
@ -26,55 +106,392 @@ public class TravelersSuitcase {
ModItemGroups.register(modEventBus); ModItemGroups.register(modEventBus);
ModBlockEntities.register(modEventBus); ModBlockEntities.register(modEventBus);
MinecraftForge.EVENT_BUS.addListener(this::onWorldLoad); modEventBus.addListener(this::commonSetup);
MinecraftForge.EVENT_BUS.addListener(this::onServerStarting);
MinecraftForge.EVENT_BUS.addListener(this::onServerStopping);
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) { private void commonSetup(final FMLCommonSetupEvent event) {
if (event.getLevel() instanceof net.minecraft.server.level.ServerLevel world) { // Register chunk generators during setup phase
if (world.dimension().location().getNamespace().equals(MODID)) { new FantasyInitializer().onInitialize();
String dimensionName = world.dimension().location().getPath(); }
java.nio.file.Path structureMarkerPath = world.getServer().getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT)
@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("data")
.resolve(MODID) .resolve(MODID)
.resolve("pending_structures") .resolve("dimension_registry")
.resolve(dimensionName + ".txt"); .resolve("registry.txt");
if (java.nio.file.Files.exists(structureMarkerPath)) {
if (!Files.exists(registryFile)) {
return;
}
var biomeRegistry = event.getServer().registryAccess().registryOrThrow(Registries.BIOME);
long seed = event.getServer().overworld().getSeed();
try { try {
net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate template = world.getServer().getStructureManager() for (String dimName : Files.readAllLines(registryFile)) {
.get(new net.minecraft.resources.ResourceLocation(MODID, "pocket_island_01")) ResourceLocation worldId = ResourceLocation.fromNamespaceAndPath(MODID, dimName);
.orElse(null);
if (template != null) { var voidGen = new PortalChunkGenerator(biomeRegistry);
net.minecraft.core.BlockPos pos = new net.minecraft.core.BlockPos(0, 64, 0);
template.placeInWorld( var cfg = new RuntimeWorldConfig()
world, .setGenerator(voidGen)
pos, .setSeed(seed);
pos, RuntimeWorldHandle handle = Fantasy.get(event.getServer()).getOrOpenPersistentWorld(worldId, cfg);
new net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings(),
world.getRandom(), // Ensure the pocket dimension ticks even when empty to prevent entity tick issues
net.minecraft.world.level.block.Block.UPDATE_ALL handle.setTickWhenEmpty(true);
}
} catch (IOException e) {
TravelersSuitcase.LOGGER.error("Failed to reload pocket dimensions", e);
}
}
@SubscribeEvent
public static void onRegisterCommands(RegisterCommandsEvent event) {
LiteralArgumentBuilder<CommandSourceStack> 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<Level> 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;
})
)
); );
java.nio.file.Files.delete(structureMarkerPath); event.getDispatcher().register(builder);
} }
} catch (java.io.IOException e) {
LOGGER.error("Failed to place structure", e); @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<Level> 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<Boolean, Entity> 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<Boolean, Entity> 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);
} }
} }
private void onServerStarting(net.minecraftforge.event.server.ServerStartingEvent event) { @SubscribeEvent
SuitcaseRegistryState state = SuitcaseRegistryState.getState(event.getServer()); public void onUseItem(PlayerInteractEvent.RightClickItem event) {
SuitcaseBlockEntity.initializeSuitcaseRegistry(state.getRegistry()); Level world = event.getLevel();
} var player = event.getEntity();
var hand = event.getHand();
var stack = player.getItemInHand(hand);
private void onServerStopping(net.minecraftforge.event.server.ServerStoppingEvent event) { if (world.isClientSide || hand != InteractionHand.MAIN_HAND) return;
SuitcaseRegistryState state = SuitcaseRegistryState.getState(event.getServer());
SuitcaseBlockEntity.saveSuitcaseRegistryTo(state.getRegistry()); if (!(world instanceof ServerLevel sw)) return;
state.setDirty(); 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);
}
} }
} }

View File

@ -70,7 +70,7 @@ public class ModBlocks {
.sound(SoundType.LODESTONE) .sound(SoundType.LODESTONE)
.noOcclusion() .noOcclusion()
.lightLevel(getPortalLuminance()) .lightLevel(getPortalLuminance())
.strength(-1f))); .strength(5.0f)));
private static <T extends Block> RegistryObject<T> registerBlock(String name, Supplier<T> blockSupplier) { private static <T extends Block> RegistryObject<T> registerBlock(String name, Supplier<T> blockSupplier) {
RegistryObject<T> block = BLOCKS.register(name, blockSupplier); RegistryObject<T> block = BLOCKS.register(name, blockSupplier);

View File

@ -28,10 +28,12 @@ import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.nbt.Tag; import net.minecraft.nbt.Tag;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import net.minecraft.world.level.storage.loot.LootParams;
public class PocketPortalBlock extends Block { public class PocketPortalBlock extends Block {
private static final Map<String, PlayerPositionData> LAST_KNOWN_POSITIONS = new HashMap<>(); private static final Map<String, PlayerPositionData> LAST_KNOWN_POSITIONS = new HashMap<>();
@ -57,6 +59,12 @@ public class PocketPortalBlock extends Block {
super(properties); super(properties);
} }
@Override
public List<ItemStack> getDrops(BlockState state, LootParams.Builder builder) {
return Collections.singletonList(new ItemStack(this));
}
public static void storePlayerPosition(ServerPlayer player) { public static void storePlayerPosition(ServerPlayer player) {
LAST_KNOWN_POSITIONS.put( LAST_KNOWN_POSITIONS.put(
player.getUUID().toString(), player.getUUID().toString(),
@ -125,7 +133,7 @@ public class PocketPortalBlock extends Block {
updateItemLore(stack, remainingPlayers); updateItemLore(stack, remainingPlayers);
} }
} }
SuitcaseBlockEntity.removeSuitcaseEntry(keystoneName, player.getUUID().toString()); SuitcaseBlockEntity.removeSuitcaseEntry(keystoneName, player.getUUID().toString(), player.getServer());
} }
@Override @Override
@ -162,13 +170,13 @@ public class PocketPortalBlock extends Block {
// Fallback: Take them to spawn // Fallback: Take them to spawn
if (!teleported) { 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, teleportToPosition(world, player, overworld,
overworld.getSharedSpawnPos().getX() + 0.5, overworld.getSharedSpawnPos().getX() + 0.5,
overworld.getSharedSpawnPos().getY() + 1.0, overworld.getSharedSpawnPos().getY() + 1.0,
overworld.getSharedSpawnPos().getZ() + 0.5, 0, 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()); LAST_KNOWN_POSITIONS.remove(player.getUUID().toString());
} }
} }
@ -278,8 +286,10 @@ public class PocketPortalBlock extends Block {
private void teleportToPosition(Level world, ServerPlayer player, ServerLevel targetWorld, private void teleportToPosition(Level world, ServerPlayer player, ServerLevel targetWorld,
double x, double y, double z, float yaw, float pitch) { double x, double y, double z, float yaw, float pitch) {
if (!world.isClientSide) {
player.teleportTo(targetWorld, x, y, z, yaw, pitch); player.teleportTo(targetWorld, x, y, z, yaw, pitch);
} }
}
private void updateItemLore(ItemStack stack, int playerCount) { private void updateItemLore(ItemStack stack, int playerCount) {
if (stack.hasTag()) { if (stack.hasTag()) {
@ -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");
}
}
} }
} }

View File

@ -3,6 +3,7 @@ package io.lampnet.travelerssuitcase.block;
import io.lampnet.travelerssuitcase.TravelersSuitcase; import io.lampnet.travelerssuitcase.TravelersSuitcase;
import io.lampnet.travelerssuitcase.block.entity.ModBlockEntities; import io.lampnet.travelerssuitcase.block.entity.ModBlockEntities;
import io.lampnet.travelerssuitcase.block.entity.SuitcaseBlockEntity; import io.lampnet.travelerssuitcase.block.entity.SuitcaseBlockEntity;
import io.lampnet.travelerssuitcase.data.PlayerEntryData;
import io.lampnet.travelerssuitcase.item.KeystoneItem; import io.lampnet.travelerssuitcase.item.KeystoneItem;
import net.minecraft.world.level.block.BaseEntityBlock; import net.minecraft.world.level.block.BaseEntityBlock;
import net.minecraft.world.level.block.Block; 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.Containers;
import net.minecraft.world.item.DyeColor; 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.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@ -243,20 +248,37 @@ public class SuitcaseBlock extends BaseEntityBlock {
return; return;
} }
String dimensionName = "pocket_dimension_" + keystoneName; String dimensionName = "pocket_dimension_" + keystoneName;
ResourceLocation dimensionRL = new ResourceLocation(TravelersSuitcase.MODID, dimensionName); ResourceLocation dimensionRL = ResourceLocation.fromNamespaceAndPath(TravelersSuitcase.MODID, dimensionName);
ResourceKey<Level> dimensionKey = ResourceKey.create(Registries.DIMENSION, dimensionRL); ResourceKey<Level> dimensionKey = ResourceKey.create(Registries.DIMENSION, dimensionRL);
ServerLevel targetWorld = Objects.requireNonNull(world.getServer()).getLevel(dimensionKey); ServerLevel targetWorld = Objects.requireNonNull(world.getServer()).getLevel(dimensionKey);
if (targetWorld != null) { if (targetWorld != null) {
boolean wasFirstTime = suitcase.isFirstTimeEntering(player);
suitcase.playerEntered(player); suitcase.playerEntered(player);
if (wasFirstTime) {
TravelersSuitcase.ENTER_POCKET_DIMENSION.trigger(player);
}
player.stopRiding(); player.stopRiding();
player.setDeltaMovement(Vec3.ZERO); player.setDeltaMovement(Vec3.ZERO);
player.fallDistance = 0f; 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, SoundEvents.BUNDLE_DROP_CONTENTS,
SoundSource.PLAYERS, 2.0f, 1.0f); SoundSource.PLAYERS,
} else { 2.0f, 1.0f
TravelersSuitcase.LOGGER.warn("Target dimension {} not found for keystone {}", dimensionRL, keystoneName); );
} }
} }
} }

View File

@ -16,6 +16,9 @@ import org.jetbrains.annotations.Nullable;
import java.util.*; import java.util.*;
import io.lampnet.travelerssuitcase.SuitcaseRegistryState;
import net.minecraft.server.MinecraftServer;
public class SuitcaseBlockEntity extends BlockEntity { public class SuitcaseBlockEntity extends BlockEntity {
private String boundKeystoneName; private String boundKeystoneName;
private boolean isLocked = false; private boolean isLocked = false;
@ -70,6 +73,11 @@ public class SuitcaseBlockEntity extends BlockEntity {
return dimensionLocked; return dimensionLocked;
} }
private static final java.util.Set<java.util.UUID> PLAYERS_WHO_ENTERED = new java.util.HashSet<>();
public boolean isFirstTimeEntering(ServerPlayer player) {
return !PLAYERS_WHO_ENTERED.contains(player.getUUID());
}
public void playerEntered(ServerPlayer player) { public void playerEntered(ServerPlayer player) {
enteredPlayers.removeIf(data -> data.uuid.equals(player.getUUID().toString())); enteredPlayers.removeIf(data -> data.uuid.equals(player.getUUID().toString()));
EnteredPlayerData data = new EnteredPlayerData( EnteredPlayerData data = new EnteredPlayerData(
@ -79,6 +87,9 @@ public class SuitcaseBlockEntity extends BlockEntity {
this.worldPosition this.worldPosition
); );
enteredPlayers.add(data); enteredPlayers.add(data);
PLAYERS_WHO_ENTERED.add(player.getUUID());
if (boundKeystoneName != null) { if (boundKeystoneName != null) {
Map<String, BlockPos> suitcases = SUITCASE_REGISTRY.computeIfAbsent( Map<String, BlockPos> suitcases = SUITCASE_REGISTRY.computeIfAbsent(
boundKeystoneName, k -> new HashMap<>() boundKeystoneName, k -> new HashMap<>()
@ -87,6 +98,11 @@ public class SuitcaseBlockEntity extends BlockEntity {
} }
PocketPortalBlock.storePlayerPosition(player); PocketPortalBlock.storePlayerPosition(player);
setChangedAndNotify(); setChangedAndNotify();
MinecraftServer server = player.getServer();
if (server != null) {
SuitcaseRegistryState.onRegistryChanged(server);
}
} }
public EnteredPlayerData getExitPosition(String playerUuid) { public EnteredPlayerData getExitPosition(String playerUuid) {
@ -182,13 +198,16 @@ public class SuitcaseBlockEntity extends BlockEntity {
return (suitcases != null) ? suitcases.get(playerUuid) : null; 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<String, BlockPos> suitcases = SUITCASE_REGISTRY.get(keystoneName); Map<String, BlockPos> suitcases = SUITCASE_REGISTRY.get(keystoneName);
if (suitcases != null) { if (suitcases != null) {
suitcases.remove(playerUuid); suitcases.remove(playerUuid);
if (suitcases.isEmpty()) { if (suitcases.isEmpty()) {
SUITCASE_REGISTRY.remove(keystoneName); SUITCASE_REGISTRY.remove(keystoneName);
} }
if (server != null) {
SuitcaseRegistryState.onRegistryChanged(server);
}
} }
} }

View File

@ -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<EnterPocketDimensionCriterion.TriggerInstance> {
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);
}
}
}

View File

@ -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; }
}

View File

@ -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; }
}

View File

@ -1,26 +1,42 @@
package io.lampnet.travelerssuitcase.item; package io.lampnet.travelerssuitcase.item;
import io.lampnet.travelerssuitcase.TravelersSuitcase; 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.ChatFormatting;
import net.minecraft.world.item.TooltipFlag; import net.minecraft.core.registries.Registries;
import net.minecraft.world.item.enchantment.Enchantments; 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.Entity;
import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item; import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
import net.minecraft.nbt.CompoundTag; import net.minecraft.world.item.TooltipFlag;
import net.minecraft.server.MinecraftServer; import net.minecraft.world.item.enchantment.Enchantments;
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.level.Level; 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.world.level.storage.LevelResource;
import net.minecraft.resources.ResourceLocation;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import net.minecraft.world.item.ItemStack.TooltipPart;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
@ -39,35 +55,143 @@ public class KeystoneItem extends Item {
@Override @Override
public @NotNull InteractionResultHolder<ItemStack> use(@NotNull Level world, Player player, @NotNull InteractionHand hand) { public @NotNull InteractionResultHolder<ItemStack> use(@NotNull Level world, Player player, @NotNull InteractionHand hand) {
ItemStack stack = player.getItemInHand(hand); ItemStack stack = player.getItemInHand(hand);
String keystoneName = stack.hasCustomHoverName() ? stack.getHoverName().getString().toLowerCase() : "";
String defaultName = "item.travelerssuitcase.keystone"; // If already enchanted, do nothing
if (!stack.hasCustomHoverName() || keystoneName.isEmpty() || keystoneName.equals(Component.translatable(defaultName).getString().toLowerCase())) {
return InteractionResultHolder.pass(stack);
}
if (world.isClientSide) {
return InteractionResultHolder.success(stack);
}
if (stack.isEnchanted()) { if (stack.isEnchanted()) {
return InteractionResultHolder.pass(stack); return InteractionResultHolder.pass(stack);
} }
String dimensionName = "pocket_dimension_" + keystoneName.replaceAll("[^a-z0-9_]", "");
createDimension(world.getServer(), dimensionName); // If no valid custom name, do nothing
stack.enchant(Enchantments.BINDING_CURSE, 1); if (!isValidKeystone(stack)) {
stack.hideTooltipPart(TooltipPart.ENCHANTMENTS); return InteractionResultHolder.pass(stack);
stack.hideTooltipPart(TooltipPart.MODIFIERS); }
CompoundTag nbt = stack.getOrCreateTag();
nbt.putInt("RepairCost", 32767); // On client side, just return success
world.playSound(null, player.getX(), player.getY(), player.getZ(), if (world.isClientSide) {
SoundEvents.AMETHYST_CLUSTER_FALL, SoundSource.PLAYERS, 2.0F, 2.0F);
return InteractionResultHolder.success(stack); 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<String> dims = new HashSet<>();
if (Files.exists(registryFile)) {
dims.addAll(Files.readAllLines(registryFile));
}
// add + save only if new
if (dims.add(dimensionName)) {
Files.write(registryFile, dims);
}
} catch (IOException e) {
TravelersSuitcase.LOGGER.error("Failed to write dimension registry", e);
}
}
public static boolean isValidKeystone(ItemStack stack) { public static boolean isValidKeystone(ItemStack stack) {
String keystoneName = stack.hasCustomHoverName() ? stack.getHoverName().getString().toLowerCase() : ""; String keystoneName = stack.hasCustomHoverName() ? stack.getHoverName().getString().toLowerCase() : "";
String defaultNameKey = "item.travelerssuitcase.keystone"; String defaultNameKey = "item.travelerssuitcase.keystone";
return stack.hasCustomHoverName() && !keystoneName.isEmpty() && return stack.hasCustomHoverName() && !keystoneName.isEmpty() &&
!keystoneName.equals(Component.translatable(defaultNameKey).getString().toLowerCase()) && !keystoneName.equals(Component.translatable(defaultNameKey).getString().toLowerCase());
stack.isEnchanted();
} }
@Override @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) { public void inventoryTick(ItemStack stack, @NotNull Level world, @NotNull Entity entity, int slot, boolean selected) {
String keystoneName = stack.hasCustomHoverName() ? stack.getHoverName().getString().toLowerCase() : ""; String keystoneName = stack.hasCustomHoverName() ? stack.getHoverName().getString().toLowerCase() : "";
String defaultNameKey = "item.travelerssuitcase.keystone"; String defaultNameKey = "item.travelerssuitcase.keystone";
if (!stack.hasCustomHoverName() || keystoneName.isEmpty() || boolean hasValidName = isValidKeystone(stack);
keystoneName.equals(Component.translatable(defaultNameKey).getString().toLowerCase())) {
if (!hasValidName) {
CompoundTag nbt = stack.getOrCreateTag(); CompoundTag nbt = stack.getOrCreateTag();
nbt.putInt("CustomModelData", 1); nbt.putInt("CustomModelData", 1);
} else if (stack.getTag() != null && stack.getTag().contains("CustomModelData")) { } else if (stack.getTag() != null && stack.getTag().contains("CustomModelData")) {
stack.getTag().remove("CustomModelData"); stack.getTag().remove("CustomModelData");
} }
}
private boolean createDimension(MinecraftServer server, String dimensionName) { // Auto-enchant and create dimension when item gets a valid custom name
if (server == null) { if (!world.isClientSide() && hasValidName && !stack.isEnchanted()) {
TravelersSuitcase.LOGGER.error("Failed to create dimension: " + dimensionName + " (MinecraftServer instance is null)"); String dimensionName = "pocket_dimension_" + keystoneName.replaceAll("[^a-z0-9_]", "");
return false; createOrLoadPersistentDimension(world.getServer(), dimensionName);
}
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);
ResourceLocation dimensionKeyLocation = new ResourceLocation("travelerssuitcase", dimensionName); stack.enchant(Enchantments.BINDING_CURSE, 1);
boolean isDimensionRegistered = server.levelKeys().stream() CompoundTag nbt = stack.getOrCreateTag();
.anyMatch(key -> key.location().equals(dimensionKeyLocation)); nbt.putInt("RepairCost", 32767);
Path dimensionRegistryPath = server.getWorldPath(LevelResource.ROOT) // Play sound effect if entity is a player
.resolve("data") if (entity instanceof Player player) {
.resolve("travelerssuitcase") world.playSound(null, player.getX(), player.getY(), player.getZ(),
.resolve("dimension_registry"); SoundEvents.AMETHYST_CLUSTER_FALL, SoundSource.PLAYERS, 2.0F, 2.0F);
Files.createDirectories(dimensionRegistryPath);
Path dimensionRegistryFile = dimensionRegistryPath.resolve("registry.txt");
Set<String> registeredDimensionsInFile;
if (Files.exists(dimensionRegistryFile)) {
registeredDimensionsInFile = new HashSet<>(Files.readAllLines(dimensionRegistryFile));
} else {
registeredDimensionsInFile = new HashSet<>();
}
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) { if (!world.isClientSide() && stack.hasTag() && stack.getTag().contains("Enchantments")) {
Path structureMarkerPath = server.getWorldPath(LevelResource.ROOT) CompoundTag nbt = stack.getOrCreateTag();
.resolve("data") if (nbt.getInt("RepairCost") < 32767) {
.resolve("travelerssuitcase") nbt.putInt("RepairCost", 32767);
.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);
} }
} }
} }

View File

@ -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;
}
}

View File

@ -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<T> implements RemoveFromRegistry<T> {
@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;
}
}

View File

@ -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<ResourceKey<Level>, ServerLevel> getWorlds();
@Accessor("storageSource")
LevelStorageSource.LevelStorageAccess getSession();
}

View File

@ -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<Boolean> 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
}
}

View File

@ -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<ServerPlayer> 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;
}
}

View File

@ -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<T> extends MappedRegistry<T> {
private final Registry<T> source;
private final Predicate<T> check;
public FilteredRegistry(Registry<T> source, Predicate<T> check) {
super(source.key(), source.registryLifecycle(), false);
this.source = source;
this.check = check;
}
public Registry<T> getSource() {
return this.source;
}
@Nullable
@Override
public ResourceLocation getKey(T value) {
return check.test(value) ? this.source.getKey(value) : null;
}
@Override
public Optional<ResourceKey<T>> getResourceKey(T entry) {
return check.test(entry) ? this.source.getResourceKey(entry) : Optional.empty();
}
@Override
public int getId(@Nullable T value) {
return check.test(value) ? this.source.getId(value) : -1;
}
public T get(int index) {
return this.source.byId(index);
}
@Override
public int size() {
return (int) this.source.stream().filter(check).count();
}
@Nullable
@Override
public T get(@Nullable ResourceKey<T> key) {
T value = this.source.get(key);
return (value != null && check.test(value)) ? value : null;
}
@Nullable
@Override
public T get(@Nullable ResourceLocation id) {
T value = this.source.get(id);
return (value != null && check.test(value)) ? value : null;
}
@Override
public Lifecycle lifecycle(T entry) {
return this.source.lifecycle(entry);
}
@Override
public @NotNull Set<ResourceLocation> keySet() {
Set<ResourceLocation> keys = new HashSet<>();
this.source.keySet().forEach(key -> {
T value = this.source.get(key);
if(value != null && check.test(value)) {
keys.add(key);
}
});
return keys;
}
@Override
public @NotNull Set<Map.Entry<ResourceKey<T>, T>> entrySet() {
Set<Map.Entry<ResourceKey<T>, T>> set = new HashSet<>();
for (Map.Entry<ResourceKey<T>, T> e : this.source.entrySet()) {
if (this.check.test(e.getValue())) {
set.add(e);
}
}
return set;
}
@Override
public boolean containsKey(ResourceLocation id) {
T value = this.source.get(id);
return value != null && check.test(value);
}
@Override
public boolean containsKey(ResourceKey<T> key) {
T value = this.source.get(key);
return value != null && check.test(value);
}
@Override
public @NotNull Iterator<T> iterator() {
return Iterators.filter(this.source.iterator(), e -> this.check.test(e));
}
// Methods below are for WritableRegistry, we don't support writing to a filtered registry
@Override
public Holder.Reference<T> register(ResourceKey<T> resourceKey, T object, Lifecycle lifecycle) {
throw new UnsupportedOperationException("Cannot register to a filtered registry");
}
public Holder.Reference<T> registerOrNah(ResourceKey<T> resourceKey, T object, Lifecycle lifecycle) {
throw new UnsupportedOperationException("Cannot register to a filtered registry");
}
@Override
public Holder.Reference<T> registerMapping(int i, ResourceKey<T> resourceKey, T object, Lifecycle lifecycle) {
throw new UnsupportedOperationException("Cannot register to a filtered registry");
}
@Override
public Holder.Reference<T> getHolderOrThrow(ResourceKey<T> resourceKey) {
return this.source.getHolderOrThrow(resourceKey);
}
@Override
public Optional<Holder.Reference<T>> getHolder(ResourceKey<T> resourceKey) {
return this.source.getHolder(resourceKey);
}
@Override
public Stream<Holder.Reference<T>> holders() {
return this.source.holders().filter(h -> check.test(h.value()));
}
}

View File

@ -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<GameRules.Key<GameRules.BooleanValue>, Boolean> booleanRules = new HashMap<>();
private final Map<GameRules.Key<GameRules.IntegerValue>, Integer> intRules = new HashMap<>();
public void set(GameRules.Key<GameRules.BooleanValue> key, boolean value) {
this.booleanRules.put(key, value);
}
public void set(GameRules.Key<GameRules.IntegerValue> 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<GameRules.Key<GameRules.BooleanValue>, Boolean> entry : this.booleanRules.entrySet()) {
GameRules.BooleanValue rule = rules.getRule(entry.getKey());
rule.set(entry.getValue(), server);
}
for (Map.Entry<GameRules.Key<GameRules.IntegerValue>, Integer> entry : this.intRules.entrySet()) {
GameRules.IntegerValue rule = rules.getRule(entry.getKey());
rule.set(entry.getValue(), server);
}
}
}

View File

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

View File

@ -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<VoidChunkGenerator> CODEC = Codec.unit(VoidChunkGenerator::new);
public VoidChunkGenerator(Registry<Biome> biomeRegistry) {
super(new FixedBiomeSource(biomeRegistry.getHolderOrThrow(Biomes.THE_VOID)));
}
public VoidChunkGenerator() {
super(new FixedBiomeSource(null));
}
@Override
protected @NotNull Codec<? extends ChunkGenerator> codec() {
return CODEC;
}
@Override
public void applyCarvers(WorldGenRegion worldGenRegion, long seed, RandomState randomState, BiomeManager biomeManager, StructureManager structureManager, ChunkAccess chunkAccess, GenerationStep.Carving carving) {
}
@Override
public void spawnOriginalMobs(WorldGenRegion worldGenRegion) {
}
@Override
public int getGenDepth() {
return 384;
}
@Override
public @NotNull CompletableFuture<ChunkAccess> fillFromNoise(Executor executor, Blender blender, RandomState randomState, StructureManager structureManager, ChunkAccess chunkAccess) {
return CompletableFuture.completedFuture(chunkAccess);
}
@Override
public int getSeaLevel() {
return 0;
}
@Override
public int getMinY() {
return 0;
}
@Override
public int getBaseHeight(int i, int j, Heightmap.Types types, LevelHeightAccessor levelHeightAccessor, RandomState randomState) {
return 0;
}
@Override
public @NotNull NoiseColumn getBaseColumn(int i, int j, LevelHeightAccessor levelHeightAccessor, RandomState randomState) {
return new NoiseColumn(0, new BlockState[0]);
}
@Override
public void addDebugScreenInfo(List<String> list, RandomState randomState, BlockPos blockPos) {
}
@Override
public void buildSurface(WorldGenRegion worldGenRegion, StructureManager structureManager, RandomState randomState,
ChunkAccess chunkAccess) {
// No surface to build for void chunks
}
}

View File

@ -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
}
}

View File

@ -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<LevelStem> 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<ServerLevel> deletionQueue = new ReferenceOpenHashSet<>();
private final Set<ServerLevel> 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<ServerLevel> deletionQueue = this.deletionQueue;
if (!deletionQueue.isEmpty()) {
deletionQueue.removeIf(this::tickDeleteWorld);
}
Set<ServerLevel> 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<Level> 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<Level> worldKey = ResourceKey.create(Registries.DIMENSION, key);
return this.worldManager.add(worldKey, config, RuntimeWorld.Style.PERSISTENT);
}
private RuntimeWorld addTemporaryWorld(RuntimeWorldConfig config) {
ResourceKey<Level> 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<ServerPlayer> 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<RuntimeWorld> temporaryWorlds = this.collectTemporaryWorlds();
for (RuntimeWorld temporary : temporaryWorlds) {
this.kickPlayers(temporary);
this.worldManager.delete(temporary);
}
}
private List<RuntimeWorld> collectTemporaryWorlds() {
List<RuntimeWorld> 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);
}
}

View File

@ -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<LevelStem> SAVE_PREDICATE = (e) -> ((FantasyDimensionOptions) (Object) e).fantasy$getSave();
Predicate<LevelStem> 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();
}

View File

@ -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);
}
}

View File

@ -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();
}

View File

@ -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<PortalChunkGenerator> 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<Biome> biomeRegistry) {
super(new FixedBiomeSource(biomeRegistry.getHolderOrThrow(
ResourceKey.create(Registries.BIOME, ResourceLocation.fromNamespaceAndPath("travelerssuitcase", "pocket_islands"))
)));
}
@Override
protected Codec<? extends ChunkGenerator> 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<ChunkAccess> 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<String> info, RandomState randomState, BlockPos pos) {
// Empty implementation
}
}

View File

@ -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<T> {
@SuppressWarnings("unchecked")
static <T> boolean remove(MappedRegistry<T> registry, ResourceLocation key) {
return ((RemoveFromRegistry<T>) registry).fantasy$remove(key);
}
@SuppressWarnings("unchecked")
static <T> boolean remove(MappedRegistry<T> registry, T value) {
return ((RemoveFromRegistry<T>) registry).fantasy$remove(value);
}
boolean fantasy$remove(T value);
boolean fantasy$remove(ResourceLocation key);
void fantasy$setFrozen(boolean value);
boolean fantasy$isFrozen();
}

View File

@ -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<Level> 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<Level> registryKey, RuntimeWorldConfig config, Style style);
}
}

View File

@ -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<LevelStem> dimensionTypeKey = Fantasy.DEFAULT_DIM_TYPE;
private Holder<LevelStem> dimensionType;
private ChunkGenerator generator = null;
private boolean shouldTickTime = false;
private long timeOfDay = 6000;
private Difficulty difficulty = Difficulty.NORMAL;
private 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<LevelStem> dimensionType) {
this.dimensionType = dimensionType;
this.dimensionTypeKey = null;
return this;
}
public RuntimeWorldConfig setDimensionType(ResourceKey<LevelStem> 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<GameRules.BooleanValue> key, boolean value) {
this.gameRules.set(key, value);
return this;
}
public RuntimeWorldConfig setGameRule(GameRules.Key<GameRules.IntegerValue> 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<LevelStem> 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;
}
}

View File

@ -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<Level> getRegistryKey() {
return this.world.dimension();
}
}

View File

@ -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<Level> 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<LevelStem> 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<Level> 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<LevelStem> 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<Level> 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<LevelStem> dimensionsRegistry = getDimensionsRegistry(RuntimeWorldManager.this.server);
RemoveFromRegistry.remove(dimensionsRegistry, dimensionKey.location());
}
}, true, false);
}
}
private static MappedRegistry<LevelStem> getDimensionsRegistry(MinecraftServer server) {
Registry<LevelStem> registry = server.registryAccess().registryOrThrow(Registries.LEVEL_STEM);
return (MappedRegistry<LevelStem>) registry;
}
}

View File

@ -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<MinecraftServer> 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);
}
}

View File

@ -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"
]
]
}

View File

@ -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": []
}
}
}

View File

@ -5,6 +5,11 @@
"compatibilityLevel": "JAVA_8", "compatibilityLevel": "JAVA_8",
"refmap": "travelerssuitcase.refmap.json", "refmap": "travelerssuitcase.refmap.json",
"mixins": [ "mixins": [
"MinecraftServerAccess",
"LevelStemMixin",
"MappedRegistryMixin",
"ServerWorldMixin",
"ServerChunkManagerMixin"
], ],
"client": [ "client": [
], ],