From 9338490eb4ca863ba7987d5d9d0939617f4901a9 Mon Sep 17 00:00:00 2001 From: IsaiahPatton Date: Wed, 9 Aug 2023 06:06:04 -0400 Subject: [PATCH] Backport: Add support for world unloading, fix world data being kept in level.dat (again), fix random crashes when adding/removing a world --- gradle.properties | 2 +- .../java/xyz/nucleoid/fantasy/Fantasy.java | 22 +++++ .../fantasy/FantasyDimensionOptions.java | 3 + .../nucleoid/fantasy/RuntimeWorldHandle.java | 7 ++ .../nucleoid/fantasy/RuntimeWorldManager.java | 31 +++++++ .../fantasy/mixin/MinecraftServerMixin.java | 19 +++++ .../mixin/registry/DimensionOptionsMixin.java | 12 +++ .../DimensionOptionsRegistryHolderMixin.java | 20 +++++ .../nucleoid/fantasy/util/SafeIterator.java | 23 ++++++ src/main/resources/fantasy.mixins.json | 1 + .../fantasy/test/FantasyInitializer.java | 81 +++++++++++++++++-- 11 files changed, 212 insertions(+), 9 deletions(-) create mode 100644 src/main/java/xyz/nucleoid/fantasy/mixin/MinecraftServerMixin.java create mode 100644 src/main/java/xyz/nucleoid/fantasy/mixin/registry/DimensionOptionsRegistryHolderMixin.java create mode 100644 src/main/java/xyz/nucleoid/fantasy/util/SafeIterator.java diff --git a/gradle.properties b/gradle.properties index d113550..1b37584 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,7 +7,7 @@ yarn_mappings=1.19+build.1 loader_version=0.14.9 # Mod Properties -mod_version=0.4.9 +mod_version=0.4.10 maven_group=xyz.nucleoid archives_base_name=fantasy diff --git a/src/main/java/xyz/nucleoid/fantasy/Fantasy.java b/src/main/java/xyz/nucleoid/fantasy/Fantasy.java index a0cf3a3..a019094 100644 --- a/src/main/java/xyz/nucleoid/fantasy/Fantasy.java +++ b/src/main/java/xyz/nucleoid/fantasy/Fantasy.java @@ -47,6 +47,7 @@ public final class Fantasy { private final RuntimeWorldManager worldManager; private final Set deletionQueue = new ReferenceOpenHashSet<>(); + private final Set unloadingQueue = new ReferenceOpenHashSet<>(); static { ServerTickEvents.START_SERVER_TICK.register(server -> { @@ -88,6 +89,11 @@ private void tick() { if (!deletionQueue.isEmpty()) { deletionQueue.removeIf(this::tickDeleteWorld); } + + Set unloadingQueue = this.unloadingQueue; + if (!unloadingQueue.isEmpty()) { + unloadingQueue.removeIf(this::tickUnloadWorld); + } } /** @@ -159,6 +165,12 @@ void enqueueWorldDeletion(ServerWorld world) { }); } + void enqueueWorldUnloading(ServerWorld world) { + this.server.submit(() -> { + this.unloadingQueue.add(world); + }); + } + private boolean tickDeleteWorld(ServerWorld world) { if (this.isWorldUnloaded(world)) { this.worldManager.delete(world); @@ -169,6 +181,16 @@ private boolean tickDeleteWorld(ServerWorld world) { } } + private boolean tickUnloadWorld(ServerWorld world) { + if (this.isWorldUnloaded(world)) { + this.worldManager.unload(world); + return true; + } else { + this.kickPlayers(world); + return false; + } + } + private void kickPlayers(ServerWorld world) { if (world.getPlayers().isEmpty()) { return; diff --git a/src/main/java/xyz/nucleoid/fantasy/FantasyDimensionOptions.java b/src/main/java/xyz/nucleoid/fantasy/FantasyDimensionOptions.java index edbb3fe..656a7d0 100644 --- a/src/main/java/xyz/nucleoid/fantasy/FantasyDimensionOptions.java +++ b/src/main/java/xyz/nucleoid/fantasy/FantasyDimensionOptions.java @@ -6,7 +6,10 @@ public interface FantasyDimensionOptions { Predicate SAVE_PREDICATE = (e) -> ((FantasyDimensionOptions) (Object) e).fantasy$getSave(); + Predicate SAVE_PROPERTIES_PREDICATE = (e) -> ((FantasyDimensionOptions) (Object) e).fantasy$getSaveProperties(); void fantasy$setSave(boolean value); boolean fantasy$getSave(); + void fantasy$setSaveProperties(boolean value); + boolean fantasy$getSaveProperties(); } diff --git a/src/main/java/xyz/nucleoid/fantasy/RuntimeWorldHandle.java b/src/main/java/xyz/nucleoid/fantasy/RuntimeWorldHandle.java index 4594d4b..cc14008 100644 --- a/src/main/java/xyz/nucleoid/fantasy/RuntimeWorldHandle.java +++ b/src/main/java/xyz/nucleoid/fantasy/RuntimeWorldHandle.java @@ -21,6 +21,13 @@ public void delete() { this.fantasy.enqueueWorldDeletion(this.world); } + /** + * Unloads the world. It only deletes the files if world is temporary. + */ + public void unload() { + this.fantasy.enqueueWorldUnloading(this.world); + } + public ServerWorld asWorld() { return this.world; } diff --git a/src/main/java/xyz/nucleoid/fantasy/RuntimeWorldManager.java b/src/main/java/xyz/nucleoid/fantasy/RuntimeWorldManager.java index 6bdbd9d..55bedaf 100644 --- a/src/main/java/xyz/nucleoid/fantasy/RuntimeWorldManager.java +++ b/src/main/java/xyz/nucleoid/fantasy/RuntimeWorldManager.java @@ -4,6 +4,8 @@ import net.fabricmc.fabric.api.event.lifecycle.v1.ServerWorldEvents; import net.minecraft.server.MinecraftServer; import net.minecraft.server.world.ServerWorld; +import net.minecraft.text.Text; +import net.minecraft.util.ProgressListener; import net.minecraft.util.registry.Registry; import net.minecraft.util.registry.RegistryKey; import net.minecraft.util.registry.SimpleRegistry; @@ -32,6 +34,7 @@ RuntimeWorld add(RegistryKey worldKey, RuntimeWorldConfig config, Runtime if (style == RuntimeWorld.Style.TEMPORARY) { ((FantasyDimensionOptions) (Object) options).fantasy$setSave(false); } + ((FantasyDimensionOptions) (Object) options).fantasy$setSaveProperties(false); SimpleRegistry dimensionsRegistry = getDimensionsRegistry(this.server); boolean isFrozen = ((RemoveFromRegistry) dimensionsRegistry).fantasy$isFrozen(); @@ -78,6 +81,34 @@ void delete(ServerWorld world) { } } + void unload(ServerWorld world) { + RegistryKey dimensionKey = world.getRegistryKey(); + + if (this.serverAccess.getWorlds().remove(dimensionKey, world)) { + world.save(new ProgressListener() { + @Override + public void setTitle(Text title) {} + + @Override + public void setTitleAndTask(Text title) {} + + @Override + public void setTask(Text task) {} + + @Override + public void progressStagePercentage(int percentage) {} + + @Override + public void setDone() { + ServerWorldEvents.UNLOAD.invoker().onWorldUnload(RuntimeWorldManager.this.server, world); + + SimpleRegistry dimensionsRegistry = getDimensionsRegistry(RuntimeWorldManager.this.server); + RemoveFromRegistry.remove(dimensionsRegistry, dimensionKey.getValue()); + } + }, true, false); + } + } + private static SimpleRegistry getDimensionsRegistry(MinecraftServer server) { GeneratorOptions generatorOptions = server.getSaveProperties().getGeneratorOptions(); return (SimpleRegistry) generatorOptions.getDimensions(); diff --git a/src/main/java/xyz/nucleoid/fantasy/mixin/MinecraftServerMixin.java b/src/main/java/xyz/nucleoid/fantasy/mixin/MinecraftServerMixin.java new file mode 100644 index 0000000..c585a30 --- /dev/null +++ b/src/main/java/xyz/nucleoid/fantasy/mixin/MinecraftServerMixin.java @@ -0,0 +1,19 @@ +package xyz.nucleoid.fantasy.mixin; + +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.world.ServerWorld; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; +import xyz.nucleoid.fantasy.util.SafeIterator; + +import java.util.Collection; +import java.util.Iterator; + +@Mixin(MinecraftServer.class) +public class MinecraftServerMixin { + @Redirect(method = "tickWorlds", at = @At(value = "INVOKE", target = "Ljava/lang/Iterable;iterator()Ljava/util/Iterator;", ordinal = 0), require = 0) + private Iterator fantasy$copyBeforeTicking(Iterable instance) { + return new SafeIterator<>((Collection) instance); + } +} \ No newline at end of file diff --git a/src/main/java/xyz/nucleoid/fantasy/mixin/registry/DimensionOptionsMixin.java b/src/main/java/xyz/nucleoid/fantasy/mixin/registry/DimensionOptionsMixin.java index 63149d7..93c5001 100644 --- a/src/main/java/xyz/nucleoid/fantasy/mixin/registry/DimensionOptionsMixin.java +++ b/src/main/java/xyz/nucleoid/fantasy/mixin/registry/DimensionOptionsMixin.java @@ -9,6 +9,8 @@ public class DimensionOptionsMixin implements FantasyDimensionOptions { @Unique private boolean fantasy$save = true; + @Unique + private boolean fantasy$saveProperties = true; @Override public void fantasy$setSave(boolean value) { @@ -19,4 +21,14 @@ public class DimensionOptionsMixin implements FantasyDimensionOptions { 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; + } } diff --git a/src/main/java/xyz/nucleoid/fantasy/mixin/registry/DimensionOptionsRegistryHolderMixin.java b/src/main/java/xyz/nucleoid/fantasy/mixin/registry/DimensionOptionsRegistryHolderMixin.java new file mode 100644 index 0000000..4287e1e --- /dev/null +++ b/src/main/java/xyz/nucleoid/fantasy/mixin/registry/DimensionOptionsRegistryHolderMixin.java @@ -0,0 +1,20 @@ +package xyz.nucleoid.fantasy.mixin.registry; + +// import net.minecraft.registry.Registry; +import net.minecraft.world.dimension.DimensionOptions; +// import net.minecraft.world.dimension.DimensionOptionsRegistryHolder; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyArg; +import xyz.nucleoid.fantasy.FantasyDimensionOptions; +import xyz.nucleoid.fantasy.util.FilteredRegistry; + +import java.util.function.Function; + +// @Mixin(DimensionOptionsRegistryHolder.class) +public class DimensionOptionsRegistryHolderMixin { + //@ModifyArg(method = "method_45516", at = @At(value = "INVOKE", target = "Lcom/mojang/serialization/MapCodec;forGetter(Ljava/util/function/Function;)Lcom/mojang/serialization/codecs/RecordCodecBuilder;")) + //private static Function> fantasy$swapRegistryGetter(Function> getter) { + // return (x) -> new FilteredRegistry<>(getter.apply(x), FantasyDimensionOptions.SAVE_PROPERTIES_PREDICATE); + //} +} \ No newline at end of file diff --git a/src/main/java/xyz/nucleoid/fantasy/util/SafeIterator.java b/src/main/java/xyz/nucleoid/fantasy/util/SafeIterator.java new file mode 100644 index 0000000..af23052 --- /dev/null +++ b/src/main/java/xyz/nucleoid/fantasy/util/SafeIterator.java @@ -0,0 +1,23 @@ +package xyz.nucleoid.fantasy.util; + +import java.util.Collection; +import java.util.Iterator; + +public final class SafeIterator implements Iterator { + private final Object[] values; + private int index = 0; + + public SafeIterator(Collection source) { + this.values = source.toArray(); + } + + @Override + public boolean hasNext() { + return this.values.length > this.index; + } + + @Override + public T next() { + return (T) this.values[this.index++]; + } +} \ No newline at end of file diff --git a/src/main/resources/fantasy.mixins.json b/src/main/resources/fantasy.mixins.json index 876e495..eb67e78 100644 --- a/src/main/resources/fantasy.mixins.json +++ b/src/main/resources/fantasy.mixins.json @@ -5,6 +5,7 @@ "compatibilityLevel": "JAVA_17", "mixins": [ "MinecraftServerAccess", + "MinecraftServerMixin", "ServerChunkManagerMixin", "ServerWorldMixin", "registry.DimensionOptionsMixin", diff --git a/src/testmod/java/xyz/nucleoid/fantasy/test/FantasyInitializer.java b/src/testmod/java/xyz/nucleoid/fantasy/test/FantasyInitializer.java index 9ab5d64..813d0be 100644 --- a/src/testmod/java/xyz/nucleoid/fantasy/test/FantasyInitializer.java +++ b/src/testmod/java/xyz/nucleoid/fantasy/test/FantasyInitializer.java @@ -1,27 +1,92 @@ package xyz.nucleoid.fantasy.test; -import com.mojang.brigadier.CommandDispatcher; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.fabricmc.fabric.api.dimension.v1.FabricDimensions; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; -import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.command.argument.IdentifierArgumentType; +import net.minecraft.text.Text; import net.minecraft.util.Identifier; +import net.minecraft.util.math.Vec3d; +import net.minecraft.util.profiler.Profiler; import net.minecraft.util.registry.Registry; +import net.minecraft.world.TeleportTarget; import xyz.nucleoid.fantasy.Fantasy; import xyz.nucleoid.fantasy.RuntimeWorldConfig; +import xyz.nucleoid.fantasy.RuntimeWorldHandle; import xyz.nucleoid.fantasy.util.VoidChunkGenerator; +import java.util.HashMap; +import java.util.WeakHashMap; + +import static net.minecraft.server.command.CommandManager.argument; +import static net.minecraft.server.command.CommandManager.literal; + public final class FantasyInitializer implements ModInitializer { + private HashMap worlds = new HashMap<>(); + @Override public void onInitialize() { ServerLifecycleEvents.SERVER_STARTED.register((s) -> { Fantasy.get(s).openTemporaryWorld(new RuntimeWorldConfig().setGenerator(new VoidChunkGenerator(s.getRegistryManager().get(Registry.BIOME_KEY).getEntry(0).get()))); - Fantasy.get(s).getOrOpenPersistentWorld( - new Identifier("fantasytest:test"), - new RuntimeWorldConfig() - .setGenerator(s.getOverworld().getChunkManager().getChunkGenerator()) - ); }); - } + CommandRegistrationCallback.EVENT.register(((dispatcher, registryAccess, environment) -> { + dispatcher.register(literal("fantasy_open").then( + argument("name", IdentifierArgumentType.identifier()) + .executes((context -> { + try { + var t = System.currentTimeMillis(); + var id = IdentifierArgumentType.getIdentifier(context, "name"); + + var x = Fantasy.get(context.getSource().getServer()).getOrOpenPersistentWorld( + id, + new RuntimeWorldConfig() + .setGenerator(context.getSource().getServer().getOverworld().getChunkManager().getChunkGenerator()) + .setSeed(id.hashCode()) + ); + context.getSource().sendFeedback(Text.literal("WorldCreate: " + (System.currentTimeMillis() - t)), false); + + worlds.put(id, x); + t = System.currentTimeMillis(); + FabricDimensions.teleport(context.getSource().getEntity(), x.asWorld(), new TeleportTarget(new Vec3d(0, 100 ,0) , Vec3d.ZERO, 0, 0)); + context.getSource().sendFeedback(Text.literal("Teleport: " + (System.currentTimeMillis() - t)), false); + } catch (Throwable e) { + e.printStackTrace(); + } + + return 0; + })) + )); + + dispatcher.register(literal("fantasy_delete").then( + argument("name", IdentifierArgumentType.identifier()) + .executes((context -> { + try { + var id = IdentifierArgumentType.getIdentifier(context, "name"); + worlds.get(id).delete(); + worlds.remove(id); + } catch (Throwable e) { + e.printStackTrace(); + } + return 0; + })) + )); + + dispatcher.register(literal("fantasy_unload").then( + argument("name", IdentifierArgumentType.identifier()) + .executes((context -> { + try { + var id = IdentifierArgumentType.getIdentifier(context, "name"); + worlds.get(id).unload(); + worlds.remove(id); + } catch (Throwable e) { + e.printStackTrace(); + } + + return 0; + })) + )); + })); + } }