Compare commits

..

4 Commits

35 changed files with 2505 additions and 496 deletions

1
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

@ -1,22 +1,85 @@
package io.lampnet.travelerssuitcase;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import io.lampnet.travelerssuitcase.block.ModBlocks;
import io.lampnet.travelerssuitcase.block.SuitcaseBlock;
import io.lampnet.travelerssuitcase.block.entity.ModBlockEntities;
import io.lampnet.travelerssuitcase.block.entity.SuitcaseBlockEntity;
import io.lampnet.travelerssuitcase.criterion.EnterPocketDimensionCriterion;
import io.lampnet.travelerssuitcase.data.MobEntryData;
import io.lampnet.travelerssuitcase.data.PlayerEntryData;
import io.lampnet.travelerssuitcase.item.KeystoneItem;
import io.lampnet.travelerssuitcase.item.ModItemGroups;
import io.lampnet.travelerssuitcase.item.ModItems;
import io.lampnet.travelerssuitcase.block.entity.SuitcaseBlockEntity;
import io.lampnet.travelerssuitcase.SuitcaseRegistryState;
import io.lampnet.travelerssuitcase.world.Fantasy;
import io.lampnet.travelerssuitcase.world.FantasyInitializer;
import io.lampnet.travelerssuitcase.world.PortalChunkGenerator;
import io.lampnet.travelerssuitcase.world.RuntimeWorldConfig;
import net.minecraft.advancements.CriteriaTriggers;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.monster.Enemy;
import net.minecraft.world.entity.animal.Wolf;
import net.minecraft.world.item.BlockItem;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.RegisterCommandsEvent;
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
import net.minecraftforge.event.server.ServerStartingEvent;
import net.minecraftforge.eventbus.api.IEventBus;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import net.minecraftforge.fml.ModLoadingContext;
import net.minecraftforge.fml.config.ModConfig;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Set;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.entity.Entity;
import io.lampnet.travelerssuitcase.world.RuntimeWorldHandle;
@Mod(TravelersSuitcase.MODID)
public class TravelersSuitcase {
public static final String MODID = "travelerssuitcase";
public static final Logger LOGGER = LogManager.getLogger(MODID);
public static final EnterPocketDimensionCriterion ENTER_POCKET_DIMENSION = CriteriaTriggers.register(new EnterPocketDimensionCriterion());
public static boolean getCanCaptureHostile() {
return Config.canCaptureHostile();
}
public static Set<EntityType<?>> getEntityBlacklist() {
return Config.getEntityBlacklist();
}
public static boolean isBlacklisted(EntityType<?> entityType) {
return Config.isEntityBlacklisted(entityType);
}
public TravelersSuitcase() {
IEventBus modEventBus = FMLJavaModLoadingContext.get().getModEventBus();
@ -25,56 +88,450 @@ public class TravelersSuitcase {
ModBlocks.register(modEventBus);
ModItemGroups.register(modEventBus);
ModBlockEntities.register(modEventBus);
FantasyInitializer.register(modEventBus);
MinecraftForge.EVENT_BUS.addListener(this::onWorldLoad);
MinecraftForge.EVENT_BUS.addListener(this::onServerStarting);
MinecraftForge.EVENT_BUS.addListener(this::onServerStopping);
// Register configuration
ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, Config.SPEC);
LOGGER.info("Initializing " + MODID);
modEventBus.addListener(this::commonSetup);
MinecraftForge.EVENT_BUS.register(this);
MinecraftForge.EVENT_BUS.register(TravelersSuitcase.class);
}
private void onWorldLoad(net.minecraftforge.event.level.LevelEvent.Load event) {
if (event.getLevel() instanceof net.minecraft.server.level.ServerLevel world) {
if (world.dimension().location().getNamespace().equals(MODID)) {
String dimensionName = world.dimension().location().getPath();
java.nio.file.Path structureMarkerPath = world.getServer().getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT)
private void commonSetup(final FMLCommonSetupEvent event) {
new FantasyInitializer().onInitialize();
}
@SubscribeEvent
public static void onServerStarting(ServerStartingEvent event) {
SuitcaseRegistryState.onServerStart(event.getServer());
Path registryFile = event.getServer().getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT)
.resolve("data")
.resolve(MODID)
.resolve("pending_structures")
.resolve(dimensionName + ".txt");
if (java.nio.file.Files.exists(structureMarkerPath)) {
.resolve("dimension_registry")
.resolve("registry.txt");
if (!Files.exists(registryFile)) {
return;
}
var biomeRegistry = event.getServer().registryAccess().registryOrThrow(Registries.BIOME);
long seed = event.getServer().overworld().getSeed();
try {
net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate template = world.getServer().getStructureManager()
.get(new net.minecraft.resources.ResourceLocation(MODID, "pocket_island_01"))
.orElse(null);
if (template != null) {
net.minecraft.core.BlockPos pos = new net.minecraft.core.BlockPos(0, 64, 0);
template.placeInWorld(
world,
pos,
pos,
new net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings(),
world.getRandom(),
net.minecraft.world.level.block.Block.UPDATE_ALL
for (String dimName : Files.readAllLines(registryFile)) {
ResourceLocation worldId = ResourceLocation.fromNamespaceAndPath(MODID, dimName);
var voidGen = new PortalChunkGenerator(biomeRegistry);
var cfg = new RuntimeWorldConfig()
.setGenerator(voidGen)
.setSeed(seed);
RuntimeWorldHandle handle = Fantasy.get(event.getServer()).getOrOpenPersistentWorld(worldId, cfg);
handle.setTickWhenEmpty(Config.tickWhenEmpty());
}
} 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("getPlayerEntry")
.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;
}
PlayerEntryData data = PlayerEntryData.get(world);
Vec3 pos = data.getEntryPos();
float yaw = data.getEntryYaw();
float pitch = data.getEntryPitch();
src.sendSuccess(() -> Component.literal(
String.format("§aPlayer entry: %.2f, %.2f, %.2f (yaw: %.1f, pitch: %.1f)",
pos.x(), pos.y(), pos.z(), yaw, pitch)
), false);
return 1;
})
)
.then(Commands.literal("getMobEntry")
.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;
}
MobEntryData data = MobEntryData.get(world);
Vec3 pos = data.getEntryPos();
float yaw = data.getEntryYaw();
float pitch = data.getEntryPitch();
src.sendSuccess(() -> Component.literal(
String.format("§aMob entry: %.2f, %.2f, %.2f (yaw: %.1f, pitch: %.1f)",
pos.x(), pos.y(), pos.z(), yaw, pitch)
), 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;
}
PlayerEntryData playerData = PlayerEntryData.get(targetWorld);
playerData.resetToDefault();
src.sendSuccess(() -> Component.literal(
"§aPlayer entry reset for pocket dimension '" + dimSuffix + "'"
), false);
return 1;
})
)
)
.then(Commands.literal("resetMobEntry")
.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;
}
MobEntryData mobData = MobEntryData.get(targetWorld);
mobData.setEntry(new Vec3(35.5, 85.0, 16.5), 0f, 0f);
src.sendSuccess(() -> Component.literal(
"§aMob 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))
.executes(ctx -> {
boolean enabled = Config.canCaptureHostile();
ctx.getSource().sendSuccess(() -> Component.literal(
"§7Hostile mob capture is currently: " + (enabled ? "§aEnabled" : "§cDisabled") +
"\n§7(Configure in config file)"
), 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")) {
src.sendFailure(Component.literal("§cUnknown entity type: " + entityString));
return 0;
}
if (entityType == EntityType.PLAYER) {
src.sendFailure(Component.literal("§cCannot blacklist players"));
return 0;
}
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;
}
src.sendSuccess(() -> Component.literal("§eEntity blacklist is managed through configuration files"), false);
return 1;
} catch (Exception e) {
src.sendFailure(Component.literal("§cInvalid entity identifier: " + entityString));
return 0;
}
})
)
)
.then(Commands.literal("list")
.executes(ctx -> {
var src = ctx.getSource();
Set<EntityType<?>> blacklist = Config.getEntityBlacklist();
if (blacklist.isEmpty()) {
src.sendSuccess(() -> Component.literal("§7No entities are blacklisted"), false);
} else {
src.sendSuccess(() -> Component.literal("§aBlacklisted entities:"), false);
blacklist.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();
src.sendSuccess(() -> Component.literal("§eEntity blacklist is managed through configuration files"), 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 (Config.isEntityBlacklisted(mob.getType())) {
player.displayClientMessage(Component.literal("§c☒"), true);
world.playSound(null, player.getX(), player.getY(), player.getZ(), SoundEvents.ZOMBIE_ATTACK_WOODEN_DOOR, SoundSource.PLAYERS, 0.3f, 1.5f);
event.setCancellationResult(InteractionResult.FAIL);
event.setCanceled(true);
return;
}
boolean isHostile = mob instanceof Enemy || (mob instanceof Wolf wolf && wolf.isAngryAtAllPlayers(world));
if (!Config.canCaptureHostile() && isHostile) {
player.displayClientMessage(Component.literal("§c☒"), true);
world.playSound(null, player.getX(), player.getY(), player.getZ(), SoundEvents.ZOMBIE_ATTACK_WOODEN_DOOR, SoundSource.PLAYERS, 0.3f, 1.5f);
event.setCancellationResult(InteractionResult.FAIL);
event.setCanceled(true);
return;
}
MobEntryData data = MobEntryData.get(targetWorld);
Vec3 dest = data.getEntryPos();
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;
}
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) {
SuitcaseRegistryState state = SuitcaseRegistryState.getState(event.getServer());
SuitcaseBlockEntity.initializeSuitcaseRegistry(state.getRegistry());
}
@SubscribeEvent
public void onUseItem(PlayerInteractEvent.RightClickItem event) {
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) {
SuitcaseRegistryState state = SuitcaseRegistryState.getState(event.getServer());
SuitcaseBlockEntity.saveSuitcaseRegistryTo(state.getRegistry());
state.setDirty();
if (world.isClientSide || hand != InteractionHand.MAIN_HAND) return;
if (!(world instanceof ServerLevel sw)) return;
var id = sw.dimension().location();
if (!id.getNamespace().equals(MODID) || !id.getPath().startsWith("pocket_dimension_")) return;
if (stack.getItem() == Items.BONE) {
Vec3 pos = player.position();
float yaw = player.getYRot();
float pitch = player.getXRot();
PlayerEntryData.get(sw).setEntry(pos, yaw, pitch);
player.sendSystemMessage(Component.literal(String.format("§aPlayer entry location set to %.1f, %.1f, %.1f", pos.x, pos.y, pos.z)));
event.setCancellationResult(InteractionResult.SUCCESS);
event.setCanceled(true);
} else if (stack.getItem() == Items.LEAD) {
Vec3 pos = player.position();
float yaw = player.getYRot();
float pitch = player.getXRot();
MobEntryData.get(sw).setEntry(pos, yaw, pitch);
player.sendSystemMessage(Component.literal(String.format("§aMob entry location set to %.1f, %.1f, %.1f", pos.x, pos.y, pos.z)));
event.setCancellationResult(InteractionResult.SUCCESS);
event.setCanceled(true);
}
}
}

View File

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

View File

@ -4,12 +4,7 @@ import io.lampnet.travelerssuitcase.block.entity.SuitcaseBlockEntity;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.item.BlockItem;
import net.minecraft.world.item.ItemStack;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.StringTag;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.sounds.SoundEvents;
@ -17,116 +12,34 @@ import net.minecraft.sounds.SoundSource;
import net.minecraft.network.chat.Component;
import net.minecraft.ChatFormatting;
import net.minecraft.core.BlockPos;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.phys.Vec3;
import net.minecraft.world.level.Level;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.nbt.Tag;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import net.minecraft.world.level.storage.loot.LootParams;
public class PocketPortalBlock extends Block {
private static final Map<String, PlayerPositionData> LAST_KNOWN_POSITIONS = new HashMap<>();
private static final int SEARCH_RADIUS_CHUNKS = 12;
public static class PlayerPositionData {
public final double x;
public final double y;
public final double z;
public final float yaw;
public final float pitch;
public final long timestamp;
public PlayerPositionData(double x, double y, double z, float yaw, float pitch) {
this.x = x;
this.y = y;
this.z = z;
this.yaw = yaw;
this.pitch = pitch;
this.timestamp = System.currentTimeMillis();
}
}
public PocketPortalBlock(BlockBehaviour.Properties properties) {
super(properties);
}
public static void storePlayerPosition(ServerPlayer player) {
LAST_KNOWN_POSITIONS.put(
player.getUUID().toString(),
new PlayerPositionData(player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot())
);
@Override
public List<ItemStack> getDrops(BlockState state, LootParams.Builder builder) {
return Collections.singletonList(new ItemStack(this));
}
private boolean attemptPlayerInventorySuitcaseTeleport(Level world, ServerLevel overworld, ServerPlayer player, String keystoneName) {
for (ServerPlayer serverPlayer : Objects.requireNonNull(world.getServer()).getPlayerList().getPlayers()) {
if (scanPlayerInventoryForSuitcase(serverPlayer, player, keystoneName, world, overworld)) {
return true;
}
}
return false;
}
private boolean scanPlayerInventoryForSuitcase(ServerPlayer inventoryOwner, ServerPlayer exitingPlayer,
String keystoneName, Level world, ServerLevel overworld) {
for (int i = 0; i < inventoryOwner.getInventory().getContainerSize(); i++) {
ItemStack stack = inventoryOwner.getInventory().getItem(i);
if (isSuitcaseItemWithKeystone(stack, keystoneName)) {
cleanUpSuitcaseItemNbt(stack, exitingPlayer, keystoneName);
inventoryOwner.getInventory().setItem(i, stack);
teleportToPosition(world, exitingPlayer, overworld,
inventoryOwner.getX(), inventoryOwner.getY() + 1.0, inventoryOwner.getZ(),
exitingPlayer.getYRot(), exitingPlayer.getXRot());
return true;
}
}
return false;
}
private boolean isSuitcaseItemWithKeystone(ItemStack stack, String keystoneName) {
if (stack.isEmpty() || !(stack.getItem() instanceof BlockItem) ||
!(((BlockItem) stack.getItem()).getBlock() instanceof SuitcaseBlock)) {
return false;
}
if (!stack.hasTag()) return false;
CompoundTag beTag = BlockItem.getBlockEntityData(stack);
if (beTag == null) return false;
return beTag.contains("BoundKeystone") && keystoneName.equals(beTag.getString("BoundKeystone"));
}
private void cleanUpSuitcaseItemNbt(ItemStack stack, ServerPlayer player, String keystoneName) {
if (!stack.hasTag()) return;
CompoundTag beTag = BlockItem.getBlockEntityData(stack);
if (beTag == null) return;
if (beTag.contains("EnteredPlayers", Tag.TAG_LIST)) {
ListTag playersList = beTag.getList("EnteredPlayers", Tag.TAG_COMPOUND);
ListTag newPlayersList = new ListTag();
boolean playerFound = false;
for (int i = 0; i < playersList.size(); i++) {
CompoundTag playerData = playersList.getCompound(i);
if (!player.getUUID().toString().equals(playerData.getString("UUID"))) {
newPlayersList.add(playerData);
} else {
playerFound = true;
}
}
if (playerFound) {
beTag.put("EnteredPlayers", newPlayersList);
int remainingPlayers = newPlayersList.size();
updateItemLore(stack, remainingPlayers);
}
}
SuitcaseBlockEntity.removeSuitcaseEntry(keystoneName, player.getUUID().toString());
}
@Override
public void entityInside(@NotNull BlockState state, Level world, @NotNull BlockPos pos, @NotNull Entity entity) {
@ -140,36 +53,16 @@ public class PocketPortalBlock extends Block {
ServerLevel overworld = Objects.requireNonNull(world.getServer()).getLevel(Level.OVERWORLD);
if (overworld == null) return;
boolean teleported = false;
// Method 1: Try to teleport to the original suitcase block entity
teleported = attemptSuitcaseTeleport(world, overworld, player, keystoneName);
// Method 2: Try to find suitcase in a player's inventory (new method)
if (!teleported) {
teleported = attemptPlayerInventorySuitcaseTeleport(world, overworld, player, keystoneName);
}
// Method 3: Try to find the suitcase as an item entity in the world
if (!teleported) {
teleported = attemptSuitcaseItemTeleport(world, overworld, player, keystoneName);
}
// Method 4: Try to use player's last known position
if (!teleported) {
teleported = attemptLastKnownPositionTeleport(world, overworld, player);
}
// Fallback: Take them to spawn
if (!teleported) {
player.displayClientMessage(Component.literal("§cCouldn't find your return point. Taking you to spawn.").withStyle(ChatFormatting.RED), false);
// Primary method: Try to teleport to the suitcase block they entered from
if (!attemptSuitcaseTeleport(world, overworld, player, keystoneName)) {
// Fallback: Take them to spawn if suitcase block cannot be found
player.displayClientMessage(Component.literal("§c...").withStyle(ChatFormatting.RED), false);
teleportToPosition(world, player, overworld,
overworld.getSharedSpawnPos().getX() + 0.5,
overworld.getSharedSpawnPos().getY() + 1.0,
overworld.getSharedSpawnPos().getZ() + 0.5, 0, 0);
}
SuitcaseBlockEntity.removeSuitcaseEntry(keystoneName, player.getUUID().toString());
LAST_KNOWN_POSITIONS.remove(player.getUUID().toString());
SuitcaseBlockEntity.removeSuitcaseEntry(keystoneName, player.getUUID().toString(), world.getServer());
}
}
}
@ -180,10 +73,11 @@ public class PocketPortalBlock extends Block {
player.fallDistance = 0f;
}
// Method 1: Try to teleport to the original suitcase block entity
// Teleport to the suitcase block coordinates they entered from
private boolean attemptSuitcaseTeleport(Level world, ServerLevel overworld, ServerPlayer player, String keystoneName) {
BlockPos suitcasePos = SuitcaseBlockEntity.findSuitcasePosition(keystoneName, player.getUUID().toString());
if (suitcasePos == null) return false;
ChunkPos suitcaseChunkPos = new ChunkPos(suitcasePos);
overworld.setChunkForced(suitcaseChunkPos.x, suitcaseChunkPos.z, true);
try {
@ -191,7 +85,10 @@ public class PocketPortalBlock extends Block {
if (targetEntity instanceof SuitcaseBlockEntity suitcase) {
SuitcaseBlockEntity.EnteredPlayerData exitData = suitcase.getExitPosition(player.getUUID().toString());
if (exitData != null) {
teleportToPosition(world, player, overworld, exitData.x(), exitData.y(), exitData.z(), exitData.yaw(), player.getXRot());
// Teleport to the suitcase block position (not the original player position)
teleportToPosition(world, player, overworld,
suitcasePos.getX() + 0.5, suitcasePos.getY() + 1.0, suitcasePos.getZ() + 0.5,
exitData.yaw(), player.getXRot());
Objects.requireNonNull(world.getServer()).execute(() -> {
overworld.setChunkForced(suitcaseChunkPos.x, suitcaseChunkPos.z, false);
});
@ -204,105 +101,24 @@ public class PocketPortalBlock extends Block {
return false;
}
// Method 2: Try to find the suitcase as an item entity in the world
private boolean attemptSuitcaseItemTeleport(Level world, ServerLevel overworld, ServerPlayer player, String keystoneName) {
BlockPos searchCenter;
BlockPos suitcasePos = SuitcaseBlockEntity.findSuitcasePosition(keystoneName, player.getUUID().toString());
if (suitcasePos != null) {
searchCenter = suitcasePos;
} else {
PlayerPositionData lastPos = LAST_KNOWN_POSITIONS.get(player.getUUID().toString());
if (lastPos != null) {
searchCenter = new BlockPos((int)lastPos.x, (int)lastPos.y, (int)lastPos.z);
} else {
searchCenter = overworld.getSharedSpawnPos();
}
}
int centerX = searchCenter.getX() >> 4;
int centerZ = searchCenter.getZ() >> 4;
for (int radius = 0; radius <= SEARCH_RADIUS_CHUNKS; radius++) {
for (int x = centerX - radius; x <= centerX + radius; x++) {
for (int z = centerZ - radius; z <= centerZ + radius; z++) {
if (radius > 0 && x > centerX - radius && x < centerX + radius &&
z > centerZ - radius && z < centerZ + radius) {
continue;
}
if (!overworld.hasChunk(x, z)) {
continue;
}
LevelChunk chunk = overworld.getChunk(x, z);
List<ItemEntity> itemEntities = overworld.getEntitiesOfClass(
ItemEntity.class,
new AABB(chunk.getPos().getMinBlockX(), overworld.getMinBuildHeight(), chunk.getPos().getMinBlockZ(),
chunk.getPos().getMaxBlockX(), overworld.getMaxBuildHeight(), chunk.getPos().getMaxBlockZ()),
itemEntity -> {
ItemStack stack = itemEntity.getItem();
CompoundTag beTag = BlockItem.getBlockEntityData(stack);
if (beTag == null) return false;
return beTag.contains("BoundKeystone") &&
keystoneName.equals(beTag.getString("BoundKeystone"));
}
);
if (!itemEntities.isEmpty()) {
ItemEntity suitcaseItemEntity = itemEntities.get(0);
cleanUpSuitcaseItemNbt(suitcaseItemEntity.getItem(), player, keystoneName);
suitcaseItemEntity.setItem(suitcaseItemEntity.getItem());
teleportToPosition(world, player, overworld,
suitcaseItemEntity.getX(), suitcaseItemEntity.getY() + 1.0, suitcaseItemEntity.getZ(),
player.getYRot(), player.getXRot());
return true;
}
}
}
}
return false;
}
// Method 3: Try to use player's last known position
private boolean attemptLastKnownPositionTeleport(Level world, ServerLevel overworld, ServerPlayer player) {
PlayerPositionData lastPos = LAST_KNOWN_POSITIONS.get(player.getUUID().toString());
if (lastPos != null) {
long currentTime = System.currentTimeMillis();
if (currentTime - lastPos.timestamp > 10 * 60 * 1000) {
return false;
}
player.displayClientMessage(Component.literal("§6Returning to your last known position."), false);
teleportToPosition(world, player, overworld,
lastPos.x, lastPos.y, lastPos.z,
lastPos.yaw, lastPos.pitch);
return true;
}
return false;
}
private void teleportToPosition(Level world, ServerPlayer player, ServerLevel targetWorld,
double x, double y, double z, float yaw, float pitch) {
player.teleportTo(targetWorld, x, y, z, yaw, pitch);
if (!world.isClientSide) {
// Use changeDimension instead of teleportTo to avoid "Player moved wrongly" errors
player.changeDimension(targetWorld, new net.minecraftforge.common.util.ITeleporter() {
@Override
public Entity placeEntity(Entity entity, ServerLevel currentWorld, ServerLevel destWorld, float placementYaw, java.util.function.Function<Boolean, Entity> repositionEntity) {
Entity e = repositionEntity.apply(false);
e.teleportTo(x, y, z);
e.setYRot(yaw);
e.setXRot(pitch);
return e;
}
});
}
}
private void updateItemLore(ItemStack stack, int playerCount) {
if (stack.hasTag()) {
assert stack.getTag() != null;
if (stack.getTag().contains("display")) {
CompoundTag display = stack.getTag().getCompound("display");
if (display.contains("Lore", Tag.TAG_LIST)) {
ListTag lore = display.getList("Lore", Tag.TAG_STRING);
ListTag newLore = new ListTag();
for (int i = 0; i < lore.size(); i++) {
String loreStr = lore.getString(i);
if (!loreStr.contains("traveler")) {
newLore.add(lore.get(i));
}
}
if (playerCount > 0) {
Component warningText = Component.literal("§c⚠ Contains " + playerCount + " traveler(s)!")
.withStyle(ChatFormatting.RED);
newLore.add(0, StringTag.valueOf(Component.Serializer.toJson(warningText)));
}
display.put("Lore", newLore);
}
}
}
}
}

View File

@ -3,6 +3,7 @@ package io.lampnet.travelerssuitcase.block;
import io.lampnet.travelerssuitcase.TravelersSuitcase;
import io.lampnet.travelerssuitcase.block.entity.ModBlockEntities;
import io.lampnet.travelerssuitcase.block.entity.SuitcaseBlockEntity;
import io.lampnet.travelerssuitcase.data.PlayerEntryData;
import io.lampnet.travelerssuitcase.item.KeystoneItem;
import net.minecraft.world.level.block.BaseEntityBlock;
import net.minecraft.world.level.block.Block;
@ -44,10 +45,12 @@ import net.minecraft.world.phys.Vec3;
import net.minecraft.world.phys.shapes.CollisionContext;
import net.minecraft.world.phys.shapes.VoxelShape;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.Containers;
import net.minecraft.world.item.DyeColor;
import net.minecraft.network.protocol.game.ClientboundStopSoundPacket;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -62,6 +65,8 @@ public class SuitcaseBlock extends BaseEntityBlock {
private final static VoxelShape SHAPE_E = Block.box(2, 0, 0, 14, 4, 16);
private final static VoxelShape SHAPE_W = Block.box(2, 0, 0, 14, 4, 16);
public SuitcaseBlock(BlockBehaviour.Properties properties) {
super(properties);
this.registerDefaultState(this.stateDefinition.any()
@ -114,42 +119,6 @@ public class SuitcaseBlock extends BaseEntityBlock {
@Override
public void onRemove(BlockState state, @NotNull Level world, @NotNull BlockPos pos, BlockState newState, boolean isMoving) {
if (!state.is(newState.getBlock())) {
BlockEntity blockEntity = world.getBlockEntity(pos);
if (blockEntity instanceof SuitcaseBlockEntity suitcase) {
ItemStack itemStack = new ItemStack(this);
String boundKeystone = suitcase.getBoundKeystoneName();
if (boundKeystone != null) {
CompoundTag beNbt = new CompoundTag();
suitcase.saveAdditional(beNbt);
if (!beNbt.isEmpty()) {
BlockItem.setBlockEntityData(itemStack, ModBlockEntities.SUITCASE_BLOCK_ENTITY.get(), beNbt);
}
CompoundTag display = itemStack.getOrCreateTagElement("display");
ListTag lore = new ListTag();
String displayName = boundKeystone.replace("_", " ");
Component boundText;
if (suitcase.isLocked()) {
boundText = Component.literal("Bound to: §k" + displayName)
.withStyle(ChatFormatting.GRAY);
} else {
boundText = Component.literal("Bound to: " + displayName)
.withStyle(ChatFormatting.GRAY);
}
Component lockText = Component.literal(suitcase.isLocked() ? "§cLocked" : "§aUnlocked")
.withStyle(ChatFormatting.GRAY);
if (!suitcase.getEnteredPlayers().isEmpty()) {
Component warningText = Component.literal("§c⚠ Contains " + suitcase.getEnteredPlayers().size() + " Traveler's!")
.withStyle(ChatFormatting.RED);
lore.add(StringTag.valueOf(Component.Serializer.toJson(warningText)));
}
lore.add(StringTag.valueOf(Component.Serializer.toJson(boundText)));
lore.add(StringTag.valueOf(Component.Serializer.toJson(lockText)));
display.put("Lore", lore);
}
Containers.dropItemStack(world, pos.getX(), pos.getY(), pos.getZ(), itemStack);
}
super.onRemove(state, world, pos, newState, isMoving);
}
}
@ -243,24 +212,72 @@ public class SuitcaseBlock extends BaseEntityBlock {
return;
}
String dimensionName = "pocket_dimension_" + keystoneName;
ResourceLocation dimensionRL = new ResourceLocation(TravelersSuitcase.MODID, dimensionName);
ResourceLocation dimensionRL = ResourceLocation.fromNamespaceAndPath(TravelersSuitcase.MODID, dimensionName);
ResourceKey<Level> dimensionKey = ResourceKey.create(Registries.DIMENSION, dimensionRL);
ServerLevel targetWorld = Objects.requireNonNull(world.getServer()).getLevel(dimensionKey);
if (targetWorld != null) {
boolean wasFirstTime = suitcase.isFirstTimeEntering(player);
suitcase.playerEntered(player);
if (wasFirstTime) {
TravelersSuitcase.ENTER_POCKET_DIMENSION.trigger(player);
}
player.stopRiding();
player.setDeltaMovement(Vec3.ZERO);
player.fallDistance = 0f;
player.teleportTo(targetWorld, 17.5, 97, 9.5, player.getYRot(), player.getXRot());
world.playSound(null, pos,
PlayerEntryData ped = PlayerEntryData.get(targetWorld);
// Use default position (17, 97, 9) unless manually configured
if (!ped.isManuallySet()) {
ped.setEntryAutomatic(new Vec3(17.5, 97.0, 9.5), 0f, 0f);
}
Vec3 dest = ped.getEntryPos();
float entryYaw = ped.getEntryYaw();
float pitch = player.getXRot();
// Ensure destination chunk is loaded before teleportation
ChunkPos destChunkPos = new ChunkPos(BlockPos.containing(dest));
targetWorld.setChunkForced(destChunkPos.x, destChunkPos.z, true);
try {
// Use changeDimension instead of teleportTo to avoid "Player moved wrongly" errors
player.changeDimension(targetWorld, new net.minecraftforge.common.util.ITeleporter() {
@Override
public Entity placeEntity(Entity entity, ServerLevel currentWorld, ServerLevel destWorld, float placementYaw, java.util.function.Function<Boolean, Entity> repositionEntity) {
Entity e = repositionEntity.apply(false);
e.teleportTo(dest.x, dest.y, dest.z);
e.setYRot(entryYaw);
e.setXRot(pitch);
return e;
}
});
// Schedule chunk unforcing on next tick to allow proper entity placement
Objects.requireNonNull(world.getServer()).execute(() -> {
targetWorld.setChunkForced(destChunkPos.x, destChunkPos.z, false);
});
} finally {
// Ensure chunk is unforced even if teleportation fails
targetWorld.setChunkForced(destChunkPos.x, destChunkPos.z, false);
}
player.connection.send(new ClientboundStopSoundPacket(null, null));
world.playSound(
null,
pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5,
SoundEvents.BUNDLE_DROP_CONTENTS,
SoundSource.PLAYERS, 2.0f, 1.0f);
} else {
TravelersSuitcase.LOGGER.warn("Target dimension {} not found for keystone {}", dimensionRL, keystoneName);
SoundSource.PLAYERS,
2.0f, 1.0f
);
}
}
}
@Override
public @NotNull VoxelShape getShape(BlockState state, @NotNull BlockGetter world, @NotNull BlockPos pos, @NotNull CollisionContext context) {
return switch (state.getValue(FACING)) {
@ -295,7 +312,7 @@ public class SuitcaseBlock extends BaseEntityBlock {
CompoundTag display = stack.getOrCreateTagElement("display");
ListTag lore = new ListTag();
if (!suitcase.getEnteredPlayers().isEmpty()) {
Component warningText = Component.literal("§c⚠ Contains " + suitcase.getEnteredPlayers().size() + " Traveler's!")
Component warningText = Component.literal("§c⚠ Contains " + suitcase.getEnteredPlayers().size() + " Travelers!")
.withStyle(ChatFormatting.RED);
lore.add(StringTag.valueOf(Component.Serializer.toJson(warningText)));
}

View File

@ -1,6 +1,6 @@
package io.lampnet.travelerssuitcase.block.entity;
import io.lampnet.travelerssuitcase.block.PocketPortalBlock;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
@ -16,6 +16,9 @@ import org.jetbrains.annotations.Nullable;
import java.util.*;
import io.lampnet.travelerssuitcase.SuitcaseRegistryState;
import net.minecraft.server.MinecraftServer;
public class SuitcaseBlockEntity extends BlockEntity {
private String boundKeystoneName;
private boolean isLocked = false;
@ -70,6 +73,11 @@ public class SuitcaseBlockEntity extends BlockEntity {
return dimensionLocked;
}
private static final java.util.Set<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) {
enteredPlayers.removeIf(data -> data.uuid.equals(player.getUUID().toString()));
EnteredPlayerData data = new EnteredPlayerData(
@ -79,14 +87,22 @@ public class SuitcaseBlockEntity extends BlockEntity {
this.worldPosition
);
enteredPlayers.add(data);
PLAYERS_WHO_ENTERED.add(player.getUUID());
if (boundKeystoneName != null) {
Map<String, BlockPos> suitcases = SUITCASE_REGISTRY.computeIfAbsent(
boundKeystoneName, k -> new HashMap<>()
);
suitcases.put(player.getUUID().toString(), this.worldPosition);
}
PocketPortalBlock.storePlayerPosition(player);
setChangedAndNotify();
MinecraftServer server = player.getServer();
if (server != null) {
SuitcaseRegistryState.onRegistryChanged(server);
}
}
public EnteredPlayerData getExitPosition(String playerUuid) {
@ -95,9 +111,9 @@ public class SuitcaseBlockEntity extends BlockEntity {
if (data.uuid.equals(playerUuid)) {
EnteredPlayerData exitData = new EnteredPlayerData(
data.uuid,
this.worldPosition.getX() + 0.5, this.worldPosition.getY() + 1.0, this.worldPosition.getZ() + 0.5,
data.x, data.y, data.z,
data.pitch, data.yaw,
this.worldPosition
data.suitcasePos != null ? data.suitcasePos : this.worldPosition
);
enteredPlayers.remove(i);
setChangedAndNotify();
@ -171,7 +187,11 @@ public class SuitcaseBlockEntity extends BlockEntity {
@Override
public @NotNull CompoundTag getUpdateTag() {
CompoundTag nbt = new CompoundTag();
this.saveAdditional(nbt);
if (boundKeystoneName != null) {
nbt.putString("BoundKeystone", boundKeystoneName);
}
nbt.putBoolean("Locked", isLocked);
nbt.putBoolean("DimensionLocked", dimensionLocked);
return nbt;
}
@ -182,13 +202,16 @@ public class SuitcaseBlockEntity extends BlockEntity {
return (suitcases != null) ? suitcases.get(playerUuid) : null;
}
public static void removeSuitcaseEntry(String keystoneName, String playerUuid) {
public static void removeSuitcaseEntry(String keystoneName, String playerUuid, MinecraftServer server) {
Map<String, BlockPos> suitcases = SUITCASE_REGISTRY.get(keystoneName);
if (suitcases != null) {
suitcases.remove(playerUuid);
if (suitcases.isEmpty()) {
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;
private float entryYaw, entryPitch;
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,77 @@
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;
private float entryYaw, entryPitch;
private boolean isManuallySet = false;
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");
data.isManuallySet = nbt.getBoolean("manually_set");
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);
nbt.putBoolean("manually_set", isManuallySet);
return nbt;
}
public void setEntry(Vec3 pos, float yaw, float pitch) {
this.entryPos = pos;
this.entryYaw = yaw;
this.entryPitch = pitch;
this.isManuallySet = true;
this.setDirty();
}
public void setEntryAutomatic(Vec3 pos, float yaw, float pitch) {
this.entryPos = pos;
this.entryYaw = yaw;
this.entryPitch = pitch;
this.setDirty();
}
public void resetToDefault() {
this.entryPos = new Vec3(17.5, 97.0, 9.5);
this.entryYaw = 0f;
this.entryPitch = 0f;
this.isManuallySet = false;
this.setDirty();
}
public Vec3 getEntryPos() { return entryPos; }
public float getEntryYaw() { return entryYaw; }
public float getEntryPitch() { return entryPitch; }
public boolean isManuallySet() { return isManuallySet; }
}

View File

@ -1,26 +1,42 @@
package io.lampnet.travelerssuitcase.item;
import io.lampnet.travelerssuitcase.TravelersSuitcase;
import io.lampnet.travelerssuitcase.world.Fantasy;
import io.lampnet.travelerssuitcase.world.FantasyWorldAccess;
import io.lampnet.travelerssuitcase.world.PortalChunkGenerator;
import io.lampnet.travelerssuitcase.world.RuntimeWorldConfig;
import io.lampnet.travelerssuitcase.world.RuntimeWorldHandle;
import net.minecraft.ChatFormatting;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.item.enchantment.Enchantments;
import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResultHolder;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.server.MinecraftServer;
import net.minecraft.sounds.SoundSource;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.network.chat.Component;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResultHolder;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.item.enchantment.Enchantments;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Mirror;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.ChunkStatus;
import net.minecraft.server.level.TicketType;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import net.minecraft.world.level.storage.LevelResource;
import net.minecraft.resources.ResourceLocation;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import net.minecraft.world.item.ItemStack.TooltipPart;
import java.io.IOException;
import java.nio.file.Files;
@ -39,35 +55,111 @@ public class KeystoneItem extends Item {
@Override
public @NotNull InteractionResultHolder<ItemStack> use(@NotNull Level world, Player player, @NotNull InteractionHand hand) {
ItemStack stack = player.getItemInHand(hand);
String keystoneName = stack.hasCustomHoverName() ? stack.getHoverName().getString().toLowerCase() : "";
String defaultName = "item.travelerssuitcase.keystone";
if (!stack.hasCustomHoverName() || keystoneName.isEmpty() || keystoneName.equals(Component.translatable(defaultName).getString().toLowerCase())) {
return InteractionResultHolder.pass(stack);
}
if (world.isClientSide) {
return InteractionResultHolder.success(stack);
}
if (stack.isEnchanted()) {
return InteractionResultHolder.pass(stack);
}
String dimensionName = "pocket_dimension_" + keystoneName.replaceAll("[^a-z0-9_]", "");
createDimension(world.getServer(), dimensionName);
stack.enchant(Enchantments.BINDING_CURSE, 1);
stack.hideTooltipPart(TooltipPart.ENCHANTMENTS);
stack.hideTooltipPart(TooltipPart.MODIFIERS);
CompoundTag nbt = stack.getOrCreateTag();
nbt.putInt("RepairCost", 32767);
world.playSound(null, player.getX(), player.getY(), player.getZ(),
SoundEvents.AMETHYST_CLUSTER_FALL, SoundSource.PLAYERS, 2.0F, 2.0F);
if (!isValidKeystone(stack)) {
return InteractionResultHolder.pass(stack);
}
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);
var biomeRegistry = server.registryAccess().registryOrThrow(Registries.BIOME);
RuntimeWorldConfig config = new RuntimeWorldConfig()
.setGenerator(new PortalChunkGenerator(biomeRegistry))
.setSeed(server.overworld().getSeed());
RuntimeWorldHandle handle = Fantasy.get(server)
.getOrOpenPersistentWorld(worldId, config);
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();
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);
ChunkPos chunkPos = new ChunkPos(pos);
world.getChunkSource().addRegionTicket(TicketType.UNKNOWN, chunkPos, 2, chunkPos);
world.getChunk(pos.getX() >> 4, pos.getZ() >> 4, ChunkStatus.FULL, true);
template.placeInWorld(
world,
pos,
pos,
new StructurePlaceSettings()
.setMirror(Mirror.NONE)
.setRotation(Rotation.NONE)
.setIgnoreEntities(false),
world.getRandom(),
Block.UPDATE_ALL
);
world.setChunkForced(pos.getX() >> 4, pos.getZ() >> 4, true);
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");
Set<String> dims = new HashSet<>();
if (Files.exists(registryFile)) {
dims.addAll(Files.readAllLines(registryFile));
}
if (dims.add(dimensionName)) {
Files.write(registryFile, dims);
}
} catch (IOException e) {
TravelersSuitcase.LOGGER.error("Failed to write dimension registry", e);
}
}
public static boolean isValidKeystone(ItemStack stack) {
String keystoneName = stack.hasCustomHoverName() ? stack.getHoverName().getString().toLowerCase() : "";
String defaultNameKey = "item.travelerssuitcase.keystone";
return stack.hasCustomHoverName() && !keystoneName.isEmpty() &&
!keystoneName.equals(Component.translatable(defaultNameKey).getString().toLowerCase()) &&
stack.isEnchanted();
!keystoneName.equals(Component.translatable(defaultNameKey).getString().toLowerCase());
}
@Override
@ -84,104 +176,37 @@ public class KeystoneItem extends Item {
public void inventoryTick(ItemStack stack, @NotNull Level world, @NotNull Entity entity, int slot, boolean selected) {
String keystoneName = stack.hasCustomHoverName() ? stack.getHoverName().getString().toLowerCase() : "";
String defaultNameKey = "item.travelerssuitcase.keystone";
if (!stack.hasCustomHoverName() || keystoneName.isEmpty() ||
keystoneName.equals(Component.translatable(defaultNameKey).getString().toLowerCase())) {
boolean hasValidName = isValidKeystone(stack);
if (!hasValidName) {
CompoundTag nbt = stack.getOrCreateTag();
nbt.putInt("CustomModelData", 1);
} else if (stack.getTag() != null && stack.getTag().contains("CustomModelData")) {
stack.getTag().remove("CustomModelData");
}
}
private boolean createDimension(MinecraftServer server, String dimensionName) {
if (server == null) {
TravelersSuitcase.LOGGER.error("Failed to create dimension: " + dimensionName + " (MinecraftServer instance is null)");
return false;
}
try {
Path datapackPath = server.getWorldPath(LevelResource.ROOT)
.resolve("datapacks")
.resolve("travelerssuitcase");
Path dimensionPath = datapackPath
.resolve("data")
.resolve("travelerssuitcase")
.resolve("dimension");
Files.createDirectories(dimensionPath);
createPackMcmeta(datapackPath);
Path dimensionFile = dimensionPath.resolve(dimensionName + ".json");
boolean dimensionExists = Files.exists(dimensionFile);
if (!world.isClientSide() && hasValidName && !stack.isEnchanted()) {
String dimensionName = "pocket_dimension_" + keystoneName.replaceAll("[^a-z0-9_]", "");
createOrLoadPersistentDimension(world.getServer(), dimensionName);
ResourceLocation dimensionKeyLocation = new ResourceLocation("travelerssuitcase", dimensionName);
boolean isDimensionRegistered = server.levelKeys().stream()
.anyMatch(key -> key.location().equals(dimensionKeyLocation));
stack.enchant(Enchantments.BINDING_CURSE, 1);
CompoundTag nbt = stack.getOrCreateTag();
nbt.putInt("RepairCost", 32767);
Path dimensionRegistryPath = server.getWorldPath(LevelResource.ROOT)
.resolve("data")
.resolve("travelerssuitcase")
.resolve("dimension_registry");
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 (entity instanceof Player player) {
world.playSound(null, player.getX(), player.getY(), player.getZ(),
SoundEvents.AMETHYST_CLUSTER_FALL, SoundSource.PLAYERS, 2.0F, 2.0F);
}
}
if (!isDimensionInRegistryFile && !isDimensionRegistered) {
Path structureMarkerPath = server.getWorldPath(LevelResource.ROOT)
.resolve("data")
.resolve("travelerssuitcase")
.resolve("pending_structures");
Files.createDirectories(structureMarkerPath);
Files.writeString(structureMarkerPath.resolve(dimensionName + ".txt"), "pending");
return true;
}
return false;
} catch (IOException e) {
TravelersSuitcase.LOGGER.error("Failed to create dimension: {}", dimensionName, e);
return false;
}
}
private void createPackMcmeta(@NotNull Path datapackPath) throws IOException {
Path packMcmeta = datapackPath.resolve("pack.mcmeta");
if (!Files.exists(packMcmeta)) {
String content = """
{
"pack": {
"pack_format": 15,
"description": "travelerssuitcase Dimensions"
}
}
""";
Files.writeString(packMcmeta, content);
if (!world.isClientSide() && stack.hasTag()) {
assert stack.getTag() != null;
if (stack.getTag().contains("Enchantments")) {
CompoundTag nbt = stack.getOrCreateTag();
if (nbt.getInt("RepairCost") < 32767) {
nbt.putInt("RepairCost", 32767);
}
}
}
}
}

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,42 @@
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) {
// For now, return false as removal is complex
return false;
}
@Override
public boolean fantasy$remove(ResourceLocation key) {
// For now, return false as removal is complex
return false;
}
@Override
public void fantasy$setFrozen(boolean value) {
if (!value && this.frozen) {
this.fantasy$originalFrozenState = true;
}
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,31 @@
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
public ServerLevel level;
@Inject(method = "runDistanceManagerUpdates", at = @At("HEAD"), cancellable = true)
private void onRunDistanceManagerUpdates(CallbackInfoReturnable<Boolean> cir) {
if (this.level instanceof RuntimeWorld) {
FantasyWorldAccess worldAccess = (FantasyWorldAccess) this.level;
if (this.level.getChunkSource().getLoadedChunksCount() > 0 || worldAccess.fantasy$shouldTick()) {
return;
}
cir.setReturnValue(false);
}
}
}

View File

@ -0,0 +1,54 @@
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;
@Inject(method = "tick", at = @At("HEAD"), cancellable = true)
private void onTick(BooleanSupplier shouldKeepTicking, CallbackInfo ci) {
if ((Object) this instanceof RuntimeWorld) {
if (!this.fantasy$shouldTick()) {
ci.cancel();
}
}
}
@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() {
if ((Object) this instanceof RuntimeWorld) {
return this.fantasy$tickWhenEmpty || !this.travelerssuitcase$isWorldEmpty();
}
return true;
}
@Unique
private boolean travelerssuitcase$isWorldEmpty() {
return this.players().isEmpty() && this.getChunkSource().getLoadedChunksCount() <= 0;
}
}

View File

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

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,104 @@
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.Arrays;
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()];
Arrays.fill(column, 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, server),
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.Config;
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 = Config.shouldTickTime();
private long timeOfDay = Config.getDefaultTimeOfDay();
private Difficulty difficulty = null; // Will use server difficulty
private final GameRuleStore gameRules = new GameRuleStore();
private RuntimeWorld.Constructor worldConstructor = RuntimeWorld::new;
private int sunnyTime = Config.getDefaultSunnyTime();
private boolean raining = Config.isDefaultRaining();
private int rainTime = Config.getDefaultRainTime();
private boolean thundering = Config.isDefaultThundering();
private int thunderTime = Config.getDefaultThunderTime();
private TriState flat = TriState.DEFAULT;
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(MinecraftServer server) {
return this.difficulty != null ? this.difficulty : server.getWorldData().getDifficulty();
}
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,224 @@
package io.lampnet.travelerssuitcase.world;
import net.minecraft.CrashReportCategory;
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;
private final MinecraftServer server;
public RuntimeWorldProperties(ServerLevelData parent, RuntimeWorldConfig config, MinecraftServer server) {
this.parent = parent;
this.config = config;
this.server = server;
}
@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(this.server);
}
@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",
"refmap": "travelerssuitcase.refmap.json",
"mixins": [
"MinecraftServerAccess",
"LevelStemMixin",
"MappedRegistryMixin",
"ServerWorldMixin",
"ServerChunkManagerMixin"
],
"client": [
],