Skip to content

Instantly share code, notes, and snippets.

@lokka30
Last active August 30, 2022 03:45
Show Gist options
  • Select an option

  • Save lokka30/8f756dd239254869e95e4eb77d3f526e to your computer and use it in GitHub Desktop.

Select an option

Save lokka30/8f756dd239254869e95e4eb77d3f526e to your computer and use it in GitHub Desktop.
LevelledMobs 4: GodsEye Presets System Brainstorming

LM4 Brainstorming - GodsEye Presets System

Today I wish to brainstorm the "GodsEye Presets System" that LM4 could incorporate.

Reminders

... since it's been a while we worked on LM4

  • A Process is identical to a Rule in LM3.
    • We renamed it in LM4 to be more logical relative to the Functions System we created.
  • Processes (again, 'rules'...) have been moved to settings.yml.
    • There is no more rules.yml.
    • Also remember presets.yml, groups.yml, etc.

Overview

This presets system is special: instead of presets only being usable in certain places, such as the root of a Process with use-presets: [...], the GodsEye presets system allows users to declare use-presets wherever they wish. This greatly expands the wealth of customisation that users have at their fingertips, allowing us and users to reduce boilerplate in settings.yml to the maximum extent.

The reason I named this presets system 'GodsEye' is because the Presets Parser will be able to work in every nook and cranny in settings.yml, as I explained above.

Technical Side

In the file-loading stage of LM's startup, Configurate will load the YAML files as per normal. However, for the settings file, it will load its standard "synchronised" root node object AND then store a 'clone' of that object. The cloned object is treated as the standard one to use throughout the plugin, and the synchronised one is used only when the file needs to be updated between file-versions.

The reason we are using a clone of the root node instead of the actual file-synchronised object (where we can make edits and save them to the file - file-synchronised) is because we want to allow the Presets System to modify the file in memory but not save these changes back to the file. This allows us to very easily parse the settings file alongside the presets declared within it so that the Functions System doesn't even need to know that parts of the file are using presets at all. This massively simplifies the logic for each and every condition and action class in the parsing stage.

Once the cloned object is made, and the Presets file is loaded, then LM will parse the settings file. The first part in the parsing stage, before it converts any functions to objects and so on, is preset replacement. Anywhere it says use-presets, it will do this:

  1. grab the config node for the preset
  2. clone it
  3. merge the node where the use-presets was declared into the cloned preset one
  4. set the original use-presets declaration node to the cloned preset one

That might sound a little complex, though this visual represention may help you understand it further...

Example for Visual Representation

Presentation section

'preset':
  something: true
  another_thing:
    a: false
    b: ['Hey']
    x:
      a: false
    z: "Cool!"

Declared section

some-place-in-settings-yml:
  use-presets: ['preset']
  something: false
  another_thing:
    b: ['Hey Hey']
    x:
      b: "Cool"
    z: "Very cool"
  another_another_thing: 1.3431

Parsed Declared

Essentially the preset + declared sections, where the declared section overrides the contents of the preset section if both overlap.

some-place-in-settings-yml:
  something: false                  # +base < declared
  another_thing:                    # +base = declared
    a: false                        # +base
    b: ['Hey Hey']                  # +base < declared
    x:                              # +base = declared
      a: false                      # +base
      b: "Cool"                     # +base < declared
    z: "Very cool"                  # +base < declared
  another_another_thing: 1.3431     # +declared

Remember that this is what the parser sees, not what the user's file looks like. The user's file is unaffected. This is all done in memory under a cloned object as I described before.

Parsed Declared - About the # +base... comments

Line feature Description
+base < declared Line added from base then overriden by declared
+base = declared Line added from base and declared but unchanged
+base Line added from base but not declared
+declared Line added from declared

Hopefully this system will work properly with lists and so on.

Problems Solved

  • In the last presets system brainstorm, we forced users to state whether they wanted their preset to "add" to a process or "override" conditions/actions of the same ID.
    • This was messy and convoluted, and forced some situations to use a lot of boilerplate.
    • This problem is solved because now users can achieve the same functionality with far less boilerplate as they can specify any configuration node to use a preset rather than only the root node of each Process. They can override specific actions/conditions, or they can add actions/conditions to a Process, whatever they want!
@lokka30
Copy link
Author

lokka30 commented Aug 30, 2022

Working source code demo

public class GodsEye {

    public final YamlConfigurationLoader settingsLoader = YamlConfigurationLoader.builder()
        .path(Path.of(Utils.getWorkingDirectory(), "settings.yml"))
        .build();

    public final YamlConfigurationLoader outLoader = YamlConfigurationLoader.builder()
        .path(Path.of(Utils.getWorkingDirectory(), "out.yml"))
        .build();

    /**
     * NON-PARSED settings root node
     * This has parity with the settings file on the file system
     * any changes to this object will reflect in settings.yml
     * this object will therefore not have any presets copied within it.
     */
    public CommentedConfigurationNode settingsUnparsedRoot;

    /**
     * PARSED settings root node
     * This does not have parity with the settings file on the file system
     * any changes to this object will NOT reflect in settings.yml
     * this object will therefore have presets copied within it.
     */
    public CommentedConfigurationNode settingsParsedRoot;

    public HashMap<String, CommentedConfigurationNode> presets = new HashMap<>();

    public static void main(final String[] args) throws Exception {
        new GodsEye();
    }

    public GodsEye() throws Exception {
        settingsUnparsedRoot = settingsLoader.load();
        settingsParsedRoot = settingsUnparsedRoot.copy();

        parsePresets();
        walkNode(settingsParsedRoot.node("functions"));

        //noinspection ResultOfMethodCallIgnored
        new File(Utils.getWorkingDirectory(), "out.yml").createNewFile();

        outLoader.save(settingsParsedRoot);
    }

    private void parsePresets() {
        for(CommentedConfigurationNode presetNode : settingsParsedRoot.node("presets").childrenList()) {
            // we need to copy the preset node and remove the 'preset' identifier child so that
            // the preset id is not copied into the places where the preset is used :)
            final CommentedConfigurationNode alteredPresetNode = presetNode.copy();
            alteredPresetNode.removeChild("preset");

            presets.put(presetNode.node("preset").getString(), alteredPresetNode);
        }

        System.out.println("Parsed " + presets.size() + " presets.");
    }

    private void walkNode(final CommentedConfigurationNode node) {

        if(node.hasChild("use-presets")) {
            final List<String> presetIds;
            try {
                presetIds = node.node("use-presets").getList(String.class);
                if(presetIds == null) {
                    throw new NullPointerException("presetIds is null");
                }
            } catch (SerializationException e) {
                throw new RuntimeException(e);
            }
            node.removeChild("use-presets");
            presetIds.forEach(presetId -> node.mergeFrom(presets.get(presetId)));
        }

        node.childrenList().forEach(this::walkNode);
        node.childrenMap().values().forEach(this::walkNode);

    }

}

public class Utils {

    public static String getWorkingDirectory() {
        return System.getProperty("user.dir");
    }

}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment