Skip to content

The Generic Modification System

Luke Tonon edited this page Dec 12, 2021 · 10 revisions

The Generic Modification System

Blueprint includes a package that contains various code files to ease the creation of data-driven modification systems.

Before continuing, we recommend reading the Modifier Systems page first.

Modifiers

Modifiers are the most important part of the Generic Modification System as they represent an object that can modify a specific type of object in a configurable way.

The IModifier interface represents a Modifier. This interface tells how to modify a specific object type using a config through its modify() method. Additionally, the interface contains serialize() and deserialize() methods for serialization and deserialization of its config type.

Simply put, the IModifier interface is an unconfigured modification procedure.

Configuring Modifiers

The ConfiguredModifier class represents a Configured Modifier.

Instances of the ConfiguredModifier class contain an IModifier instance and a config object instance. This class also contains a modify() method for using the internal modifier with the internal config object to modify a specific type of object.

With this in mind, a JSON structure for a ConfiguredModifier would look like the following:

{
	"type": "some_modifier_type",
	"config": (uses structure of the type's config)
}

Registering Modifiers

To start a new Modification System, a ModifierRegistry must get created.

A ModifierRegistry contains a BiMap used for looking up IModifier instances by name and vice-versa. This class is so important because instances of it get used for serializing and deserializing ConfiguredModifier instances.

A registry class for modifiers that modify a specific type of object may look something like the following:

public final class MyModifiers {
	public static final ModifierRegistry<TypeToModify, MySerializationContext, MyDeserializationContext> REGISTRY = new ModifierRegistry<>();
	
	public static final MyModifier MY_MODIFIER_A = REGISTRY.register("my_a", new MyModifier(...));
}

We recommend you leave your ModifierRegistry instances as public so other mods can add their own modifiers to get used in your systems.

Selecting Modifier Targets

Data-driven modifiers are nothing without a way to target which objects you want to modify. Blueprint has a built-in system for targeting a specific list of named resources, known as the Modifier Target Selector System.

Modifier Target Selectors

A modifier target selector is represented by the ModifierTargetSelector interface. This interface tells how to select targets using a config through its getTargetNames() method. Additionally, the interface contains serialize() and deserialize() methods for serialization and deserialization of its config type.

Built-in Modifier Target Selectors

Blueprint has a few built-in ModifierTargetSelector implementations:

Type Usage Config
choice Chooses to use one of two configured selectors based on a condition. {"first":(configured selector),"second":(configured selector),"condition":(Forge ICondition)}
empty Chooses no targets. {}
multi Chooses targets using multiple configured selectors. [(configured selector)...]
names Chooses a list of configured targets. ["name"...]
regex Chooses a list of targets based on a configured regular expression. (regular expression)

Configuring Modifier Target Selectors

A configured modifier target selector is represented by the ConfiguredModifierTargetSelector class.

Instances of the ConfiguredModifierTargetSelector class contain an ModifierTargetSelector instance and a config object instance. This class also contains a getTargetNames() method for using the internal modifier target selector with the internal config object to select targets.

Also, when a ConfiguredModifierTargetSelector instance gets deserialized, a conditions field is used to disable the selector if a list of conditions is true.

The JSON structure for a ConfiguredModifierTargetSelector instance would look like the following:

{
	"type": "some_type",
	"config": (uses structure of the type's config),
	"conditions": [(Forge ICondition)...]
}

Targeted Modifiers

Modifiers and target selectors are useful on their own, but combining them allows for even greater possibilities.

The TargetedModifier class represents a list of configured modifiers with conditions, a target selector, and a priority.

The priority field takes on one of Forge's EventPriority enum values. These priorities go from HIGHEST to LOWEST and are used to prioritize the processing of configured modifiers to assist in resolving compatibility issues between configured modifiers.

TargetedModifier instances are not stored in memory for re-use but are used for data generators and mapping out prioritized configured modifiers to specific targets.

The JSON structure for a TargetedModifier instance is as follows:

{
	"target": {
		"type": "modifier_target_selector_type",
		"config": (uses structure of the type's config)
	},
	"modifiers": [(configured modifier)...]
}

Registering Modifier Target Selectors

All modification systems use the ModifierTargetSelectorRegistry enum as the registry for ModifierTargetSelector instances. This means that every registered ModifierTargetSelector instance can get used in every modifier system that uses the Generic Modification System.

Modders can register their own ModifierTargetSelector implementations simply by doing ModifierTargetSelectorRegistry.INSTANCE.register(name, selector) during mod loading.

Modification Data Managers

A Modification Data Manager is simply a name for a system that deserializes and runs targeted modifiers that modify a specific type of object.

The Generic Modification System does not require modders to manage the deserialization of their targeted modifiers in a specific way but does include the ModificationManager class to simplify the creation of managers for simple JSON reloaders.

Data Generators for Modifiers

We all know how annoying it can be to make tons of JSON files for stuff by hand, and fortunately, Blueprint comes with the ModifierDataProvider class to mitigate this for modifiers.

This class is pretty complex like many other parts of the Generic Modification System as it needs to be robust. The ModifierDataProvider class implements the DataProvider interface to get used as a data generator.

Like many other DataProvider implementations, the class contains a DataGenerator instance, a String name, and a Gson instance. The rest of the fields in the class are unique, so a table is provided for them:

Field Type Purpose
pathResolver BiFunction<Path, ProviderEntry<T, S, D>, Path> To resolve the paths for ProviderEntry instances.
targetKeyName String Tells what name to use for the targeted modifiers' target key.
modifierRegistry ModifierRegistry For serializing the configured modifier instances.
additionalSerializationGetter Function<TargetedModifier<T, S, D>, S> To get the additional serialization object for TargetedModifier instances.
entries List<ProviderEntry<T, S, D>> To store the list of ProviderEntry instances to serialize.

Blueprint has a few of its own usages of the ModifierDataProvider class, which are in LootModifiers#createDataProvider() and AdvancementModifiers#createDataProvider().

Provider Entries

The ProviderEntry static nested class represents an entry to get serialized by a ModifierDataProvider instance. This class contains a TargetedModifier instance, a 2D array for the conditions for the TargetedModifier instance, and a ResourceLocation name for identifying the entry.

ModifierDataProvider instances use a list of ProviderEntry instances to serialize TargetedModifier instances with conditions.