Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 120ab466aa | |||
| 57f0f340a9 | |||
| 881ba1a9a2 | |||
| 54c9024615 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,3 +4,4 @@ run/
|
||||
run-data/
|
||||
.idea/
|
||||
.gradle/
|
||||
.c*
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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() {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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)));
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
197
src/main/java/io/lampnet/travelerssuitcase/world/Fantasy.java
Normal file
197
src/main/java/io/lampnet/travelerssuitcase/world/Fantasy.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
]
|
||||
]
|
||||
}
|
||||
@ -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": []
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@ -5,6 +5,11 @@
|
||||
"compatibilityLevel": "JAVA_8",
|
||||
"refmap": "travelerssuitcase.refmap.json",
|
||||
"mixins": [
|
||||
"MinecraftServerAccess",
|
||||
"LevelStemMixin",
|
||||
"MappedRegistryMixin",
|
||||
"ServerWorldMixin",
|
||||
"ServerChunkManagerMixin"
|
||||
],
|
||||
"client": [
|
||||
],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user