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

Custom Resources Article #167

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .vitepress/sidebars/develop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
133 changes: 133 additions & 0 deletions develop/custom-resources.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
---
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**.

::: 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}

The `JsonDataLoader` will function as the main key point in our system. It provides the `apply(Map<Identifier, JsonElement> prepared, ResourceManager manager, Profiler profiler)`
method, which we'll override to read files and load them into our storage.

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 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
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 **data pack**:

```

Check failure on line 42 in develop/custom-resources.md

View workflow job for this annotation

GitHub Actions / markdownlint

Fenced code blocks should have a language specified

develop/custom-resources.md:42 MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md040.md
root
└─ example
└─ fruit
├─ orange.json
├─ apple.json
└─ banana.json
```

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)

## 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.

@[code transcludeWith=:::3](@/reference/latest/src/main/java/com/example/docs/resources/FruitDataLoader.java)

::: 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.
:::

`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<Fruit> CODEC = Codec.STRING.fieldOf("name").xmap(Fruit::new, Fruit::name).codec();
```

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"
}
```

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<Fruit> deserialize(JsonElement json) {
DataResult<Fruit> result = CODEC.parse(JsonOps.INSTANCE, json);

Check failure on line 92 in develop/custom-resources.md

View workflow job for this annotation

GitHub Actions / markdownlint

Hard tabs

develop/custom-resources.md:92:1 MD010/no-hard-tabs Hard tabs [Column: 1] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md010.md
return result.resultOrPartial(LOGGER::error);

Check failure on line 93 in develop/custom-resources.md

View workflow job for this annotation

GitHub Actions / markdownlint

Hard tabs

develop/custom-resources.md:93:1 MD010/no-hard-tabs Hard tabs [Column: 1] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md010.md
}
```

::: 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}

@[code transcludeWith=:::4](@/reference/latest/src/main/java/com/example/docs/resources/FruitDataLoader.java)

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.

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!

::: details Take a look at the `BookDataLoader` class
@[code transcludeWith=:::1](@/reference/latest/src/main/java/com/example/docs/resources/BookDataLoader.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)
:::
Original file line number Diff line number Diff line change
@@ -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<Book> 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<Book> deserialize(JsonElement json) {
DataResult<Book> result = CODEC.parse(JsonOps.INSTANCE, json);
return result.resultOrPartial(LOGGER::error);
}
}
//:::1
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.example.docs.resources;

import com.example.docs.FabricDocsReference;

import com.google.gson.Gson;

Check failure on line 5 in reference/latest/src/main/java/com/example/docs/resources/BookDataLoader.java

View workflow job for this annotation

GitHub Actions / mod

Wrong order for 'com.google.gson.Gson' import.
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;

import com.mojang.serialization.Codec;

Check failure on line 9 in reference/latest/src/main/java/com/example/docs/resources/BookDataLoader.java

View workflow job for this annotation

GitHub Actions / mod

Extra separation in import group before 'com.mojang.serialization.Codec'

Check failure on line 9 in reference/latest/src/main/java/com/example/docs/resources/BookDataLoader.java

View workflow job for this annotation

GitHub Actions / mod

Unused import - com.mojang.serialization.Codec.
import com.mojang.serialization.DataResult;

Check failure on line 10 in reference/latest/src/main/java/com/example/docs/resources/BookDataLoader.java

View workflow job for this annotation

GitHub Actions / mod

Unused import - com.mojang.serialization.DataResult.
import com.mojang.serialization.JsonOps;

Check failure on line 11 in reference/latest/src/main/java/com/example/docs/resources/BookDataLoader.java

View workflow job for this annotation

GitHub Actions / mod

Unused import - com.mojang.serialization.JsonOps.

import com.mojang.serialization.codecs.RecordCodecBuilder;

Check failure on line 13 in reference/latest/src/main/java/com/example/docs/resources/BookDataLoader.java

View workflow job for this annotation

GitHub Actions / mod

Extra separation in import group before 'com.mojang.serialization.codecs.RecordCodecBuilder'

Check failure on line 13 in reference/latest/src/main/java/com/example/docs/resources/BookDataLoader.java

View workflow job for this annotation

GitHub Actions / mod

Unused 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;

Check failure on line 18 in reference/latest/src/main/java/com/example/docs/resources/BookDataLoader.java

View workflow job for this annotation

GitHub Actions / mod

Wrong order for 'net.minecraft.resource.JsonDataLoader' import.
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;

Check failure on line 24 in reference/latest/src/main/java/com/example/docs/resources/BookDataLoader.java

View workflow job for this annotation

GitHub Actions / mod

Wrong order for 'org.slf4j.Logger' import.
import org.slf4j.LoggerFactory;

import java.util.HashMap;

Check failure on line 27 in reference/latest/src/main/java/com/example/docs/resources/BookDataLoader.java

View workflow job for this annotation

GitHub Actions / mod

Wrong order for 'java.util.HashMap' import.
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<Identifier, Book> DATA = new HashMap<>();
public static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();

public BookDataLoader() {
super(GSON, "fruit");
}

@Override
protected void apply(Map<Identifier, JsonElement> prepared, ResourceManager manager, Profiler profiler) {
int it = 0;
DATA.clear();
for(Map.Entry<Identifier, JsonElement> entry : prepared.entrySet()) {
Identifier identifier = entry.getKey();
JsonElement json = entry.getValue();
Optional<Book> 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<Void> reload(Synchronizer synchronizer, ResourceManager manager, Profiler prepareProfiler, Profiler applyProfiler, Executor prepareExecutor, Executor applyExecutor) {
return new BookDataLoader().reload(synchronizer, manager, prepareProfiler, applyProfiler, prepareExecutor, applyExecutor);
}
});
}
}
//:::1
Original file line number Diff line number Diff line change
@@ -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<Fruit> CODEC = Codec.STRING.fieldOf("name").xmap(Fruit::new, Fruit::name).codec();

public static Optional<Fruit> deserialize(JsonElement json) {
DataResult<Fruit> result = CODEC.parse(JsonOps.INSTANCE, json);
return result.resultOrPartial(LOGGER::error);
}
}
//:::1
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
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(BookDataLoader.class);
// A HashMap we'll use to store our data, feel free to use something more advanced.
public static final HashMap<Identifier, Fruit> 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
protected void apply(Map<Identifier, JsonElement> prepared, ResourceManager manager, Profiler profiler) {
int it = 0;
DATA.clear();
for(Map.Entry<Identifier, JsonElement> entry : prepared.entrySet()) {
Identifier identifier = entry.getKey();
JsonElement json = entry.getValue();
Optional<Fruit> optional = Fruit.deserialize(json);
if(optional.isPresent()){
DATA.put(identifier, optional.get());
it++;
}
}
LOGGER.info("Loaded {} items.", it);
}
//:::3

//:::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<Void> reload(Synchronizer synchronizer, ResourceManager manager, Profiler prepareProfiler, Profiler applyProfiler, Executor prepareExecutor, Executor applyExecutor) {
return new BookDataLoader().reload(synchronizer, manager, prepareProfiler, applyProfiler, prepareExecutor, applyExecutor);
}
});
}
//:::4
}
1 change: 1 addition & 0 deletions sidebar_translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading