Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 120ab466aa | |||
| 57f0f340a9 | |||
| 881ba1a9a2 | |||
| 54c9024615 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,3 +4,4 @@ run/
|
|||||||
run-data/
|
run-data/
|
||||||
.idea/
|
.idea/
|
||||||
.gradle/
|
.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.
|
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
|
||||||
mod_license=MIT
|
mod_license=MIT
|
||||||
# The mod version. See https://semver.org/
|
# The mod version. See https://semver.org/
|
||||||
mod_version=1.0-SNAPSHOT
|
mod_version=1.2.7-SNAPSHOTv2
|
||||||
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
|
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
|
||||||
# This should match the base package used for the mod sources.
|
# This should match the base package used for the mod sources.
|
||||||
# See https://maven.apache.org/guides/mini/guide-naming-conventions.html
|
# See https://maven.apache.org/guides/mini/guide-naming-conventions.html
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
package io.lampnet.travelerssuitcase;
|
package io.lampnet.travelerssuitcase;
|
||||||
|
|
||||||
import net.minecraft.resources.ResourceLocation;
|
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.common.ForgeConfigSpec;
|
||||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||||
import net.minecraftforge.fml.common.Mod;
|
import net.minecraftforge.fml.common.Mod;
|
||||||
@ -10,20 +11,210 @@ import net.minecraftforge.registries.ForgeRegistries;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Mod.EventBusSubscriber(modid = TravelersSuitcase.MODID, bus = Mod.EventBusSubscriber.Bus.MOD)
|
@Mod.EventBusSubscriber(modid = TravelersSuitcase.MODID, bus = Mod.EventBusSubscriber.Bus.MOD)
|
||||||
public class Config {
|
public class Config {
|
||||||
private static final ForgeConfigSpec.Builder BUILDER = new ForgeConfigSpec.Builder();
|
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) {
|
public static final ForgeConfigSpec.LongValue DEFAULT_TIME_OF_DAY;
|
||||||
return obj instanceof final String itemName && ForgeRegistries.ITEMS.containsKey(new ResourceLocation(itemName));
|
|
||||||
|
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
|
@SubscribeEvent
|
||||||
static void onLoad(final ModConfigEvent event) {
|
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;
|
package io.lampnet.travelerssuitcase;
|
||||||
|
|
||||||
import io.lampnet.travelerssuitcase.block.entity.SuitcaseBlockEntity;
|
import io.lampnet.travelerssuitcase.block.entity.SuitcaseBlockEntity;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
import net.minecraft.nbt.CompoundTag;
|
import net.minecraft.nbt.CompoundTag;
|
||||||
import net.minecraft.nbt.ListTag;
|
import net.minecraft.nbt.ListTag;
|
||||||
import net.minecraft.nbt.Tag;
|
import net.minecraft.nbt.Tag;
|
||||||
import net.minecraft.server.MinecraftServer;
|
import net.minecraft.server.MinecraftServer;
|
||||||
import net.minecraft.world.level.saveddata.SavedData;
|
import net.minecraft.server.level.ServerLevel;
|
||||||
import net.minecraft.core.BlockPos;
|
|
||||||
import net.minecraft.world.level.Level;
|
import net.minecraft.world.level.Level;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import net.minecraft.world.level.saveddata.SavedData;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
public class SuitcaseRegistryState extends SavedData {
|
public class SuitcaseRegistryState extends SavedData {
|
||||||
private static final String REGISTRY_KEY = "travelers_suitcase_registry";
|
public static final String DATA_NAME = "travelers_suitcase_registry";
|
||||||
private final Map<String, Map<String, BlockPos>> registry = new HashMap<>();
|
private final Map<String, Map<String, BlockPos>> registry = new HashMap<>();
|
||||||
|
|
||||||
public SuitcaseRegistryState() {
|
public SuitcaseRegistryState() {
|
||||||
@ -23,62 +23,86 @@ public class SuitcaseRegistryState extends SavedData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NotNull CompoundTag save(@NotNull CompoundTag nbt) {
|
public CompoundTag save(CompoundTag nbt) {
|
||||||
ListTag keystonesList = new ListTag();
|
CompoundTag top = new CompoundTag();
|
||||||
for (Map.Entry<String, Map<String, BlockPos>> keystoneEntry : registry.entrySet()) {
|
for (Map.Entry<String, Map<String, BlockPos>> entry : registry.entrySet()) {
|
||||||
CompoundTag keystoneNbt = new CompoundTag();
|
String keystone = entry.getKey();
|
||||||
keystoneNbt.putString("KeystoneName", keystoneEntry.getKey());
|
Map<String, BlockPos> playerMap = entry.getValue();
|
||||||
ListTag playersList = getPlayersList(keystoneEntry);
|
|
||||||
keystoneNbt.put("Players", playersList);
|
ListTag playerList = new ListTag();
|
||||||
keystonesList.add(keystoneNbt);
|
for (Map.Entry<String, BlockPos> e2 : playerMap.entrySet()) {
|
||||||
|
CompoundTag record = new CompoundTag();
|
||||||
|
record.putString("UUID", e2.getKey());
|
||||||
|
BlockPos pos = e2.getValue();
|
||||||
|
record.putInt("X", pos.getX());
|
||||||
|
record.putInt("Y", pos.getY());
|
||||||
|
record.putInt("Z", pos.getZ());
|
||||||
|
playerList.add(record);
|
||||||
}
|
}
|
||||||
nbt.put("SuitcaseRegistry", keystonesList);
|
top.put(keystone, playerList);
|
||||||
|
}
|
||||||
|
nbt.put("RegistryEntries", top);
|
||||||
return nbt;
|
return nbt;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static @NotNull ListTag getPlayersList(Map.Entry<String, Map<String, BlockPos>> keystoneEntry) {
|
|
||||||
ListTag playersList = new ListTag();
|
|
||||||
for (Map.Entry<String, BlockPos> playerEntry : keystoneEntry.getValue().entrySet()) {
|
|
||||||
CompoundTag playerNbt = new CompoundTag();
|
|
||||||
playerNbt.putString("UUID", playerEntry.getKey());
|
|
||||||
BlockPos pos = playerEntry.getValue();
|
|
||||||
playerNbt.putInt("X", pos.getX());
|
|
||||||
playerNbt.putInt("Y", pos.getY());
|
|
||||||
playerNbt.putInt("Z", pos.getZ());
|
|
||||||
|
|
||||||
playersList.add(playerNbt);
|
|
||||||
}
|
|
||||||
return playersList;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static SuitcaseRegistryState load(CompoundTag nbt) {
|
public static SuitcaseRegistryState load(CompoundTag nbt) {
|
||||||
SuitcaseRegistryState state = new SuitcaseRegistryState();
|
SuitcaseRegistryState data = new SuitcaseRegistryState();
|
||||||
if (nbt.contains("SuitcaseRegistry")) {
|
if (nbt.contains("RegistryEntries", Tag.TAG_COMPOUND)) {
|
||||||
ListTag keystonesList = nbt.getList("SuitcaseRegistry", Tag.TAG_COMPOUND);
|
CompoundTag top = nbt.getCompound("RegistryEntries");
|
||||||
for (int i = 0; i < keystonesList.size(); i++) {
|
for (String keystone : top.getAllKeys()) {
|
||||||
CompoundTag keystoneNbt = keystonesList.getCompound(i);
|
ListTag playerList = top.getList(keystone, Tag.TAG_COMPOUND);
|
||||||
String keystoneName = keystoneNbt.getString("KeystoneName");
|
Map<String, BlockPos> playerMap = new HashMap<>();
|
||||||
Map<String, BlockPos> playersMap = new HashMap<>();
|
for (int i = 0; i < playerList.size(); i++) {
|
||||||
ListTag playersList = keystoneNbt.getList("Players", Tag.TAG_COMPOUND);
|
CompoundTag rec = playerList.getCompound(i);
|
||||||
for (int j = 0; j < playersList.size(); j++) {
|
String uuid = rec.getString("UUID");
|
||||||
CompoundTag playerNbt = playersList.getCompound(j);
|
int x = rec.getInt("X");
|
||||||
String uuid = playerNbt.getString("UUID");
|
int y = rec.getInt("Y");
|
||||||
BlockPos pos = new BlockPos(
|
int z = rec.getInt("Z");
|
||||||
playerNbt.getInt("X"),
|
playerMap.put(uuid, new BlockPos(x, y, z));
|
||||||
playerNbt.getInt("Y"),
|
}
|
||||||
playerNbt.getInt("Z")
|
data.registry.put(keystone, playerMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void syncFromStaticRegistry() {
|
||||||
|
registry.clear();
|
||||||
|
SuitcaseBlockEntity.saveSuitcaseRegistryTo(registry);
|
||||||
|
setDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void syncToStaticRegistry() {
|
||||||
|
SuitcaseBlockEntity.initializeSuitcaseRegistry(registry);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void onServerStart(MinecraftServer server) {
|
||||||
|
Level overworld = server.getLevel(Level.OVERWORLD);
|
||||||
|
if (overworld == null) return;
|
||||||
|
|
||||||
|
SuitcaseRegistryState data = Objects.requireNonNull(((ServerLevel) overworld).getDataStorage()).computeIfAbsent(
|
||||||
|
SuitcaseRegistryState::load,
|
||||||
|
SuitcaseRegistryState::new,
|
||||||
|
DATA_NAME
|
||||||
);
|
);
|
||||||
playersMap.put(uuid, pos);
|
data.syncToStaticRegistry();
|
||||||
}
|
}
|
||||||
state.registry.put(keystoneName, playersMap);
|
|
||||||
}
|
public static void onRegistryChanged(MinecraftServer server) {
|
||||||
}
|
Level overworld = server.getLevel(Level.OVERWORLD);
|
||||||
return state;
|
if (overworld == null) return;
|
||||||
|
|
||||||
|
SuitcaseRegistryState data = Objects.requireNonNull(((ServerLevel) overworld).getDataStorage()).computeIfAbsent(
|
||||||
|
SuitcaseRegistryState::load,
|
||||||
|
SuitcaseRegistryState::new,
|
||||||
|
DATA_NAME
|
||||||
|
);
|
||||||
|
data.syncFromStaticRegistry();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SuitcaseRegistryState getState(MinecraftServer server) {
|
public static SuitcaseRegistryState getState(MinecraftServer server) {
|
||||||
return Objects.requireNonNull(server.getLevel(Level.OVERWORLD)).getDataStorage()
|
return Objects.requireNonNull(((ServerLevel) server.getLevel(Level.OVERWORLD)).getDataStorage())
|
||||||
.computeIfAbsent(SuitcaseRegistryState::load, SuitcaseRegistryState::new, REGISTRY_KEY);
|
.computeIfAbsent(SuitcaseRegistryState::load, SuitcaseRegistryState::new, DATA_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<String, Map<String, BlockPos>> getRegistry() {
|
public Map<String, Map<String, BlockPos>> getRegistry() {
|
||||||
|
|||||||
@ -1,22 +1,85 @@
|
|||||||
package io.lampnet.travelerssuitcase;
|
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.ModBlocks;
|
||||||
|
import io.lampnet.travelerssuitcase.block.SuitcaseBlock;
|
||||||
import io.lampnet.travelerssuitcase.block.entity.ModBlockEntities;
|
import io.lampnet.travelerssuitcase.block.entity.ModBlockEntities;
|
||||||
|
import io.lampnet.travelerssuitcase.block.entity.SuitcaseBlockEntity;
|
||||||
|
import io.lampnet.travelerssuitcase.criterion.EnterPocketDimensionCriterion;
|
||||||
|
import io.lampnet.travelerssuitcase.data.MobEntryData;
|
||||||
|
import io.lampnet.travelerssuitcase.data.PlayerEntryData;
|
||||||
|
import io.lampnet.travelerssuitcase.item.KeystoneItem;
|
||||||
import io.lampnet.travelerssuitcase.item.ModItemGroups;
|
import io.lampnet.travelerssuitcase.item.ModItemGroups;
|
||||||
import io.lampnet.travelerssuitcase.item.ModItems;
|
import io.lampnet.travelerssuitcase.item.ModItems;
|
||||||
import io.lampnet.travelerssuitcase.block.entity.SuitcaseBlockEntity;
|
import io.lampnet.travelerssuitcase.world.Fantasy;
|
||||||
import io.lampnet.travelerssuitcase.SuitcaseRegistryState;
|
import io.lampnet.travelerssuitcase.world.FantasyInitializer;
|
||||||
|
import io.lampnet.travelerssuitcase.world.PortalChunkGenerator;
|
||||||
|
import io.lampnet.travelerssuitcase.world.RuntimeWorldConfig;
|
||||||
|
import net.minecraft.advancements.CriteriaTriggers;
|
||||||
|
import net.minecraft.commands.CommandSourceStack;
|
||||||
|
import net.minecraft.commands.Commands;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.registries.BuiltInRegistries;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.nbt.CompoundTag;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.sounds.SoundEvents;
|
||||||
|
import net.minecraft.sounds.SoundSource;
|
||||||
|
import net.minecraft.world.InteractionHand;
|
||||||
|
import net.minecraft.world.InteractionResult;
|
||||||
|
import net.minecraft.world.entity.EntityType;
|
||||||
|
import net.minecraft.world.entity.LivingEntity;
|
||||||
|
import net.minecraft.world.entity.monster.Enemy;
|
||||||
|
import net.minecraft.world.entity.animal.Wolf;
|
||||||
|
import net.minecraft.world.item.BlockItem;
|
||||||
|
import net.minecraft.world.item.Items;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
import net.minecraftforge.common.MinecraftForge;
|
import net.minecraftforge.common.MinecraftForge;
|
||||||
|
import net.minecraftforge.event.RegisterCommandsEvent;
|
||||||
|
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
|
||||||
|
import net.minecraftforge.event.server.ServerStartingEvent;
|
||||||
import net.minecraftforge.eventbus.api.IEventBus;
|
import net.minecraftforge.eventbus.api.IEventBus;
|
||||||
|
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||||
import net.minecraftforge.fml.common.Mod;
|
import net.minecraftforge.fml.common.Mod;
|
||||||
|
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
|
||||||
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
|
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
|
||||||
|
import net.minecraftforge.fml.ModLoadingContext;
|
||||||
|
import net.minecraftforge.fml.config.ModConfig;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.world.entity.Entity;
|
||||||
|
|
||||||
|
|
||||||
|
import io.lampnet.travelerssuitcase.world.RuntimeWorldHandle;
|
||||||
|
|
||||||
@Mod(TravelersSuitcase.MODID)
|
@Mod(TravelersSuitcase.MODID)
|
||||||
public class TravelersSuitcase {
|
public class TravelersSuitcase {
|
||||||
public static final String MODID = "travelerssuitcase";
|
public static final String MODID = "travelerssuitcase";
|
||||||
public static final Logger LOGGER = LogManager.getLogger(MODID);
|
public static final Logger LOGGER = LogManager.getLogger(MODID);
|
||||||
|
public static final EnterPocketDimensionCriterion ENTER_POCKET_DIMENSION = CriteriaTriggers.register(new EnterPocketDimensionCriterion());
|
||||||
|
|
||||||
|
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() {
|
public TravelersSuitcase() {
|
||||||
IEventBus modEventBus = FMLJavaModLoadingContext.get().getModEventBus();
|
IEventBus modEventBus = FMLJavaModLoadingContext.get().getModEventBus();
|
||||||
@ -25,56 +88,450 @@ public class TravelersSuitcase {
|
|||||||
ModBlocks.register(modEventBus);
|
ModBlocks.register(modEventBus);
|
||||||
ModItemGroups.register(modEventBus);
|
ModItemGroups.register(modEventBus);
|
||||||
ModBlockEntities.register(modEventBus);
|
ModBlockEntities.register(modEventBus);
|
||||||
|
FantasyInitializer.register(modEventBus);
|
||||||
|
|
||||||
MinecraftForge.EVENT_BUS.addListener(this::onWorldLoad);
|
// Register configuration
|
||||||
MinecraftForge.EVENT_BUS.addListener(this::onServerStarting);
|
ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, Config.SPEC);
|
||||||
MinecraftForge.EVENT_BUS.addListener(this::onServerStopping);
|
|
||||||
|
|
||||||
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) {
|
private void commonSetup(final FMLCommonSetupEvent event) {
|
||||||
if (event.getLevel() instanceof net.minecraft.server.level.ServerLevel world) {
|
new FantasyInitializer().onInitialize();
|
||||||
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)
|
@SubscribeEvent
|
||||||
|
public static void onServerStarting(ServerStartingEvent event) {
|
||||||
|
SuitcaseRegistryState.onServerStart(event.getServer());
|
||||||
|
|
||||||
|
Path registryFile = event.getServer().getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT)
|
||||||
.resolve("data")
|
.resolve("data")
|
||||||
.resolve(MODID)
|
.resolve(MODID)
|
||||||
.resolve("pending_structures")
|
.resolve("dimension_registry")
|
||||||
.resolve(dimensionName + ".txt");
|
.resolve("registry.txt");
|
||||||
if (java.nio.file.Files.exists(structureMarkerPath)) {
|
|
||||||
|
if (!Files.exists(registryFile)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var biomeRegistry = event.getServer().registryAccess().registryOrThrow(Registries.BIOME);
|
||||||
|
|
||||||
|
long seed = event.getServer().overworld().getSeed();
|
||||||
try {
|
try {
|
||||||
net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate template = world.getServer().getStructureManager()
|
for (String dimName : Files.readAllLines(registryFile)) {
|
||||||
.get(new net.minecraft.resources.ResourceLocation(MODID, "pocket_island_01"))
|
ResourceLocation worldId = ResourceLocation.fromNamespaceAndPath(MODID, dimName);
|
||||||
.orElse(null);
|
|
||||||
if (template != null) {
|
var voidGen = new PortalChunkGenerator(biomeRegistry);
|
||||||
net.minecraft.core.BlockPos pos = new net.minecraft.core.BlockPos(0, 64, 0);
|
|
||||||
template.placeInWorld(
|
var cfg = new RuntimeWorldConfig()
|
||||||
world,
|
.setGenerator(voidGen)
|
||||||
pos,
|
.setSeed(seed);
|
||||||
pos,
|
RuntimeWorldHandle handle = Fantasy.get(event.getServer()).getOrOpenPersistentWorld(worldId, cfg);
|
||||||
new net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings(),
|
|
||||||
world.getRandom(),
|
handle.setTickWhenEmpty(Config.tickWhenEmpty());
|
||||||
net.minecraft.world.level.block.Block.UPDATE_ALL
|
}
|
||||||
|
} 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) {
|
@SubscribeEvent
|
||||||
SuitcaseRegistryState state = SuitcaseRegistryState.getState(event.getServer());
|
public void onUseItem(PlayerInteractEvent.RightClickItem event) {
|
||||||
SuitcaseBlockEntity.initializeSuitcaseRegistry(state.getRegistry());
|
Level world = event.getLevel();
|
||||||
}
|
var player = event.getEntity();
|
||||||
|
var hand = event.getHand();
|
||||||
|
var stack = player.getItemInHand(hand);
|
||||||
|
|
||||||
private void onServerStopping(net.minecraftforge.event.server.ServerStoppingEvent event) {
|
if (world.isClientSide || hand != InteractionHand.MAIN_HAND) return;
|
||||||
SuitcaseRegistryState state = SuitcaseRegistryState.getState(event.getServer());
|
|
||||||
SuitcaseBlockEntity.saveSuitcaseRegistryTo(state.getRegistry());
|
if (!(world instanceof ServerLevel sw)) return;
|
||||||
state.setDirty();
|
var id = sw.dimension().location();
|
||||||
|
if (!id.getNamespace().equals(MODID) || !id.getPath().startsWith("pocket_dimension_")) return;
|
||||||
|
|
||||||
|
if (stack.getItem() == Items.BONE) {
|
||||||
|
Vec3 pos = player.position();
|
||||||
|
float yaw = player.getYRot();
|
||||||
|
float pitch = player.getXRot();
|
||||||
|
PlayerEntryData.get(sw).setEntry(pos, yaw, pitch);
|
||||||
|
player.sendSystemMessage(Component.literal(String.format("§aPlayer entry location set to %.1f, %.1f, %.1f", pos.x, pos.y, pos.z)));
|
||||||
|
event.setCancellationResult(InteractionResult.SUCCESS);
|
||||||
|
event.setCanceled(true);
|
||||||
|
} else if (stack.getItem() == Items.LEAD) {
|
||||||
|
Vec3 pos = player.position();
|
||||||
|
float yaw = player.getYRot();
|
||||||
|
float pitch = player.getXRot();
|
||||||
|
MobEntryData.get(sw).setEntry(pos, yaw, pitch);
|
||||||
|
player.sendSystemMessage(Component.literal(String.format("§aMob entry location set to %.1f, %.1f, %.1f", pos.x, pos.y, pos.z)));
|
||||||
|
event.setCancellationResult(InteractionResult.SUCCESS);
|
||||||
|
event.setCanceled(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -70,7 +70,7 @@ public class ModBlocks {
|
|||||||
.sound(SoundType.LODESTONE)
|
.sound(SoundType.LODESTONE)
|
||||||
.noOcclusion()
|
.noOcclusion()
|
||||||
.lightLevel(getPortalLuminance())
|
.lightLevel(getPortalLuminance())
|
||||||
.strength(-1f)));
|
.strength(5.0f)));
|
||||||
|
|
||||||
private static <T extends Block> RegistryObject<T> registerBlock(String name, Supplier<T> blockSupplier) {
|
private static <T extends Block> RegistryObject<T> registerBlock(String name, Supplier<T> blockSupplier) {
|
||||||
RegistryObject<T> block = BLOCKS.register(name, blockSupplier);
|
RegistryObject<T> block = BLOCKS.register(name, blockSupplier);
|
||||||
|
|||||||
@ -4,12 +4,7 @@ import io.lampnet.travelerssuitcase.block.entity.SuitcaseBlockEntity;
|
|||||||
import net.minecraft.world.level.block.Block;
|
import net.minecraft.world.level.block.Block;
|
||||||
import net.minecraft.world.level.block.state.BlockState;
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
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.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.ServerPlayer;
|
||||||
import net.minecraft.server.level.ServerLevel;
|
import net.minecraft.server.level.ServerLevel;
|
||||||
import net.minecraft.sounds.SoundEvents;
|
import net.minecraft.sounds.SoundEvents;
|
||||||
@ -17,116 +12,34 @@ import net.minecraft.sounds.SoundSource;
|
|||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
import net.minecraft.ChatFormatting;
|
import net.minecraft.ChatFormatting;
|
||||||
import net.minecraft.core.BlockPos;
|
import net.minecraft.core.BlockPos;
|
||||||
import net.minecraft.world.phys.AABB;
|
|
||||||
import net.minecraft.world.level.ChunkPos;
|
import net.minecraft.world.level.ChunkPos;
|
||||||
import net.minecraft.world.phys.Vec3;
|
import net.minecraft.world.phys.Vec3;
|
||||||
import net.minecraft.world.level.Level;
|
import net.minecraft.world.level.Level;
|
||||||
import net.minecraft.world.entity.Entity;
|
import net.minecraft.world.entity.Entity;
|
||||||
import net.minecraft.world.level.chunk.LevelChunk;
|
|
||||||
import net.minecraft.resources.ResourceLocation;
|
import net.minecraft.resources.ResourceLocation;
|
||||||
import net.minecraft.world.level.block.state.BlockBehaviour;
|
import net.minecraft.world.level.block.state.BlockBehaviour;
|
||||||
import net.minecraft.nbt.Tag;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import net.minecraft.world.level.storage.loot.LootParams;
|
||||||
|
|
||||||
public class PocketPortalBlock extends Block {
|
public class PocketPortalBlock extends Block {
|
||||||
private static final Map<String, PlayerPositionData> LAST_KNOWN_POSITIONS = new HashMap<>();
|
|
||||||
private static final 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) {
|
public PocketPortalBlock(BlockBehaviour.Properties properties) {
|
||||||
super(properties);
|
super(properties);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void storePlayerPosition(ServerPlayer player) {
|
@Override
|
||||||
LAST_KNOWN_POSITIONS.put(
|
public List<ItemStack> getDrops(BlockState state, LootParams.Builder builder) {
|
||||||
player.getUUID().toString(),
|
return Collections.singletonList(new ItemStack(this));
|
||||||
new PlayerPositionData(player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot())
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
@Override
|
||||||
public void entityInside(@NotNull BlockState state, Level world, @NotNull BlockPos pos, @NotNull Entity entity) {
|
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);
|
ServerLevel overworld = Objects.requireNonNull(world.getServer()).getLevel(Level.OVERWORLD);
|
||||||
if (overworld == null) return;
|
if (overworld == null) return;
|
||||||
|
|
||||||
boolean teleported = false;
|
// Primary method: Try to teleport to the suitcase block they entered from
|
||||||
|
if (!attemptSuitcaseTeleport(world, overworld, player, keystoneName)) {
|
||||||
// Method 1: Try to teleport to the original suitcase block entity
|
// Fallback: Take them to spawn if suitcase block cannot be found
|
||||||
teleported = attemptSuitcaseTeleport(world, overworld, player, keystoneName);
|
player.displayClientMessage(Component.literal("§c...").withStyle(ChatFormatting.RED), false);
|
||||||
|
|
||||||
// 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);
|
|
||||||
teleportToPosition(world, player, overworld,
|
teleportToPosition(world, player, overworld,
|
||||||
overworld.getSharedSpawnPos().getX() + 0.5,
|
overworld.getSharedSpawnPos().getX() + 0.5,
|
||||||
overworld.getSharedSpawnPos().getY() + 1.0,
|
overworld.getSharedSpawnPos().getY() + 1.0,
|
||||||
overworld.getSharedSpawnPos().getZ() + 0.5, 0, 0);
|
overworld.getSharedSpawnPos().getZ() + 0.5, 0, 0);
|
||||||
}
|
}
|
||||||
SuitcaseBlockEntity.removeSuitcaseEntry(keystoneName, player.getUUID().toString());
|
SuitcaseBlockEntity.removeSuitcaseEntry(keystoneName, player.getUUID().toString(), world.getServer());
|
||||||
LAST_KNOWN_POSITIONS.remove(player.getUUID().toString());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -180,10 +73,11 @@ public class PocketPortalBlock extends Block {
|
|||||||
player.fallDistance = 0f;
|
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) {
|
private boolean attemptSuitcaseTeleport(Level world, ServerLevel overworld, ServerPlayer player, String keystoneName) {
|
||||||
BlockPos suitcasePos = SuitcaseBlockEntity.findSuitcasePosition(keystoneName, player.getUUID().toString());
|
BlockPos suitcasePos = SuitcaseBlockEntity.findSuitcasePosition(keystoneName, player.getUUID().toString());
|
||||||
if (suitcasePos == null) return false;
|
if (suitcasePos == null) return false;
|
||||||
|
|
||||||
ChunkPos suitcaseChunkPos = new ChunkPos(suitcasePos);
|
ChunkPos suitcaseChunkPos = new ChunkPos(suitcasePos);
|
||||||
overworld.setChunkForced(suitcaseChunkPos.x, suitcaseChunkPos.z, true);
|
overworld.setChunkForced(suitcaseChunkPos.x, suitcaseChunkPos.z, true);
|
||||||
try {
|
try {
|
||||||
@ -191,7 +85,10 @@ public class PocketPortalBlock extends Block {
|
|||||||
if (targetEntity instanceof SuitcaseBlockEntity suitcase) {
|
if (targetEntity instanceof SuitcaseBlockEntity suitcase) {
|
||||||
SuitcaseBlockEntity.EnteredPlayerData exitData = suitcase.getExitPosition(player.getUUID().toString());
|
SuitcaseBlockEntity.EnteredPlayerData exitData = suitcase.getExitPosition(player.getUUID().toString());
|
||||||
if (exitData != null) {
|
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(() -> {
|
Objects.requireNonNull(world.getServer()).execute(() -> {
|
||||||
overworld.setChunkForced(suitcaseChunkPos.x, suitcaseChunkPos.z, false);
|
overworld.setChunkForced(suitcaseChunkPos.x, suitcaseChunkPos.z, false);
|
||||||
});
|
});
|
||||||
@ -204,105 +101,24 @@ public class PocketPortalBlock extends Block {
|
|||||||
return false;
|
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,
|
private void teleportToPosition(Level world, ServerPlayer player, ServerLevel targetWorld,
|
||||||
double x, double y, double z, float yaw, float pitch) {
|
double x, double y, double z, float yaw, float pitch) {
|
||||||
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.TravelersSuitcase;
|
||||||
import io.lampnet.travelerssuitcase.block.entity.ModBlockEntities;
|
import io.lampnet.travelerssuitcase.block.entity.ModBlockEntities;
|
||||||
import io.lampnet.travelerssuitcase.block.entity.SuitcaseBlockEntity;
|
import io.lampnet.travelerssuitcase.block.entity.SuitcaseBlockEntity;
|
||||||
|
import io.lampnet.travelerssuitcase.data.PlayerEntryData;
|
||||||
import io.lampnet.travelerssuitcase.item.KeystoneItem;
|
import io.lampnet.travelerssuitcase.item.KeystoneItem;
|
||||||
import net.minecraft.world.level.block.BaseEntityBlock;
|
import net.minecraft.world.level.block.BaseEntityBlock;
|
||||||
import net.minecraft.world.level.block.Block;
|
import net.minecraft.world.level.block.Block;
|
||||||
@ -44,10 +45,12 @@ import net.minecraft.world.phys.Vec3;
|
|||||||
import net.minecraft.world.phys.shapes.CollisionContext;
|
import net.minecraft.world.phys.shapes.CollisionContext;
|
||||||
import net.minecraft.world.phys.shapes.VoxelShape;
|
import net.minecraft.world.phys.shapes.VoxelShape;
|
||||||
import net.minecraft.world.level.BlockGetter;
|
import net.minecraft.world.level.BlockGetter;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
import net.minecraft.world.level.Level;
|
import net.minecraft.world.level.Level;
|
||||||
import net.minecraft.world.Containers;
|
|
||||||
import net.minecraft.world.item.DyeColor;
|
import net.minecraft.world.item.DyeColor;
|
||||||
|
|
||||||
|
import net.minecraft.network.protocol.game.ClientboundStopSoundPacket;
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
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_E = Block.box(2, 0, 0, 14, 4, 16);
|
||||||
private final static VoxelShape SHAPE_W = 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) {
|
public SuitcaseBlock(BlockBehaviour.Properties properties) {
|
||||||
super(properties);
|
super(properties);
|
||||||
this.registerDefaultState(this.stateDefinition.any()
|
this.registerDefaultState(this.stateDefinition.any()
|
||||||
@ -114,42 +119,6 @@ public class SuitcaseBlock extends BaseEntityBlock {
|
|||||||
@Override
|
@Override
|
||||||
public void onRemove(BlockState state, @NotNull Level world, @NotNull BlockPos pos, BlockState newState, boolean isMoving) {
|
public void onRemove(BlockState state, @NotNull Level world, @NotNull BlockPos pos, BlockState newState, boolean isMoving) {
|
||||||
if (!state.is(newState.getBlock())) {
|
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);
|
super.onRemove(state, world, pos, newState, isMoving);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -243,24 +212,72 @@ public class SuitcaseBlock extends BaseEntityBlock {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String dimensionName = "pocket_dimension_" + keystoneName;
|
String dimensionName = "pocket_dimension_" + keystoneName;
|
||||||
ResourceLocation dimensionRL = new ResourceLocation(TravelersSuitcase.MODID, dimensionName);
|
ResourceLocation dimensionRL = ResourceLocation.fromNamespaceAndPath(TravelersSuitcase.MODID, dimensionName);
|
||||||
ResourceKey<Level> dimensionKey = ResourceKey.create(Registries.DIMENSION, dimensionRL);
|
ResourceKey<Level> dimensionKey = ResourceKey.create(Registries.DIMENSION, dimensionRL);
|
||||||
ServerLevel targetWorld = Objects.requireNonNull(world.getServer()).getLevel(dimensionKey);
|
ServerLevel targetWorld = Objects.requireNonNull(world.getServer()).getLevel(dimensionKey);
|
||||||
if (targetWorld != null) {
|
if (targetWorld != null) {
|
||||||
|
boolean wasFirstTime = suitcase.isFirstTimeEntering(player);
|
||||||
suitcase.playerEntered(player);
|
suitcase.playerEntered(player);
|
||||||
|
if (wasFirstTime) {
|
||||||
|
TravelersSuitcase.ENTER_POCKET_DIMENSION.trigger(player);
|
||||||
|
}
|
||||||
|
|
||||||
player.stopRiding();
|
player.stopRiding();
|
||||||
player.setDeltaMovement(Vec3.ZERO);
|
player.setDeltaMovement(Vec3.ZERO);
|
||||||
player.fallDistance = 0f;
|
player.fallDistance = 0f;
|
||||||
player.teleportTo(targetWorld, 17.5, 97, 9.5, player.getYRot(), player.getXRot());
|
|
||||||
world.playSound(null, pos,
|
PlayerEntryData ped = PlayerEntryData.get(targetWorld);
|
||||||
|
|
||||||
|
// 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,
|
SoundEvents.BUNDLE_DROP_CONTENTS,
|
||||||
SoundSource.PLAYERS, 2.0f, 1.0f);
|
SoundSource.PLAYERS,
|
||||||
} else {
|
2.0f, 1.0f
|
||||||
TravelersSuitcase.LOGGER.warn("Target dimension {} not found for keystone {}", dimensionRL, keystoneName);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NotNull VoxelShape getShape(BlockState state, @NotNull BlockGetter world, @NotNull BlockPos pos, @NotNull CollisionContext context) {
|
public @NotNull VoxelShape getShape(BlockState state, @NotNull BlockGetter world, @NotNull BlockPos pos, @NotNull CollisionContext context) {
|
||||||
return switch (state.getValue(FACING)) {
|
return switch (state.getValue(FACING)) {
|
||||||
@ -295,7 +312,7 @@ public class SuitcaseBlock extends BaseEntityBlock {
|
|||||||
CompoundTag display = stack.getOrCreateTagElement("display");
|
CompoundTag display = stack.getOrCreateTagElement("display");
|
||||||
ListTag lore = new ListTag();
|
ListTag lore = new ListTag();
|
||||||
if (!suitcase.getEnteredPlayers().isEmpty()) {
|
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);
|
.withStyle(ChatFormatting.RED);
|
||||||
lore.add(StringTag.valueOf(Component.Serializer.toJson(warningText)));
|
lore.add(StringTag.valueOf(Component.Serializer.toJson(warningText)));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package io.lampnet.travelerssuitcase.block.entity;
|
package io.lampnet.travelerssuitcase.block.entity;
|
||||||
|
|
||||||
import io.lampnet.travelerssuitcase.block.PocketPortalBlock;
|
|
||||||
import net.minecraft.core.BlockPos;
|
import net.minecraft.core.BlockPos;
|
||||||
import net.minecraft.nbt.CompoundTag;
|
import net.minecraft.nbt.CompoundTag;
|
||||||
import net.minecraft.nbt.ListTag;
|
import net.minecraft.nbt.ListTag;
|
||||||
@ -16,6 +16,9 @@ import org.jetbrains.annotations.Nullable;
|
|||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
|
import io.lampnet.travelerssuitcase.SuitcaseRegistryState;
|
||||||
|
import net.minecraft.server.MinecraftServer;
|
||||||
|
|
||||||
public class SuitcaseBlockEntity extends BlockEntity {
|
public class SuitcaseBlockEntity extends BlockEntity {
|
||||||
private String boundKeystoneName;
|
private String boundKeystoneName;
|
||||||
private boolean isLocked = false;
|
private boolean isLocked = false;
|
||||||
@ -70,6 +73,11 @@ public class SuitcaseBlockEntity extends BlockEntity {
|
|||||||
return dimensionLocked;
|
return dimensionLocked;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final java.util.Set<java.util.UUID> PLAYERS_WHO_ENTERED = new java.util.HashSet<>();
|
||||||
|
public boolean isFirstTimeEntering(ServerPlayer player) {
|
||||||
|
return !PLAYERS_WHO_ENTERED.contains(player.getUUID());
|
||||||
|
}
|
||||||
|
|
||||||
public void playerEntered(ServerPlayer player) {
|
public void playerEntered(ServerPlayer player) {
|
||||||
enteredPlayers.removeIf(data -> data.uuid.equals(player.getUUID().toString()));
|
enteredPlayers.removeIf(data -> data.uuid.equals(player.getUUID().toString()));
|
||||||
EnteredPlayerData data = new EnteredPlayerData(
|
EnteredPlayerData data = new EnteredPlayerData(
|
||||||
@ -79,14 +87,22 @@ public class SuitcaseBlockEntity extends BlockEntity {
|
|||||||
this.worldPosition
|
this.worldPosition
|
||||||
);
|
);
|
||||||
enteredPlayers.add(data);
|
enteredPlayers.add(data);
|
||||||
|
|
||||||
|
PLAYERS_WHO_ENTERED.add(player.getUUID());
|
||||||
|
|
||||||
if (boundKeystoneName != null) {
|
if (boundKeystoneName != null) {
|
||||||
Map<String, BlockPos> suitcases = SUITCASE_REGISTRY.computeIfAbsent(
|
Map<String, BlockPos> suitcases = SUITCASE_REGISTRY.computeIfAbsent(
|
||||||
boundKeystoneName, k -> new HashMap<>()
|
boundKeystoneName, k -> new HashMap<>()
|
||||||
);
|
);
|
||||||
suitcases.put(player.getUUID().toString(), this.worldPosition);
|
suitcases.put(player.getUUID().toString(), this.worldPosition);
|
||||||
}
|
}
|
||||||
PocketPortalBlock.storePlayerPosition(player);
|
|
||||||
setChangedAndNotify();
|
setChangedAndNotify();
|
||||||
|
|
||||||
|
MinecraftServer server = player.getServer();
|
||||||
|
if (server != null) {
|
||||||
|
SuitcaseRegistryState.onRegistryChanged(server);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public EnteredPlayerData getExitPosition(String playerUuid) {
|
public EnteredPlayerData getExitPosition(String playerUuid) {
|
||||||
@ -95,9 +111,9 @@ public class SuitcaseBlockEntity extends BlockEntity {
|
|||||||
if (data.uuid.equals(playerUuid)) {
|
if (data.uuid.equals(playerUuid)) {
|
||||||
EnteredPlayerData exitData = new EnteredPlayerData(
|
EnteredPlayerData exitData = new EnteredPlayerData(
|
||||||
data.uuid,
|
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,
|
data.pitch, data.yaw,
|
||||||
this.worldPosition
|
data.suitcasePos != null ? data.suitcasePos : this.worldPosition
|
||||||
);
|
);
|
||||||
enteredPlayers.remove(i);
|
enteredPlayers.remove(i);
|
||||||
setChangedAndNotify();
|
setChangedAndNotify();
|
||||||
@ -171,7 +187,11 @@ public class SuitcaseBlockEntity extends BlockEntity {
|
|||||||
@Override
|
@Override
|
||||||
public @NotNull CompoundTag getUpdateTag() {
|
public @NotNull CompoundTag getUpdateTag() {
|
||||||
CompoundTag nbt = new CompoundTag();
|
CompoundTag nbt = new CompoundTag();
|
||||||
this.saveAdditional(nbt);
|
if (boundKeystoneName != null) {
|
||||||
|
nbt.putString("BoundKeystone", boundKeystoneName);
|
||||||
|
}
|
||||||
|
nbt.putBoolean("Locked", isLocked);
|
||||||
|
nbt.putBoolean("DimensionLocked", dimensionLocked);
|
||||||
return nbt;
|
return nbt;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,13 +202,16 @@ public class SuitcaseBlockEntity extends BlockEntity {
|
|||||||
return (suitcases != null) ? suitcases.get(playerUuid) : null;
|
return (suitcases != null) ? suitcases.get(playerUuid) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void removeSuitcaseEntry(String keystoneName, String playerUuid) {
|
public static void removeSuitcaseEntry(String keystoneName, String playerUuid, MinecraftServer server) {
|
||||||
Map<String, BlockPos> suitcases = SUITCASE_REGISTRY.get(keystoneName);
|
Map<String, BlockPos> suitcases = SUITCASE_REGISTRY.get(keystoneName);
|
||||||
if (suitcases != null) {
|
if (suitcases != null) {
|
||||||
suitcases.remove(playerUuid);
|
suitcases.remove(playerUuid);
|
||||||
if (suitcases.isEmpty()) {
|
if (suitcases.isEmpty()) {
|
||||||
SUITCASE_REGISTRY.remove(keystoneName);
|
SUITCASE_REGISTRY.remove(keystoneName);
|
||||||
}
|
}
|
||||||
|
if (server != null) {
|
||||||
|
SuitcaseRegistryState.onRegistryChanged(server);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
package io.lampnet.travelerssuitcase.item;
|
||||||
|
|
||||||
import io.lampnet.travelerssuitcase.TravelersSuitcase;
|
import io.lampnet.travelerssuitcase.TravelersSuitcase;
|
||||||
|
import io.lampnet.travelerssuitcase.world.Fantasy;
|
||||||
|
|
||||||
|
import io.lampnet.travelerssuitcase.world.FantasyWorldAccess;
|
||||||
|
import io.lampnet.travelerssuitcase.world.PortalChunkGenerator;
|
||||||
|
import io.lampnet.travelerssuitcase.world.RuntimeWorldConfig;
|
||||||
|
import io.lampnet.travelerssuitcase.world.RuntimeWorldHandle;
|
||||||
import net.minecraft.ChatFormatting;
|
import net.minecraft.ChatFormatting;
|
||||||
import net.minecraft.world.item.TooltipFlag;
|
import net.minecraft.core.registries.Registries;
|
||||||
import net.minecraft.world.item.enchantment.Enchantments;
|
import net.minecraft.nbt.CompoundTag;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraft.server.MinecraftServer;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.sounds.SoundEvents;
|
||||||
|
import net.minecraft.sounds.SoundSource;
|
||||||
|
import net.minecraft.world.InteractionHand;
|
||||||
|
import net.minecraft.world.InteractionResultHolder;
|
||||||
import net.minecraft.world.entity.Entity;
|
import net.minecraft.world.entity.Entity;
|
||||||
import net.minecraft.world.entity.player.Player;
|
import net.minecraft.world.entity.player.Player;
|
||||||
import net.minecraft.world.item.Item;
|
import net.minecraft.world.item.Item;
|
||||||
import net.minecraft.world.item.ItemStack;
|
import net.minecraft.world.item.ItemStack;
|
||||||
import net.minecraft.nbt.CompoundTag;
|
import net.minecraft.world.item.TooltipFlag;
|
||||||
import net.minecraft.server.MinecraftServer;
|
import net.minecraft.world.item.enchantment.Enchantments;
|
||||||
import net.minecraft.sounds.SoundSource;
|
|
||||||
import net.minecraft.sounds.SoundEvents;
|
|
||||||
import net.minecraft.network.chat.Component;
|
|
||||||
import net.minecraft.world.InteractionHand;
|
|
||||||
import net.minecraft.world.InteractionResultHolder;
|
|
||||||
import net.minecraft.world.level.Level;
|
import net.minecraft.world.level.Level;
|
||||||
|
import net.minecraft.world.level.block.Block;
|
||||||
|
import net.minecraft.world.level.block.Mirror;
|
||||||
|
import net.minecraft.world.level.block.Rotation;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkStatus;
|
||||||
|
import net.minecraft.server.level.TicketType;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
|
||||||
import net.minecraft.world.level.storage.LevelResource;
|
import net.minecraft.world.level.storage.LevelResource;
|
||||||
import net.minecraft.resources.ResourceLocation;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
import net.minecraft.world.item.ItemStack.TooltipPart;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@ -39,35 +55,111 @@ public class KeystoneItem extends Item {
|
|||||||
@Override
|
@Override
|
||||||
public @NotNull InteractionResultHolder<ItemStack> use(@NotNull Level world, Player player, @NotNull InteractionHand hand) {
|
public @NotNull InteractionResultHolder<ItemStack> use(@NotNull Level world, Player player, @NotNull InteractionHand hand) {
|
||||||
ItemStack stack = player.getItemInHand(hand);
|
ItemStack stack = player.getItemInHand(hand);
|
||||||
String keystoneName = stack.hasCustomHoverName() ? stack.getHoverName().getString().toLowerCase() : "";
|
|
||||||
String defaultName = "item.travelerssuitcase.keystone";
|
|
||||||
if (!stack.hasCustomHoverName() || keystoneName.isEmpty() || keystoneName.equals(Component.translatable(defaultName).getString().toLowerCase())) {
|
|
||||||
return InteractionResultHolder.pass(stack);
|
|
||||||
}
|
|
||||||
if (world.isClientSide) {
|
|
||||||
return InteractionResultHolder.success(stack);
|
|
||||||
}
|
|
||||||
if (stack.isEnchanted()) {
|
if (stack.isEnchanted()) {
|
||||||
return InteractionResultHolder.pass(stack);
|
return InteractionResultHolder.pass(stack);
|
||||||
}
|
}
|
||||||
String dimensionName = "pocket_dimension_" + keystoneName.replaceAll("[^a-z0-9_]", "");
|
|
||||||
createDimension(world.getServer(), dimensionName);
|
if (!isValidKeystone(stack)) {
|
||||||
stack.enchant(Enchantments.BINDING_CURSE, 1);
|
return InteractionResultHolder.pass(stack);
|
||||||
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);
|
|
||||||
return InteractionResultHolder.success(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) {
|
public static boolean isValidKeystone(ItemStack stack) {
|
||||||
String keystoneName = stack.hasCustomHoverName() ? stack.getHoverName().getString().toLowerCase() : "";
|
String keystoneName = stack.hasCustomHoverName() ? stack.getHoverName().getString().toLowerCase() : "";
|
||||||
String defaultNameKey = "item.travelerssuitcase.keystone";
|
String defaultNameKey = "item.travelerssuitcase.keystone";
|
||||||
return stack.hasCustomHoverName() && !keystoneName.isEmpty() &&
|
return stack.hasCustomHoverName() && !keystoneName.isEmpty() &&
|
||||||
!keystoneName.equals(Component.translatable(defaultNameKey).getString().toLowerCase()) &&
|
!keystoneName.equals(Component.translatable(defaultNameKey).getString().toLowerCase());
|
||||||
stack.isEnchanted();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -84,104 +176,37 @@ public class KeystoneItem extends Item {
|
|||||||
public void inventoryTick(ItemStack stack, @NotNull Level world, @NotNull Entity entity, int slot, boolean selected) {
|
public void inventoryTick(ItemStack stack, @NotNull Level world, @NotNull Entity entity, int slot, boolean selected) {
|
||||||
String keystoneName = stack.hasCustomHoverName() ? stack.getHoverName().getString().toLowerCase() : "";
|
String keystoneName = stack.hasCustomHoverName() ? stack.getHoverName().getString().toLowerCase() : "";
|
||||||
String defaultNameKey = "item.travelerssuitcase.keystone";
|
String defaultNameKey = "item.travelerssuitcase.keystone";
|
||||||
if (!stack.hasCustomHoverName() || keystoneName.isEmpty() ||
|
boolean hasValidName = isValidKeystone(stack);
|
||||||
keystoneName.equals(Component.translatable(defaultNameKey).getString().toLowerCase())) {
|
|
||||||
|
if (!hasValidName) {
|
||||||
CompoundTag nbt = stack.getOrCreateTag();
|
CompoundTag nbt = stack.getOrCreateTag();
|
||||||
nbt.putInt("CustomModelData", 1);
|
nbt.putInt("CustomModelData", 1);
|
||||||
} else if (stack.getTag() != null && stack.getTag().contains("CustomModelData")) {
|
} else if (stack.getTag() != null && stack.getTag().contains("CustomModelData")) {
|
||||||
stack.getTag().remove("CustomModelData");
|
stack.getTag().remove("CustomModelData");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private boolean createDimension(MinecraftServer server, String dimensionName) {
|
if (!world.isClientSide() && hasValidName && !stack.isEnchanted()) {
|
||||||
if (server == null) {
|
String dimensionName = "pocket_dimension_" + keystoneName.replaceAll("[^a-z0-9_]", "");
|
||||||
TravelersSuitcase.LOGGER.error("Failed to create dimension: " + dimensionName + " (MinecraftServer instance is null)");
|
createOrLoadPersistentDimension(world.getServer(), dimensionName);
|
||||||
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);
|
|
||||||
|
|
||||||
ResourceLocation dimensionKeyLocation = new ResourceLocation("travelerssuitcase", dimensionName);
|
stack.enchant(Enchantments.BINDING_CURSE, 1);
|
||||||
boolean isDimensionRegistered = server.levelKeys().stream()
|
CompoundTag nbt = stack.getOrCreateTag();
|
||||||
.anyMatch(key -> key.location().equals(dimensionKeyLocation));
|
nbt.putInt("RepairCost", 32767);
|
||||||
|
|
||||||
Path dimensionRegistryPath = server.getWorldPath(LevelResource.ROOT)
|
if (entity instanceof Player player) {
|
||||||
.resolve("data")
|
world.playSound(null, player.getX(), player.getY(), player.getZ(),
|
||||||
.resolve("travelerssuitcase")
|
SoundEvents.AMETHYST_CLUSTER_FALL, SoundSource.PLAYERS, 2.0F, 2.0F);
|
||||||
.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 (!isDimensionInRegistryFile && !isDimensionRegistered) {
|
if (!world.isClientSide() && stack.hasTag()) {
|
||||||
Path structureMarkerPath = server.getWorldPath(LevelResource.ROOT)
|
assert stack.getTag() != null;
|
||||||
.resolve("data")
|
if (stack.getTag().contains("Enchantments")) {
|
||||||
.resolve("travelerssuitcase")
|
CompoundTag nbt = stack.getOrCreateTag();
|
||||||
.resolve("pending_structures");
|
if (nbt.getInt("RepairCost") < 32767) {
|
||||||
Files.createDirectories(structureMarkerPath);
|
nbt.putInt("RepairCost", 32767);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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",
|
"compatibilityLevel": "JAVA_8",
|
||||||
"refmap": "travelerssuitcase.refmap.json",
|
"refmap": "travelerssuitcase.refmap.json",
|
||||||
"mixins": [
|
"mixins": [
|
||||||
|
"MinecraftServerAccess",
|
||||||
|
"LevelStemMixin",
|
||||||
|
"MappedRegistryMixin",
|
||||||
|
"ServerWorldMixin",
|
||||||
|
"ServerChunkManagerMixin"
|
||||||
],
|
],
|
||||||
"client": [
|
"client": [
|
||||||
],
|
],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user