From 66ccdef15e8f053c38352e6c6d485bcbbf869812 Mon Sep 17 00:00:00 2001 From: Matyrobbrt <65940752+Matyrobbrt@users.noreply.github.com> Date: Mon, 29 Jan 2024 17:40:29 +0200 Subject: [PATCH] Add a guide on data maps (#46) --- docs/datamaps/_category_.json | 3 + docs/datamaps/index.md | 192 ++++++++++++++++++++++++++++++++++ docs/datamaps/neo_maps.md | 25 +++++ docs/datamaps/structure.md | 103 ++++++++++++++++++ docusaurus.config.js | 2 +- 5 files changed, 324 insertions(+), 1 deletion(-) create mode 100644 docs/datamaps/_category_.json create mode 100644 docs/datamaps/index.md create mode 100644 docs/datamaps/neo_maps.md create mode 100644 docs/datamaps/structure.md diff --git a/docs/datamaps/_category_.json b/docs/datamaps/_category_.json new file mode 100644 index 000000000..43a13824e --- /dev/null +++ b/docs/datamaps/_category_.json @@ -0,0 +1,3 @@ +{ + "label": "Data Maps" +} \ No newline at end of file diff --git a/docs/datamaps/index.md b/docs/datamaps/index.md new file mode 100644 index 000000000..3886d4ff6 --- /dev/null +++ b/docs/datamaps/index.md @@ -0,0 +1,192 @@ +# Data Maps + +A registry data map contains data-driven, reloadable objects that can be attached to a registry object. +This system allows more easily data-driving game behaviour, as they provide functionality such as syncing or conflict resolution, leading to a better and more configurable user experience. + +You can think of tags as registry object ➜ boolean maps, while data maps are more flexible registry object ➜ object maps. + +A data map can be attached to both static, built-in, registries and dynamic data-driven datapack registries. + +Data maps support reloading through the use of the `/reload` command or any other means that reload server resources. + +## Registration +A data map type should be statically created and then registered to the `RegisterDataMapTypesEvent` (which is fired on the [mod event bus](../concepts/events)). The `DataMapType` can be created using a `DataMapType$Builder`, through `DataMapType#builder`. + +The builder provides a `synced` method which can be used to mark a data map as synced and have it sent to clients. + +A simple `DataMapType` has two generic arguments: `R` (the type of the registry the data map is for) and `T` (the values that are being attached). A data map of `SomeObject`s that are attached to `Item`s can, as such, be represented as `DataMapType`. + +Data maps are serialized and deserialized using [Codecs](../datastorage/codecs.md). + +Let's take the following record representing the data map value as an example: +```java +public record DropHealing( + float amount, float chance +) { + public static final Codec CODEC = RecordCodecBuilder.create(in -> in.group( + Codec.FLOAT.fieldOf("amount").forGetter(DropHealing::amount), + Codec.floatRange(0, 1).fieldOf("chance").forGetter(DropHealing::chance) + ).apply(in, DropHealing::new)); +} +``` + +:::warning +The value (`T`) should be an *immutable* object, as otherwise weird behaviour can be caused if the object is attached to all entries within a tag (since no copy is created). +::: + +For the purposes of this example, we will use this data map to heal players when they drop an item. +The `DataMapType` can be created as such: +```java +public static final DataMapType DROP_HEALING = DataMapType.builder( + new ResourceLocation("mymod:drop_healing"), Registries.ITEM, DropHealing.CODEC +).build(); +``` +and then registered to the `RegisterDataMapTypesEvent` using `RegisterDataMapTypesEvent#register`. + +## Syncing +A synced data map will have its values synced to clients. A data map can be marked as synced using `DataMapType$Builder#synced(Codec networkCodec, boolean mandatory)`. +The values of the data map will then be synced using the `networkCodec`. +If the `mandatory` flag is set to `true`, clients that do not support the data map (including Vanilla clients) will not be able to connect to the server, nor vice-versa. A non-mandatory data map on the other hand is optional, so it will not prevent any clients from joining. + +:::tip +A separate network codec allows for packet sizes to be smaller, as you can choose what data to send, and in what format. Otherwise the default codec can be used. +::: + +## JSON Structure and location +Data maps are loaded from a JSON file located at `mapNamespace/data_maps/registryNamespace/registryPath/mapPath.json`, where: +- `mapNamespace` is the namespace of the ID of the data map +- `mapPath` is the path of the ID of the data map +- `registryNamespace` is the namespace of the ID of the registry; if the namespace is `minecraft`, this value will be omitted +- `registryPath` is the path of the ID of the registry + +For more information, please [check out the dedicated page](./structure.md). + +## Usage +As data maps can be used on any registry, they can be queried through `Holder`s, and not through the actual registry objects. +You can query a data map value using `Holder#getData(DataMapType)`. If that object doesn't have a value attached, the method will return `null`. + +:::note +Only reference holders will return a value in that method. `Direct` holders will **not**. Generally, you will only encounter reference holders (which are returned by methods such as `Registry#wrapAsHolder`, `Registry#getHolder` or the different `builtInRegistryHolder` methods). +::: + +To continue the example above, we can implement our intended behaviour as follows: +```java +public static void onItemDrop(final ItemTossEvent event) { + final ItemStack stack = event.getEntity().getItem(); + // ItemStack has a getItemHolder method that will return a Holder which points to the item the stack is of + //highlight-next-line + final DropHealing value = stack.getItemHolder().getData(DROP_HEALING); + // Since getData returns null if the item will not have a drop healing value attached, we guard against it being null + if (value != null) { + // And here we simply use the values + if (event.getPlayer().level().getRandom().nextFloat() > value.chance()) { + event.getPlayer().heal(value.amount()); + } + } +} +``` + +## Advanced data maps +Advanced data maps are data maps which have additional functionality. Namely, the ability of merging values and selectively removing them, through a remover. Implementing some form of merging and removers is highly recommended for data maps whose values are collection-likes (like `Map`s or `List`s). + +`AdvancedDataMapType` have one more generic besides `T` and `R`: `VR extends DataMapValueRemover`. This additional generic allows you to datagen remove objects with increased type safety. + +### Creation +You create an `AdvancedDataMapType` using `AdvancedDataMapType#builder`. Unlike the normal builder, the builder returned by that method will have two more methods (`merger` and `remover`), and it will return an `AdvancedDataMapType`. Registration methods remain the same. + +### Mergers +An advanced data map can provide a `DataMapValueMerger` through `AdvancedDataMapType#merger`. This merger will be used to handle conflicts between data packs that attempt to attach a value to the same object. +The merger will be given the two conflicting values, and their sources (as an `Either, ResourceKey>` since values can be attached to all entries within a tag, not just individual entries), and is expected to return the value that will actually be attached. +Generally, mergers should simply merge the values, and should not perform "hard" overwrites unless necessary (i.e. if merging isn't possible). If a pack wants to bypass the merger, it can do so by specifying the object-level `replace` field. + +Let's imagine a scenario where we have a data map that attaches integers to items: +```java +public class IntMerger implements DataMapValueMerger { + @Override + public Integer merge(Registry registry, Either, ResourceKey> first, Integer firstValue, Either, ResourceKey> second, Integer secondValue) { + //highlight-next-line + return firstValue + secondValue; + } +} +``` +The above merger will merge the values if two datapacks attach to the same object. So if the first pack attaches the value `12` to `minecraft:carrot`, and the second pack attaches the value `15` to `minecraft:carrot`, the final value will be `27`. However, if the second pack specifies the object-level `replace` field, the final value will be `15` as the merger won't be invoked. + +NeoForge provides some default mergers for merging lists, sets and maps in `DataMapValueMerger`. + +The default merger (`DataMapValueMerger#defaultMerger`) has the typical behaviour you'd expect from normal data packs, where the newest value (which comes from the highest datapack) overwrites the previous value. + +### Removers +An advanced data map can provide a `DataMapValueRemover` through `AdvancedDataMapType#remover`. The remover will allow selective removals of data map values, effectively decomposition. +While by default a datapack can only remove the whole object attached to a registry entry, with a remover it can remove just speciffic values from the attached object (i.e. just the entry with a given key in the case of a map, or the entry with a specific property in the case of a list). + +The codec that is passed to the builder will decode remover instances. These removers will then be given the value currently attached and its source, and are expected to create a new object to replace the old value. +Alternatively, an empty `Optional` will lead to the value being completely removed. + +An example of a remover that will remove a value with a specific key from a `Map`-based data map: +```java +public record MapRemover(String key) implements DataMapValueRemover> { + public static final Codec CODEC = Codec.STRING.xmap(MapRemover::new, MapRemover::key); + + @Override + public Optional> remove(Map value, Registry registry, Either, ResourceKey> source, Item object) { + final Map newMap = new HashMap<>(value); + newMap.remove(key); + return Optional.of(newMap); + } +} +``` + +With the remover above in mind, we're attaching maps of string to string to items. Take the following data map JSON file: +```js +{ + "values": { + //highlight-start + "minecraft:carrot": { + "somekey1": "value1", + "somekey2": "value2" + } + //highlight-end + } +} +``` +That file will attach the map `[somekey1=value1, somekey2=value2]` to the `minecraft:carrot` item. Now, another datapack can come on top of it and remove just the value with the `somekey1` key, as such: +```js +{ + "remove": { + // As the remover is decoded as a string, we can use a string as the value here. If it were decoded as an object, we would have needed to use an object. + //highlight-next-line + "minecraft:carrot": "somekey1" + } +} +``` +After the second datapack is read and applied, the new value attached to the `minecraft:carrot` item will be `[somekey2=value2]`. + +## Datagen +Data maps can be [generated](../datagen) through `DataMapProvider`. +You should extend that class, and then override the `generate` method to create your entries, similar to tag generation. + +Considering the drop healing example from the start, we could generate some values as follows: +```java +public class DropHealingGen extends DataMapProvider { + + public DropHealingGen(PackOutput packOutput, CompletableFuture lookupProvider) { + super(packOutput, lookupProvider); + } + + @Override + protected void gather() { + // In the examples below, we do not need to forcibly replace any value as that's the default behaviour since a merger isn't provided, so the third parameter can be false. + + // If you were to provide a merger for your data map, then the third parameter will cause the old value to be overwritten if set to true, without invoking the merger + builder(DROP_HEALING) + // Always give entities that drop any item in the minecraft:fox_food tag 12 hearts + .add(ItemTags.FOX_FOOD, new DropHealing(12, 1f), false) + // Have a 10% chance of healing entities that drop an acacia boat by one point + .add(Items.ACACIA_BOAT.builtInRegistryHolder(), new DropHealing(1, 0.1f), false); + } +} +``` + +:::tip +There are `add` overloads that accept raw `ResourceLocation`s if you want to attach values to objects added by optional dependencies. In that case you should also provide [a loading condition](../resources/server/conditional) through the var-args parameter to avoid crashes. +::: \ No newline at end of file diff --git a/docs/datamaps/neo_maps.md b/docs/datamaps/neo_maps.md new file mode 100644 index 000000000..6ce2d6a9d --- /dev/null +++ b/docs/datamaps/neo_maps.md @@ -0,0 +1,25 @@ +# Built-in Data Maps +NeoForge provides a few data maps that mostly replace hardcoded in-code vanilla maps. +These data maps can be found in `NeoForgeDataMaps`, and are always *optional* to ensure compatibility with vanilla clients. + +## `neoforge:compostables` +NeoForge provides a data map that allows configuring composter values, as a replacement for `ComposterBlock#COMPOSTABLES` (which is now ignored). +This data map is located at `neoforged/data_maps/item/compostables.json` and its objects have the following structure: +```js +{ + // A 0 to 1 (inclusive) float representing the chance that the item will update the level of the composter + "chance": 1 +} +``` + +Example: +```js +{ + "values": { + // Give acacia logs a 50% chance that they will fill a composter + "minecraft:acacia_log": { + "chance": 0.5 + } + } +} +``` \ No newline at end of file diff --git a/docs/datamaps/structure.md b/docs/datamaps/structure.md new file mode 100644 index 000000000..431b6a4c0 --- /dev/null +++ b/docs/datamaps/structure.md @@ -0,0 +1,103 @@ +# JSON Structure +For the purposes of this page, we will use a data map which is an object with two float keys: `amount` and `chance` as an example. The codec for that object can be found [here](./index.md#registration). + +## Location +Data maps are loaded from a JSON file located at `mapNamespace/data_maps/registryNamespace/registryPath/mapPath.json`, where: +- `mapNamespace` is the namespace of the ID of the data map +- `mapPath` is the path of the ID of the data map +- `registryNamespace` is the namespace of the ID of the registry +- `registryPath` is the path of the ID of the registry + +:::note +The registry namespace is ommited if it is `minecraft`. +::: + +Examples: +- For a data map named `mymod:drop_healing` for the `minecraft:item` registry (as in the example), the path will be `mymod/data_maps/item/drop_healing.json`. +- For a data map named `somemod:somemap` for the `minecraft:block` registry, the path will be `somemod/data_maps/block/somemap.json`. +- For a data map named `example:stuff` for the `somemod:custom` registry, the path will be `example/data_maps/somemod/custom/stuff.json`. + +## Global `replace` field +The JSON file has an optional, global `replace` field, which is similar to tags, and when `true` will remove all previously attached values of that data map. This is useful for datapacks that want to completely change the entire data map. + +## Loading conditions +Data map files support [loading conditions](../resources/server/conditional) both at root-level and at entry-level through a `neoforge:conditions` array. + +## Adding values +Values can be attached to objects using the `values` map. Each key will represent either the ID of an individual registry entry to attach the value to, or a tag key, preceeded by `#`. If it is a tag, the same value will be attached to all entries in that tag. +The key will be the object to attach. + +```js +{ + "values": { + // Attach a value to the carrot item + "minecraft:carrot": { + "amount": 12, + "chance": 1 + }, + // Attach a value to all items in the logs tag + "#minecraft:logs": { + "amount": 1, + "chance": 0.1 + } + } +} +``` + +:::info +The above structure will invoke mergers in the case of [advanced data maps](./index.md#advanced-data-maps). If you do not want to invoke the merger for a specific object, then you will have to use a structure similar to this one: +```js +{ + "values": { + // Overwrite the value of the carrot item + "minecraft:carrot": { + // highlight-next-line + "replace": true, + // The new value will be under a value sub-object + "value": { + "amount": 12, + "chance": 1 + } + } + } +} +``` +::: + +## Removing values + +A JSON file can also remove values previously attached to objects, through the use of the `remove` array: +```js +{ + // Remove the value attached to apples and potatoes + "remove": ["minecraft:apple", "minecraft:potato"] +} +``` +The array contains a list of registry entry IDs or tags to remove the value from. + +:::warning +Removals happen after the values in the current JSON file have been attached, so you can use the removal feature to remove a value attached to an object through a tag: +```js +{ + "values": { + "#minecraft:logs": 12 + }, + // Remove the value from the acacia log, so that all logs but acacia have the value 12 attached to them + "remove": ["minecraft:acacia_log"] +} +``` +::: + +:::info +In the case of [advanced data maps](./index.md#advanced-data-maps) that provide a custom remover, the arguments of the remover can be provided by transforming the `remove` array into a map. +Let's assume that the remover object is serialized as a string and removes the value with a given key for a `Map`-based data map: +```js +{ + "remove": { + // The remover will be deserialized from the value (`somekey1` in this case) + // and applied to the value attached to the carrot item + "minecraft:carrot": "somekey1" + } +} +``` +::: \ No newline at end of file diff --git a/docusaurus.config.js b/docusaurus.config.js index 6ae368ad3..02bbdef53 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -170,7 +170,7 @@ const config = { prism: { theme: lightTheme, darkTheme: darkTheme, - additionalLanguages: ["java", "gradle", "toml", "groovy", "kotlin"], + additionalLanguages: ["java", "gradle", "toml", "groovy", "kotlin", "javascript", "json", "json5"], }, algolia: { // The application ID provided by Algolia