Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for re-creating persistent worlds #33

Open
LCLPYT opened this issue Oct 8, 2023 · 5 comments
Open

Support for re-creating persistent worlds #33

LCLPYT opened this issue Oct 8, 2023 · 5 comments

Comments

@LCLPYT
Copy link
Contributor

LCLPYT commented Oct 8, 2023

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
Copy link

Aareon commented May 4, 2024

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

@Aareon
Copy link

Aareon commented May 4, 2024

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
Copy link
Contributor Author

LCLPYT commented May 4, 2024

@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
Copy link
Contributor Author

LCLPYT commented May 4, 2024

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
Copy link

Aareon commented May 4, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants