From 3fca0fbb1dca818c26f6020dad6b6e97a108859a Mon Sep 17 00:00:00 2001 From: Manchick0 Date: Sat, 17 Aug 2024 23:34:23 +0200 Subject: [PATCH 1/6] Added the guide on custom resources for runtime loading. Introduced a comprehensive guide to custom resources, covering client and server-side data. Implemented a `JsonDataLoader` example and updated sidebars, including their translations. --- .vitepress/sidebars/develop.ts | 4 + develop/custom-resources.md | 122 ++++++++++++++++++ .../docs/resources/FruitDataLoader.java | 91 +++++++++++++ sidebar_translations.json | 1 + 4 files changed, 218 insertions(+) create mode 100644 develop/custom-resources.md create mode 100644 reference/latest/src/main/java/com/example/docs/resources/FruitDataLoader.java diff --git a/.vitepress/sidebars/develop.ts b/.vitepress/sidebars/develop.ts index fe327e419..dc4a5809b 100644 --- a/.vitepress/sidebars/develop.ts +++ b/.vitepress/sidebars/develop.ts @@ -195,6 +195,10 @@ export default [ text: "develop.misc.codecs", link: "/develop/codecs", }, + { + text: "develop.misc.custom-resources", + link: "/develop/custom-resources", + }, { text: "develop.misc.events", link: "/develop/events" diff --git a/develop/custom-resources.md b/develop/custom-resources.md new file mode 100644 index 000000000..b8cc1420b --- /dev/null +++ b/develop/custom-resources.md @@ -0,0 +1,122 @@ +--- +title: Custom Resources +description: A comprehensive guide on how to use the resource system and load custom resources during the runtime. +authors: + - Manchick +--- + +# Custom Resources {#what-are-custom-resources} + +Some resources in Minecraft are loaded during the runtime, instead of being hard-coded into the game. +Each resource type falls into one of two categories: **Client Resources** and **Server Data**. +Client Resources are the ones that are loaded from a **resource pack**, thus being fully client-side, and accessible at any time. +Server Data, on the other hand, is used for various tasks on the server, and is loaded from a **data pack**. + +## Deciding on your Resource Type {#deciding-on-a-resource-type} + +Which type you'll use is completely up to you and your needs. Most of the time, the type depends on +the environment you're working with. For example, for recipes for a custom block, you'd want to use the +server data, while client resources might be handy if you want to allow the creation of new themes for your screen. + +# Creating a JsonDataLoader + +> This article assumes you have a class for the object you want to read with a proper deserialization method. +> For example, `MyClass#deserialize(JsonElement element)`. If you're unsure of how to create such methods, consider +> taking a closer look at what a [Codec](../develop/codecs.md) is. + +The JsonDataLoader will function as the main key point in our system. It provides the `apply(Map prepared, ResourceManager manager, Profiler profiler)` +method, which we'll override to load the read files into our storage. + +Let's start by creating a class itself. It should extend `JsonDataLoader`, and override the constructor +and the `apply` method. + +```java +public class ExampleDataLoader extends JsonDataLoader { + + public ExampleDataLoader(Gson gson, String dataType) { + super(gson, dataType); + } + + @Override + protected void apply(Map prepared, ResourceManager manager, Profiler profiler) { + + } +} +``` + +Let's add some static fields, which we'll then use later on. We're also going to use +a `HashMap` to store our data, but you can always replace it with a more advanced registry. + +@[code transcludeWith=:::1](@/reference/latest/src/main/java/com/example/docs/resources/FruitDataLoader.java) + +Our current constructor takes in a Gson object and a string. We've already created a custom +Gson object as a static field, so we can simply replace it. The string, on the other hand, is a bit +trickier. It represents the **folder** within the pack. In our example, setting it to `"fruit"` +will load the files that are located within the `fruit` folder of the pack: + +``` +root +└─ example + └─ fruit + ├─ orange.json + ├─ apple.json + └─ banana.json +``` + +With that being out of place, or constructor should now look like this: + +@[code transcludeWith=:::2](@/reference/latest/src/main/java/com/example/docs/resources/FruitDataLoader.java) + +## Deserializing Fields + +Now that our class looks clean, we can take a proper look at the `apply` method. This is the place where +we're going to deserialize `JsonElements` into custom objects, and store them afterward. + +@[code transcludeWith=:::3](@/reference/latest/src/main/java/com/example/docs/resources/FruitDataLoader.java) + +We can use an iterator to keep track on how many items we've loaded so far, to then log it to the console. +This is genuinely useful for debugging purposes. + +We should clear all existing entries before adding new ones, and this is VERY IMPORTANT. This +ensures that the deletion of a file **actually deletes** it after reloading. + +The `Map.Entry<>` is used here to bundle an identifier with a JsonElement. While it's clear what the JsonElement +represents - the JsonElement read from the file, what is the identifier for? + +It's actually quite simple, the identifier corresponds to the location of the file within +your folder, specified by that very string in the constructor. In our example with fruits, the identifiers +would be: + +``` +example:orange +example:apple +example:banana +``` + +We then iterate over the entries of the prepared map, deserialize them, and load them into +our very own registry, a simple `HashMap` in our case. + +# Registering the Custom Resource + +With that our `JsonDataLoader` is done, all that is left is to register it within our `ModInitializer`. +Since the registration method is quite big, you might want to create a separate static method. Let's take +a look: + +@[code transcludeWith=:::4](@/reference/latest/src/main/java/com/example/docs/resources/FruitDataLoader.java) + +Right away, you can see that we use the `ResourceType` enum to specify the resource type. We've discussed +what they are at the beginning of the article. We'll use `SERVER_DATA`, but you can go for `CLIENT_RESOURCES` +if you need. + +`getFabricId()` returns an identifier that is then used by Fabric to further register our `JsonDataLoader`. This +is usually the same as the `dataType` sting parsed to the constructor, but with a proper namespace. + +We create a new instance of our `JsonDataLoader` class in the `reload()` method, and then call the `reload()` +method again, but this time on the instance we've created. This ensures that our resources get reloaded, when needed +(Either by pressing CTRL + T or typing /reload, depending on your `ResourceType`). + +--- + +An example `Fruit` class used in this article. Feel free to copy it and adjust for your needs. + +@[code transcludeWith=:::5](@/reference/latest/src/main/java/com/example/docs/resources/FruitDataLoader.java) \ No newline at end of file diff --git a/reference/latest/src/main/java/com/example/docs/resources/FruitDataLoader.java b/reference/latest/src/main/java/com/example/docs/resources/FruitDataLoader.java new file mode 100644 index 000000000..e7a094fc6 --- /dev/null +++ b/reference/latest/src/main/java/com/example/docs/resources/FruitDataLoader.java @@ -0,0 +1,91 @@ +package com.example.docs.resources; + +import com.example.docs.FabricDocsReference; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; + +import com.mojang.serialization.Codec; + +import com.mojang.serialization.DataResult; +import com.mojang.serialization.JsonOps; + +import net.fabricmc.fabric.api.resource.IdentifiableResourceReloadListener; +import net.fabricmc.fabric.api.resource.ResourceManagerHelper; + +import net.minecraft.resource.JsonDataLoader; +import net.minecraft.resource.ResourceManager; +import net.minecraft.resource.ResourceType; +import net.minecraft.util.Identifier; +import net.minecraft.util.profiler.Profiler; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +public class FruitDataLoader extends JsonDataLoader { + + //:::1 + // A Logger we'll use for debugging reasons. + public static final Logger LOGGER = LoggerFactory.getLogger(FruitDataLoader.class); + // A HashMap we'll use to store our data, feel free to use something more advanced. + public static final HashMap DATA = new HashMap<>(); + // A Gson used in our JsonDataLoader, explicitly set to print multiline. + public static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + //:::2 + public FruitDataLoader() { + super(GSON, "fruit"); + } + + //:::3 + @Override + protected void apply(Map prepared, ResourceManager manager, Profiler profiler) { + int it = 0; + DATA.clear(); + for(Map.Entry entry : prepared.entrySet()) { + Identifier identifier = entry.getKey(); + JsonElement json = entry.getValue(); + Optional optional = Fruit.deserialize(json); + if(optional.isPresent()){ + DATA.put(identifier, optional.get()); + it++; + } + } + LOGGER.info("Loaded {} items.", it); + } + + //:::4 + public static void register(){ + ResourceManagerHelper.get(ResourceType.SERVER_DATA).registerReloadListener(new IdentifiableResourceReloadListener() { + + @Override + public Identifier getFabricId() { + return Identifier.of(FabricDocsReference.MOD_ID, "fruit"); + } + + @Override + public CompletableFuture reload(Synchronizer synchronizer, ResourceManager manager, Profiler prepareProfiler, Profiler applyProfiler, Executor prepareExecutor, Executor applyExecutor) { + return new FruitDataLoader().reload(synchronizer, manager, prepareProfiler, applyProfiler, prepareExecutor, applyExecutor); + } + }); + } + + //:::5 + public record Fruit(String name) { + + public static final Logger LOGGER = LoggerFactory.getLogger(Fruit.class); + public static final Codec CODEC = Codec.STRING.fieldOf("name").xmap(Fruit::new, Fruit::name).codec(); + + public static Optional deserialize(JsonElement json) { + DataResult result = CODEC.parse(JsonOps.INSTANCE, json); + return result.resultOrPartial(LOGGER::error); + } + } +} \ No newline at end of file diff --git a/sidebar_translations.json b/sidebar_translations.json index 180cd7850..778cf7211 100644 --- a/sidebar_translations.json +++ b/sidebar_translations.json @@ -50,6 +50,7 @@ "develop.rendering.particles.creatingParticles": "Creating Custom Particles", "develop.misc": "Miscellaneous Pages", "develop.misc.codecs": "Codecs", + "develop.misc.custom-resources": "Custom Resources", "develop.misc.events": "Events", "develop.misc.text-and-translations": "Text and Translations", "develop.misc.ideTipsAndTricks": "IDE Tips and Tricks", From fde44d9c03e3aee9fb2c62103b3279787d533d26 Mon Sep 17 00:00:00 2001 From: Manchick0 Date: Sat, 17 Aug 2024 23:38:38 +0200 Subject: [PATCH 2/6] Updated the class to have proper entrypoints --- .../java/com/example/docs/resources/FruitDataLoader.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/reference/latest/src/main/java/com/example/docs/resources/FruitDataLoader.java b/reference/latest/src/main/java/com/example/docs/resources/FruitDataLoader.java index e7a094fc6..00656b490 100644 --- a/reference/latest/src/main/java/com/example/docs/resources/FruitDataLoader.java +++ b/reference/latest/src/main/java/com/example/docs/resources/FruitDataLoader.java @@ -38,11 +38,13 @@ public class FruitDataLoader extends JsonDataLoader { public static final HashMap DATA = new HashMap<>(); // A Gson used in our JsonDataLoader, explicitly set to print multiline. public static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + //:::1 //:::2 public FruitDataLoader() { super(GSON, "fruit"); } + //:::2 //:::3 @Override @@ -60,6 +62,7 @@ protected void apply(Map prepared, ResourceManager mana } LOGGER.info("Loaded {} items.", it); } + //:::3 //:::4 public static void register(){ @@ -76,6 +79,7 @@ public CompletableFuture reload(Synchronizer synchronizer, ResourceManager } }); } + //:::4 //:::5 public record Fruit(String name) { @@ -88,4 +92,5 @@ public static Optional deserialize(JsonElement json) { return result.resultOrPartial(LOGGER::error); } } + //:::5 } \ No newline at end of file From 3aac1f079793f70d6e6f4d68f68b7ed9f0204139 Mon Sep 17 00:00:00 2001 From: Manchick0 Date: Sat, 17 Aug 2024 23:53:09 +0200 Subject: [PATCH 3/6] Fixed tiny mistakes. --- develop/custom-resources.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/develop/custom-resources.md b/develop/custom-resources.md index b8cc1420b..11d26f03e 100644 --- a/develop/custom-resources.md +++ b/develop/custom-resources.md @@ -24,16 +24,20 @@ server data, while client resources might be handy if you want to allow the crea > For example, `MyClass#deserialize(JsonElement element)`. If you're unsure of how to create such methods, consider > taking a closer look at what a [Codec](../develop/codecs.md) is. -The JsonDataLoader will function as the main key point in our system. It provides the `apply(Map prepared, ResourceManager manager, Profiler profiler)` +The `JsonDataLoader` will function as the main key point in our system. It provides the `apply(Map prepared, ResourceManager manager, Profiler profiler)` method, which we'll override to load the read files into our storage. +> We'll create a `JsonDataLoader` for loading JSON files from the `fruit` folder +> in a data pack, deserialize them into actual fruits, and store them +> for further retrieval. + Let's start by creating a class itself. It should extend `JsonDataLoader`, and override the constructor and the `apply` method. ```java -public class ExampleDataLoader extends JsonDataLoader { +public class FruitDataLoader extends JsonDataLoader { - public ExampleDataLoader(Gson gson, String dataType) { + public FruitDataLoader(Gson gson, String dataType) { super(gson, dataType); } @@ -109,7 +113,7 @@ what they are at the beginning of the article. We'll use `SERVER_DATA`, but you if you need. `getFabricId()` returns an identifier that is then used by Fabric to further register our `JsonDataLoader`. This -is usually the same as the `dataType` sting parsed to the constructor, but with a proper namespace. +is usually the same as the `dataType` string parsed to the constructor, but with a proper namespace. We create a new instance of our `JsonDataLoader` class in the `reload()` method, and then call the `reload()` method again, but this time on the instance we've created. This ensures that our resources get reloaded, when needed From 4670b18db537662b3e7126702908e8373cb995fd Mon Sep 17 00:00:00 2001 From: Manchick0 Date: Sun, 18 Aug 2024 08:07:21 +0200 Subject: [PATCH 4/6] Added EmptyDataLoader class for documentation. Introduced a new EmptyDataLoader class extending JsonDataLoader, intended solely for guidance in the 'custom resources' documentation. --- develop/custom-resources.md | 15 ++--------- .../docs/resources/EmptyDataLoader.java | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+), 13 deletions(-) create mode 100644 reference/latest/src/main/java/com/example/docs/resources/EmptyDataLoader.java diff --git a/develop/custom-resources.md b/develop/custom-resources.md index 11d26f03e..da172a03b 100644 --- a/develop/custom-resources.md +++ b/develop/custom-resources.md @@ -34,19 +34,8 @@ method, which we'll override to load the read files into our storage. Let's start by creating a class itself. It should extend `JsonDataLoader`, and override the constructor and the `apply` method. -```java -public class FruitDataLoader extends JsonDataLoader { - - public FruitDataLoader(Gson gson, String dataType) { - super(gson, dataType); - } - - @Override - protected void apply(Map prepared, ResourceManager manager, Profiler profiler) { - - } -} -``` + +@[code transcludeWith=:::1](@/reference/latest/src/main/java/com/example/docs/resources/EmptyDataLoader.java) Let's add some static fields, which we'll then use later on. We're also going to use a `HashMap` to store our data, but you can always replace it with a more advanced registry. diff --git a/reference/latest/src/main/java/com/example/docs/resources/EmptyDataLoader.java b/reference/latest/src/main/java/com/example/docs/resources/EmptyDataLoader.java new file mode 100644 index 000000000..0900ea362 --- /dev/null +++ b/reference/latest/src/main/java/com/example/docs/resources/EmptyDataLoader.java @@ -0,0 +1,26 @@ +package com.example.docs.resources; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; + +import net.minecraft.resource.JsonDataLoader; +import net.minecraft.resource.ResourceManager; +import net.minecraft.util.Identifier; +import net.minecraft.util.profiler.Profiler; + +import java.util.Map; + +// An empty class, used exclusively for guidance. +// :::1 +public class EmptyDataLoader extends JsonDataLoader { + + public EmptyDataLoader(Gson gson, String dataType) { + super(gson, dataType); + } + + @Override + protected void apply(Map prepared, ResourceManager manager, Profiler profiler) { + + } +} +//:::1 \ No newline at end of file From 10133af1bfbb5f29903c10787b4956cd57bcb8e9 Mon Sep 17 00:00:00 2001 From: Manchick0 Date: Sun, 18 Aug 2024 21:29:14 +0200 Subject: [PATCH 5/6] Fixed small syntax errors. --- develop/custom-resources.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/develop/custom-resources.md b/develop/custom-resources.md index da172a03b..578348df0 100644 --- a/develop/custom-resources.md +++ b/develop/custom-resources.md @@ -12,17 +12,17 @@ Each resource type falls into one of two categories: **Client Resources** and ** Client Resources are the ones that are loaded from a **resource pack**, thus being fully client-side, and accessible at any time. Server Data, on the other hand, is used for various tasks on the server, and is loaded from a **data pack**. -## Deciding on your Resource Type {#deciding-on-a-resource-type} +## Deciding on Your Resource Type {#deciding-on-a-resource-type} Which type you'll use is completely up to you and your needs. Most of the time, the type depends on the environment you're working with. For example, for recipes for a custom block, you'd want to use the server data, while client resources might be handy if you want to allow the creation of new themes for your screen. -# Creating a JsonDataLoader +## Creating a JsonDataLoader {#creating-a-json-data-loader} > This article assumes you have a class for the object you want to read with a proper deserialization method. > For example, `MyClass#deserialize(JsonElement element)`. If you're unsure of how to create such methods, consider -> taking a closer look at what a [Codec](../develop/codecs.md) is. +> taking a closer look at what a [Codec](../develop/codecs) is. The `JsonDataLoader` will function as the main key point in our system. It provides the `apply(Map prepared, ResourceManager manager, Profiler profiler)` method, which we'll override to load the read files into our storage. @@ -60,7 +60,7 @@ With that being out of place, or constructor should now look like this: @[code transcludeWith=:::2](@/reference/latest/src/main/java/com/example/docs/resources/FruitDataLoader.java) -## Deserializing Fields +## Deserializing Fields {#deserializing-fields} Now that our class looks clean, we can take a proper look at the `apply` method. This is the place where we're going to deserialize `JsonElements` into custom objects, and store them afterward. @@ -89,7 +89,7 @@ example:banana We then iterate over the entries of the prepared map, deserialize them, and load them into our very own registry, a simple `HashMap` in our case. -# Registering the Custom Resource +## Registering the Custom Resource {#registering-the-custom-resource} With that our `JsonDataLoader` is done, all that is left is to register it within our `ModInitializer`. Since the registration method is quite big, you might want to create a separate static method. Let's take @@ -112,4 +112,4 @@ method again, but this time on the instance we've created. This ensures that our An example `Fruit` class used in this article. Feel free to copy it and adjust for your needs. -@[code transcludeWith=:::5](@/reference/latest/src/main/java/com/example/docs/resources/FruitDataLoader.java) \ No newline at end of file +@[code transcludeWith=:::5](@/reference/latest/src/main/java/com/example/docs/resources/FruitDataLoader.java) From 8fec670a9664d0c17f23673f2320adb408d8fb06 Mon Sep 17 00:00:00 2001 From: Manchick0 Date: Tue, 20 Aug 2024 00:32:22 +0200 Subject: [PATCH 6/6] Refactor data loader classes and update docs Deleted EmptyDataLoader.java and introduced new Book and Fruit data loader classes for improved data handling. Updated documentation to reflect the changes, including new examples and clarifications on resource types and Codec usage. --- develop/custom-resources.md | 118 ++++++++++-------- .../java/com/example/docs/resources/Book.java | 28 +++++ .../docs/resources/BookDataLoader.java | 75 +++++++++++ .../docs/resources/EmptyDataLoader.java | 26 ---- .../com/example/docs/resources/Fruit.java | 23 ++++ .../docs/resources/FruitDataLoader.java | 20 +-- 6 files changed, 197 insertions(+), 93 deletions(-) create mode 100644 reference/latest/src/main/java/com/example/docs/resources/Book.java create mode 100644 reference/latest/src/main/java/com/example/docs/resources/BookDataLoader.java delete mode 100644 reference/latest/src/main/java/com/example/docs/resources/EmptyDataLoader.java create mode 100644 reference/latest/src/main/java/com/example/docs/resources/Fruit.java diff --git a/develop/custom-resources.md b/develop/custom-resources.md index 578348df0..d849ff80e 100644 --- a/develop/custom-resources.md +++ b/develop/custom-resources.md @@ -12,40 +12,32 @@ Each resource type falls into one of two categories: **Client Resources** and ** Client Resources are the ones that are loaded from a **resource pack**, thus being fully client-side, and accessible at any time. Server Data, on the other hand, is used for various tasks on the server, and is loaded from a **data pack**. -## Deciding on Your Resource Type {#deciding-on-a-resource-type} - -Which type you'll use is completely up to you and your needs. Most of the time, the type depends on -the environment you're working with. For example, for recipes for a custom block, you'd want to use the -server data, while client resources might be handy if you want to allow the creation of new themes for your screen. +::: info +In this article, we'll create two separate `JsonDataLoaders`. A server-side one, +to load `Fruit` objects from a `data pack`, and a client-side one - to load `Book`s +from a resource pack. We'll also briefly discuss how `Codec`s could help us on +our journey! In case you're unfamiliar with them, consider taking a closer look +at what [Codecs](../develop/codecs) are. +::: ## Creating a JsonDataLoader {#creating-a-json-data-loader} -> This article assumes you have a class for the object you want to read with a proper deserialization method. -> For example, `MyClass#deserialize(JsonElement element)`. If you're unsure of how to create such methods, consider -> taking a closer look at what a [Codec](../develop/codecs) is. - The `JsonDataLoader` will function as the main key point in our system. It provides the `apply(Map prepared, ResourceManager manager, Profiler profiler)` -method, which we'll override to load the read files into our storage. - -> We'll create a `JsonDataLoader` for loading JSON files from the `fruit` folder -> in a data pack, deserialize them into actual fruits, and store them -> for further retrieval. - -Let's start by creating a class itself. It should extend `JsonDataLoader`, and override the constructor -and the `apply` method. - +method, which we'll override to read files and load them into our storage. -@[code transcludeWith=:::1](@/reference/latest/src/main/java/com/example/docs/resources/EmptyDataLoader.java) +Imagine you want to add a system, where all players can create their own `Fruit`s. Why not +make it dynamic and allow their creation with a data pack? We'll create a `FruitDataLoader` +that serves our needs. Make it extend `JsonDataLoader` and override all required methods. -Let's add some static fields, which we'll then use later on. We're also going to use +Let's introduce some static fields, which we'll then use later on? We're also going to use a `HashMap` to store our data, but you can always replace it with a more advanced registry. @[code transcludeWith=:::1](@/reference/latest/src/main/java/com/example/docs/resources/FruitDataLoader.java) -Our current constructor takes in a Gson object and a string. We've already created a custom -Gson object as a static field, so we can simply replace it. The string, on the other hand, is a bit +Our current constructor takes in a `Gson` object and a string. We've already created a custom +`Gson` object as a static field, so we can simply replace it. The string, on the other hand, is a bit trickier. It represents the **folder** within the pack. In our example, setting it to `"fruit"` -will load the files that are located within the `fruit` folder of the pack: +will load the files that are located within the `fruit` folder of the **data pack**: ``` root @@ -56,7 +48,7 @@ root └─ banana.json ``` -With that being out of place, or constructor should now look like this: +With that being out of place, our constructor should now look like this: @[code transcludeWith=:::2](@/reference/latest/src/main/java/com/example/docs/resources/FruitDataLoader.java) @@ -67,49 +59,75 @@ we're going to deserialize `JsonElements` into custom objects, and store them af @[code transcludeWith=:::3](@/reference/latest/src/main/java/com/example/docs/resources/FruitDataLoader.java) -We can use an iterator to keep track on how many items we've loaded so far, to then log it to the console. -This is genuinely useful for debugging purposes. - -We should clear all existing entries before adding new ones, and this is VERY IMPORTANT. This +::: warning +We should clear all existing entries before adding new ones, and this is **VERY IMPORTANT**. This ensures that the deletion of a file **actually deletes** it after reloading. +::: -The `Map.Entry<>` is used here to bundle an identifier with a JsonElement. While it's clear what the JsonElement -represents - the JsonElement read from the file, what is the identifier for? +`JsonDataLoader` allows us to easily read **JSON** files from the specified folder. The `apply` method +is the exact place where you tend to do it. Since we deal with **JSON** files here, it makes total sense +(and is even preferred) to use `Codec`s for deserialization purposes. Let's take a look at an example +of how such codec might look like: + +```java +Codec CODEC = Codec.STRING.fieldOf("name").xmap(Fruit::new, Fruit::name).codec(); +``` -It's actually quite simple, the identifier corresponds to the location of the file within -your folder, specified by that very string in the constructor. In our example with fruits, the identifiers -would be: +This right here is probably the simplest codec you can get. It directly maps `name` field to +a `Fruit` object, meaning our JSON files should look like this: +```json +{ + "name": "apple" +} ``` -example:orange -example:apple -example:banana + +The `prepared` map is an argument parsed to our `apply` method. It essentially maps `Identifiers` to +`JsonElements`. Identifiers specify paths within the folder, e.g `example:orange`, `example:apple`. +JsonElements are even more handy. Since we're using codecs, we can easily convert them into `Fruit` objects, +by calling the **deserialization** method. It may vary, but it always looks more-or-less like this: + +```java +public static Optional deserialize(JsonElement json) { + DataResult result = CODEC.parse(JsonOps.INSTANCE, json); + return result.resultOrPartial(LOGGER::error); +} ``` -We then iterate over the entries of the prepared map, deserialize them, and load them into -our very own registry, a simple `HashMap` in our case. +::: tip +We can use an iterator to keep track on how many items we've loaded so far, to then log it to the console. +This is genuinely useful for debugging purposes. +::: ## Registering the Custom Resource {#registering-the-custom-resource} -With that our `JsonDataLoader` is done, all that is left is to register it within our `ModInitializer`. -Since the registration method is quite big, you might want to create a separate static method. Let's take -a look: - @[code transcludeWith=:::4](@/reference/latest/src/main/java/com/example/docs/resources/FruitDataLoader.java) -Right away, you can see that we use the `ResourceType` enum to specify the resource type. We've discussed -what they are at the beginning of the article. We'll use `SERVER_DATA`, but you can go for `CLIENT_RESOURCES` -if you need. +Custom resources, just like a lot of other features, require a proper registration within your `ModInitializer`. +Now, this registration process is quite big, so you might want to put it in a static method. + +::: tip +Since our `FruitDataLoader` tends to load files from a **data pack**, we use the `SERVER_DATA` `ResourceType` here. +If your object should instead be loaded from a **resource pack**, considering changing this value to `CLIENT_RESOURCES` +::: `getFabricId()` returns an identifier that is then used by Fabric to further register our `JsonDataLoader`. This is usually the same as the `dataType` string parsed to the constructor, but with a proper namespace. We create a new instance of our `JsonDataLoader` class in the `reload()` method, and then call the `reload()` -method again, but this time on the instance we've created. This ensures that our resources get reloaded, when needed -(Either by pressing CTRL + T or typing /reload, depending on your `ResourceType`). +method again, but this time on the instance we've created. This ensures that our resources get reloaded, when needed. ---- +With that our `FruitDataLoader` is done, and we can focus on the `BookDataLoader` to load our books. +Luckily, the process is similar. In fact, it's pretty much the same! -An example `Fruit` class used in this article. Feel free to copy it and adjust for your needs. +::: details Take a look at the `BookDataLoader` class +@[code transcludeWith=:::1](@/reference/latest/src/main/java/com/example/docs/resources/BookDataLoader.java) +::: + +--- -@[code transcludeWith=:::5](@/reference/latest/src/main/java/com/example/docs/resources/FruitDataLoader.java) +Example `Fruit` and `Book` classes used in this article. Feel free to copy them and adjust for your needs. +::: code-group +@[code transcludeWith=:::1](@/reference/latest/src/main/java/com/example/docs/resources/Fruit.java) +@[code transcludeWith=:::1](@/reference/latest/src/main/java/com/example/docs/resources/Book.java) +::: diff --git a/reference/latest/src/main/java/com/example/docs/resources/Book.java b/reference/latest/src/main/java/com/example/docs/resources/Book.java new file mode 100644 index 000000000..df869db4a --- /dev/null +++ b/reference/latest/src/main/java/com/example/docs/resources/Book.java @@ -0,0 +1,28 @@ +package com.example.docs.resources; + +import com.google.gson.JsonElement; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.JsonOps; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Optional; + +//:::1 +public record Book(String name, String author) { + + public static final Logger LOGGER = LoggerFactory.getLogger(Book.class); + public static final Codec CODEC = RecordCodecBuilder.create(instance -> { + return instance.group(Codec.STRING.fieldOf("name").forGetter(Book::name), + Codec.STRING.fieldOf("author").forGetter(Book::author) + ).apply(instance, Book::new); + }); + + public static Optional deserialize(JsonElement json) { + DataResult result = CODEC.parse(JsonOps.INSTANCE, json); + return result.resultOrPartial(LOGGER::error); + } +} +//:::1 diff --git a/reference/latest/src/main/java/com/example/docs/resources/BookDataLoader.java b/reference/latest/src/main/java/com/example/docs/resources/BookDataLoader.java new file mode 100644 index 000000000..a4c00e2dd --- /dev/null +++ b/reference/latest/src/main/java/com/example/docs/resources/BookDataLoader.java @@ -0,0 +1,75 @@ +package com.example.docs.resources; + +import com.example.docs.FabricDocsReference; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.JsonOps; + +import com.mojang.serialization.codecs.RecordCodecBuilder; + +import net.fabricmc.fabric.api.resource.IdentifiableResourceReloadListener; +import net.fabricmc.fabric.api.resource.ResourceManagerHelper; + +import net.minecraft.resource.JsonDataLoader; +import net.minecraft.resource.ResourceManager; +import net.minecraft.resource.ResourceType; +import net.minecraft.util.Identifier; +import net.minecraft.util.profiler.Profiler; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +//:::1 +public class BookDataLoader extends JsonDataLoader { + + public static final Logger LOGGER = LoggerFactory.getLogger(BookDataLoader.class); + public static final HashMap DATA = new HashMap<>(); + public static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + public BookDataLoader() { + super(GSON, "fruit"); + } + + @Override + protected void apply(Map prepared, ResourceManager manager, Profiler profiler) { + int it = 0; + DATA.clear(); + for(Map.Entry entry : prepared.entrySet()) { + Identifier identifier = entry.getKey(); + JsonElement json = entry.getValue(); + Optional optional = Book.deserialize(json); + if(optional.isPresent()){ + DATA.put(identifier, optional.get()); + it++; + } + } + LOGGER.info("Loaded {} items.", it); + } + + public static void register(){ + ResourceManagerHelper.get(ResourceType.CLIENT_RESOURCES).registerReloadListener(new IdentifiableResourceReloadListener() { + + @Override + public Identifier getFabricId() { + return Identifier.of(FabricDocsReference.MOD_ID, "fruit"); + } + + @Override + public CompletableFuture reload(Synchronizer synchronizer, ResourceManager manager, Profiler prepareProfiler, Profiler applyProfiler, Executor prepareExecutor, Executor applyExecutor) { + return new BookDataLoader().reload(synchronizer, manager, prepareProfiler, applyProfiler, prepareExecutor, applyExecutor); + } + }); + } +} +//:::1 diff --git a/reference/latest/src/main/java/com/example/docs/resources/EmptyDataLoader.java b/reference/latest/src/main/java/com/example/docs/resources/EmptyDataLoader.java deleted file mode 100644 index 0900ea362..000000000 --- a/reference/latest/src/main/java/com/example/docs/resources/EmptyDataLoader.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.docs.resources; - -import com.google.gson.Gson; -import com.google.gson.JsonElement; - -import net.minecraft.resource.JsonDataLoader; -import net.minecraft.resource.ResourceManager; -import net.minecraft.util.Identifier; -import net.minecraft.util.profiler.Profiler; - -import java.util.Map; - -// An empty class, used exclusively for guidance. -// :::1 -public class EmptyDataLoader extends JsonDataLoader { - - public EmptyDataLoader(Gson gson, String dataType) { - super(gson, dataType); - } - - @Override - protected void apply(Map prepared, ResourceManager manager, Profiler profiler) { - - } -} -//:::1 \ No newline at end of file diff --git a/reference/latest/src/main/java/com/example/docs/resources/Fruit.java b/reference/latest/src/main/java/com/example/docs/resources/Fruit.java new file mode 100644 index 000000000..35d2364c4 --- /dev/null +++ b/reference/latest/src/main/java/com/example/docs/resources/Fruit.java @@ -0,0 +1,23 @@ +package com.example.docs.resources; + +import com.google.gson.JsonElement; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.JsonOps; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Optional; + +//:::1 +public record Fruit(String name) { + + public static final Logger LOGGER = LoggerFactory.getLogger(Fruit.class); + public static final Codec CODEC = Codec.STRING.fieldOf("name").xmap(Fruit::new, Fruit::name).codec(); + + public static Optional deserialize(JsonElement json) { + DataResult result = CODEC.parse(JsonOps.INSTANCE, json); + return result.resultOrPartial(LOGGER::error); + } +} +//:::1 diff --git a/reference/latest/src/main/java/com/example/docs/resources/FruitDataLoader.java b/reference/latest/src/main/java/com/example/docs/resources/FruitDataLoader.java index 00656b490..20ff90da8 100644 --- a/reference/latest/src/main/java/com/example/docs/resources/FruitDataLoader.java +++ b/reference/latest/src/main/java/com/example/docs/resources/FruitDataLoader.java @@ -7,7 +7,6 @@ import com.google.gson.JsonElement; import com.mojang.serialization.Codec; - import com.mojang.serialization.DataResult; import com.mojang.serialization.JsonOps; @@ -33,7 +32,7 @@ public class FruitDataLoader extends JsonDataLoader { //:::1 // A Logger we'll use for debugging reasons. - public static final Logger LOGGER = LoggerFactory.getLogger(FruitDataLoader.class); + public static final Logger LOGGER = LoggerFactory.getLogger(BookDataLoader.class); // A HashMap we'll use to store our data, feel free to use something more advanced. public static final HashMap DATA = new HashMap<>(); // A Gson used in our JsonDataLoader, explicitly set to print multiline. @@ -75,22 +74,9 @@ public Identifier getFabricId() { @Override public CompletableFuture reload(Synchronizer synchronizer, ResourceManager manager, Profiler prepareProfiler, Profiler applyProfiler, Executor prepareExecutor, Executor applyExecutor) { - return new FruitDataLoader().reload(synchronizer, manager, prepareProfiler, applyProfiler, prepareExecutor, applyExecutor); + return new BookDataLoader().reload(synchronizer, manager, prepareProfiler, applyProfiler, prepareExecutor, applyExecutor); } }); } //:::4 - - //:::5 - public record Fruit(String name) { - - public static final Logger LOGGER = LoggerFactory.getLogger(Fruit.class); - public static final Codec CODEC = Codec.STRING.fieldOf("name").xmap(Fruit::new, Fruit::name).codec(); - - public static Optional deserialize(JsonElement json) { - DataResult result = CODEC.parse(JsonOps.INSTANCE, json); - return result.resultOrPartial(LOGGER::error); - } - } - //:::5 -} \ No newline at end of file +}