NucleoidMC / fantasy

Library to support creating dimensions at runtime
GNU Lesser General Public License v3.0
95 stars 26 forks source link

Support for re-creating persistent worlds #33

Open LCLPYT opened 11 months ago

LCLPYT commented 11 months ago

The problem

Currently, if you want to load a previously created persistent world, you have to use Fantasy::getOrOpenPersistentWorld. The method obviously requires you to pass a dimension type and chunk generator via a RuntimeWorldConfig. If you created the world with a custom generator, you will have to memorize it somehow. That creates an unnecessary overhead for the developer IMO.

Use case

I personally create worlds for minigames in single player. Often, I change the generator options, such as the dimension type or the chunk generator in the level.dat.

On my minigame server, I copy the level save into the dimensions directory, every time the game starts. I would like to load the map as dimension using the fantasy library. Then, I have to re-create the generator options by hand in Java, for each individual map. In my case, an API would surely be helpful.

Potential solution

I think there should be an API method on the Fantasy class, such as openPersistentWorld(Identifier) in order to open worlds inside the dimensions/ directory of the currently loaded save. The method needs to somehow re-construct the RuntimeWorldConfig, the world was created with.

For this to work, fantasy would also need to save the config/the generator data to disk on persistent world creation.

Implementation details

My proposal would be to check whether there is a level.dat file in the directory of the dimension that should be loaded. If it exists, the code from net.minecraft.world.level.storage.LevelStorage#createLevelDataParser can be used to parse it.

This way, users could also put worlds they created in singleplayer / another world into the dimensions folder of the desired save (my use case).

As for writing the level.dat, one could use net.minecraft.world.level.storage.LevelStorage.Session#backupLevelDataFile().

Maybe there are even better ways to save and load the world data, I am not sure...

Sample implementation

I implemented the level.dat parsing and config re-creation as proof of concept in one of my libraries. If you like the approach, I would be happy to create a pull request providing the feature to fantasy. Please let me know, if the concept / the implementation is to your liking.

I am still working on the leve.dat creation when creating a persistent world. So it can only be tested with other saves ATM.

Aareon commented 4 months ago

+1 for implementing an API for listing and opening previously created dimensions

Aareon commented 4 months ago

Is there a known way of determining what mods created specific dimensions? Perhaps reading from the registry for what dimensions were registered by what mods?

LCLPYT commented 4 months ago

@Aareon I don't think that there is an official API for listing dimensions created by fantasy yet. But maybe this extension can help you? I personally use it in a command that dynamically unloads worlds...

LCLPYT commented 4 months ago

Alternatively you could also read the registry entries of the dimension registry and filter by identifier. However, you cannot get the RuntimeWorldHandle this way...

        DynamicRegistryManager registryManager = server.getCombinedDynamicRegistries().getCombinedRegistryManager();
        Registry<DimensionOptions> dimensionRegistry = registryManager.get(RegistryKeys.DIMENSION);

        // iterate all registered dimensions
        for (RegistryKey<DimensionOptions> key : dimensionsRegistry.getKeys()) {
            Identifier id = key.getValue();

            // filter the dimension by mod namespace
            if (id.getNamespace().equals("target_mod_id")) {
                // do something with it, e.g. get the world
                RegistryKey<World> wKey = RegistryKey.of(RegistryKeys.WORLD, id);
                ServerWorld world = server.getWorld(wKey);
            }
        }

However, this requires that each mod uses an unique namespace to distinguish the worlds by mod.

Aareon commented 4 months ago

Just from looking, this seems like exactly what I'm looking for. Luckily I don't think I need the RuntimeWorldHandle in my case, as I just want to check what namespaces have been registered so that I can blacklist those namespaces from use in my mod. Here's how I go about creating my RuntimeWorldHandle

private static int jumpToDimension(ServerCommandSource source, String namespace, String name) {
        MinecraftServer server = source.getServer();
        ServerPlayerEntity player = Objects.requireNonNull(source.getPlayer());
        Fantasy fantasy = Fantasy.get(server);
        Identifier dimensionId = new Identifier(namespace, name);

        RegistryKey<World> dimensionKey = RegistryKey.of(RegistryKeys.WORLD, dimensionId);

        if (!DimensionUtility.doesDimensionExist(server, dimensionKey)) {
            source.sendFeedback(() -> Text.literal("Dimension §6%s§r:§c%s§r does not exist".formatted(namespace, name)), false);
            return 1;
        }

        RuntimeWorldHandle worldHandle = fantasy.getOrOpenPersistentWorld(dimensionId, DimensionUtility.createStandardVoidConfig(server));
        player.teleport(worldHandle.asWorld(), player.getX(), player.getY() + 1.5, player.getZ(), player.getYaw(), player.getPitch());
        LOGGER.info("Teleported player %s to %s,%s,%s".formatted(player.getName().getString(), player.getX(), player.getY(), player.getZ()));
        source.sendFeedback(() -> Text.literal("Teleported to dimension: §6%s§r:§c%s§r".formatted(namespace, name)), true);
        return 0;
    }

    public static boolean doesDimensionExist(MinecraftServer server, RegistryKey<World> dimensionKey) {
        try {
            ServerWorld world = server.getWorld(dimensionKey);
            if (world != null) {
                return true; // Dimension exists
            }
        } catch (NullPointerException | IllegalArgumentException e) {
            // Dimension does not exist or other error occurred
            return false;
        }
        return false;
    }

I think your extension should help me append new protected namespaces after world initialization to my blacklistedNamespaces key in my config.