Reload Listeners
In some situations, integrating with the existing resource systems provided by Minecraft or NeoForge just isn't going to cut it. Instead, having your system load files by itself from a resource or data pack is more desirable. For this purpose, you can register a custom reload listener, implementing PreparableReloadListener or one of its subinterfaces/subclasses.
The idea behind a reload listener is simple: When a resource pack or data pack reload happens, the listener is called upon to read its contents from the new set of resource or data packs. It will then keep the contents until the next reload, at which point the contents will be discarded and the cycle starts anew.
When creating a custom reload listener, you should ask yourself two different questions:
- What context/side does the reload listener run in? See Adding and Retrieving Reload Listeners for available options.
- What kind of data does the reload listener load (if any at all?) See Reload Listener Class Hierarchy for available options.
The former determines which event is used for registration, and where the reload listener instance will be located; while the latter determines which base class or interface is used, and which methods need to be overridden.
On the server side, the datapack registry or data map systems may be better suited for many use cases.
Reloading
Both resource pack and data pack reload function similar in principle and only differ in the associated physical and logical side, and by extension the timing and the location files are loaded from.
| Client Reload Listener | Server Reload Listener | |
|---|---|---|
| Loads From | Resource packs (assets folder) | Data packs (data folder) |
| First Reload (Physical Client) | - Startup | - Creating a new world ("Preparing for world creation...") - Joining an existing world - Joining a server |
| Subsequent Reloads (Physical Client) | - Changing resource packs in the Options menu - Downloading a server's custom resource pack on server join - Pressing F3+T | - /reload command |
| First Reload (Physical Server) | never | - Startup |
| Subsequent Reloads (Physical Server) | never | - /reload command |
On the physical client, when leaving a world, any stored data becomes stale. Depending on how your system is set up, this can cause memory leaks. See Server-Side Reload Listeners below for what mechanisms to use to avoid memory leaks.
The /reload command also triggers reloads of some other datapack-driven systems, such as tags. This is by design and cannot be circumvented.
Multi-Threaded Reloading
Conceptually, the work of a single reload listener can commonly be split into two stages:
- Preparation: The necessary files are collected, validated and parsed into some object that can be used in the next step. The preparation stage is run on multiple threads, using what is called the task executor.
- Application: The object from the previous step is "applied" to the game, usually by means of setting some field or adding to some collection. The application stage is run on the main thread, using what is called the reload executor.
To ensure synchronization, after preparation, a PreparationBarrier is waited for by the underlying CompletableFuture. Only once all preparation threads have run, the application stage is allowed to run. In code, this looks roughly as follows:
@Override
public CompletableFuture<Void> reload(
SharedState currentReload,
Executor taskExecutor,
PreparationBarrier barrier,
Executor reloadExecutor
) {
return CompletableFuture
.supplyAsync(() -> {
// Collect and return the result of the preparation stage here.
}, taskExecutor) // Use the task executor for this call.
.thenCompose(barrier::wait)
.thenAcceptAsync(preparations -> {
// Run the application stage.
// `preparations` is the return value of the `supplyAsync` call above.
}, reloadExecutor); // Use the reload executor for this call.
}
See PreparableReloadListener below for an explanation of the parameters.
Adding and Retrieving Reload Listeners
All reload listeners are registered using the same basic principle, though with some differences depending on the logical side. First, we need a reload listener class. Then, we register it to the side-specific event. Finally, we can retrieve the reload listener in a side-specific way and operate on it.
Client-Side Reload Listeners
On the client side, reload listeners are only collected once during startup. It is therefore sufficient to hold the reload listener in a singleton instance, like so:
// Instead of PreparableReloadListener, extend one of its subclasses if applicable, see below.
public class MyClientReloadListener implements PreparableReloadListener {
// The id we're going to use in registration below
public static final Identifier ID =
Identifier.fromNamespaceAndPath("mymod", "my_client_reload_listener");
// The instance through which the listener is accessed
public static final MyClientReloadListener INSTANCE = new MyClientReloadListener();
// Hide the constructor in accordance with the singleton pattern
private MyClientReloadListener() {
}
// other methods added here later
}
Next, we add the reload listener in the AddClientReloadListenersEvent like so:
@SubscribeEvent // on the game event bus only on the physical client
public static void addClientReloadListeners(AddClientReloadListenersEvent event) {
event.addListener(MyClientReloadListener.ID, MyClientReloadListener.INSTANCE);
}
And that's it! To access the reload listener, simply access the MyClientReloadListener.INSTANCE field.
Server-Side Reload Listeners
Server-side reload listeners can be implemented in two ways, depending on what they do:
- If they do not need to store any data and just run some code, they should be disposable, meaning no reference to them (that is visible from the outside) is held at all.
- If they need to load and store data by themselves, they should be retained.
Disposable Server-Side Reload Listeners
For a disposable reload listener, we use code that is very similar to the client-side reload listener code above, however we simply do not keep a reference around at all:
// Instead of PreparableReloadListener, extend one of its subclasses if applicable, see below.
public class MyDisposableReloadListener implements PreparableReloadListener {
// The id we're going to use in registration below
public static final Identifier ID =
Identifier.fromNamespaceAndPath("mymod", "my_disposable_reload_listener");
// No instance, since there's no data to access;
// also no hiding the constructor since we'll construct our class directly
// other methods added here later
}
And then, we add the reload listener in the AddServerReloadListenersEvent like so:
@SubscribeEvent // on the game event bus
public static void addServerReloadListeners(AddServerReloadListenersEvent event) {
event.addListener(MyDisposableReloadListener.ID, new MyDisposableReloadListener());
}
Retained Server-Side Reload Listeners
When storing data, it is theoretically possible to follow the same singleton pattern as above, and just register to AddServerReloadListenersEvent. However, since server-side reload listeners are collected at the beginning of every datapack reload, this poses a high risk of memory leaks at reloading time if not handled properly. Furthermore, it can also lead to unintended "reaching across sides" in the form of client code accessing the server's reload listeners.
To combat this, NeoForge introduces the concept of retained listeners. Retained listener are accessed from a MinecraftServer instance (which can be retrieved from a ServerLevel) rather than a singleton instance, using a ListenerKey<T>.
Not too much changes from the other two approaches, the main change is the addition of a LISTENER_KEY:
// Instead of PreparableReloadListener, extend one of its subclasses if applicable, see below.
public class MyRetainedReloadListener implements PreparableReloadListener {
// The id we're going to use in registration below
public static final Identifier ID =
Identifier.fromNamespaceAndPath("mymod", "my_retained_reload_listener");
// Create a listener key for use in the event
public static final ListenerKey<MyRetainedReloadListener> LISTENER_KEY = ListenerKey.create(ID);
// other methods added here later
}
Next, we register a retained listener to the AddServerReloadListenersEvent like so:
@SubscribeEvent // on the game event bus
public static void addServerReloadListeners(AddServerReloadListenersEvent event) {
event.addRetainedListener(MyRetainedReloadListener.LISTENER_KEY, new MyRetainedReloadListener());
}
And finally, we can access the listener from a ServerLevel like so:
MyServerReloadListener listener = serverLevel
.getServer()
.getServerResources()
.managers()
.getListener(MyRetainedReloadListener.LISTENER_KEY);
Reload Listener Class Hierarchy
Green elements are interfaces, yellow classes are abstract.
PreparableReloadListener
PreparableReloadListener sits at the top of the hierarchy, and is the type accepted by the events above. It defines a method #reload() that returns a CompletableFuture<Void> and accepts four parameters:
SharedState currentReload: The shared state of the reload.Executor taskExecutor: AnExecutorfor tasks that can run on multiple threads.PreparationBarrier barrier: A threading barrier object.Executor reloadExecutor: AnExecutorfor tasks that need to run on the main thread.
Additionally, it defines two default methods:
prepareSharedState(SharedState currentReload): Does nothing by default. See Shared Reloading State.getName(): Returns a name to use in logging. By default, returns the class name.
In a PreparableReloadListener, you can load basically anything. For example, this is directly implemented by many reload listeners that load binary data (textures, fonts, etc., but notably not sounds), as well as some others such as data maps. However, many systems - for example many JSON-based systems - use one of the abstract classes below instead.
ContextAwareReloadListener
ContextAwareReloadListener is a utility class that supplies a data load condition context, obtainable via #getContext(). Additionally, it provides a registry access via #getRegistryLookup().
This class is added into the hierarchy by NeoForge, as data load conditions are a NeoForge system.
SimplePreparableReloadListener
SimplePreparableReloadListener<T> is an example implementation of a PreparableReloadListener that splits the preparation and application stages (see Multi-Threaded Reloading) into two entirely separate methods. Instead of #reload(), you must now override #prepare() and #apply(). For example:
// The generic type denotes the type of the object that is passed from #prepare() to #apply().
// For example, in many cases, this will be a List<MyObject>, Map<?, MyObject> or similar.
// This is often (but not necessarily) the same as the type of the stored data.
public class MyReloadListener extends SimplePreparableReloadListener<MyObject> {
// We use the singleton pattern of client reload listeners here for the sake of example.
// This part can be adjusted as needed if you're using a server reload listener.
public static final Identifier ID = Identifier.fromNamespaceAndPath("mymod", "my_listener");
public static final MyReloadListener INSTANCE = new MyReloadListener();
private MyReloadListener() {}
// A field to hold our object.
private MyObject myObject;
@Override
protected MyObject prepare(ResourceManager manager, ProfilerFiller profiler) {
// Run whatever logic to collect, parse and validate the files.
return new MyObject();
}
@Override
protected void apply(MyObject preparations, ResourceManager manager, ProfilerFiller profiler) {
// Set the field in our object to the prepared objects, for later use.
this.myObject = preparations;
}
// Provide access to the values in whatever way you deem necessary. For example:
public MyObject getMyObject() {
return myObject;
}
}
SimpleJsonResourceReloadListener
SimpleJsonResourceReloadListener<T> is a further specialization of SimplePreparableReloadListener that loads and parses JSON files using a Codec. It extends SimplePreparableReloadListener<Map<Identifier, T>>, meaning a map of filenames (represented as identifiers) to whatever type you want to parse your JSONs to.
The class also completely implements the preparation stage for you, in the manner that most JSON systems work: going through the resource packs top to bottom, and only retains the top-most entry for each filename. This means that to perform merging (similar to e.g. tags) or other operations that involve all the files for each filename from different resource/data packs, you need to implement folder scanning yourself.
All that remains for you to do is store the Map<Identifier, T> in #apply(). The simplest implementation looks like this:
public class MyReloadListener extends SimpleJsonResourceReloadListener<MyObject> {
// As above.
public static final Identifier ID = Identifier.fromNamespaceAndPath("mymod", "my_listener");
public static final MyReloadListener INSTANCE = new MyReloadListener();
// This map will store our values.
private final Map<Identifier, MyObject> values = new HashMap<>();
private MyReloadListener() {
// Add a super call here. The parameters are the codec (assuming MyObject.CODEC to be a Codec<MyObject>)
// and a FileToIdConverter, see the FileToIdConverter section below.
super(MyObject.CODEC, FileToIdConverter.json("mymod/my_listener"));
}
@Override
protected void apply(Map<Identifier, MyObject> preparations, ResourceManager resourceManager, ProfilerFiller profiler) {
// Clear out the old values and add our new ones.
values.clear();
values.putAll(preparations);
}
// Provide access to the values in whatever way you deem necessary. For example:
@Nullable
public MyObject get(Identifier id) {
return values.get(id);
}
public Map<Identifier, MyObject> getAll() {
return Collections.unmodifiableMap(values);
}
}
And then simply access your values like so (of course making sure that key actually exists):
MyObject myObject = MyReloadListener.INSTANCE.get(Identifier.fromNamespaceAndPath("mymod", "example"));
FileToIdConverter
FileToIdConverter is a utility record used for converting filenames into Identifiers. It defines a namespace-local prefix and an extension, which are stripped away. According to these, the converter splits each file path into five parts:
- The
assetsordatadirectory - The namespace, consisting of the next subdirectory's name
- The prefix of the
FileToIdConverter(which may contain slashes, making it span multiple directory layers) - The path of the file (which again may contain slashes to span multiple directory layers)
- The extension of the file.
It will then construct an Identifier from the namespace and the path of the file, i.e., the second and fourth part. For example, consider the following FileToIdConverter:
FileToIdConverter converter = new FileToIdConverter("mymod/my_listener", ".json");
// Equivalent, automatically sets the .json extension:
FileToIdConverter converter = FileToIdConverter.json("mymod/my_listener");
The above converter will split the path assets/mymod/mymod/my_listener/example_1.json as follows:
assetsmymodis the namespacemymod/my_listeneris the prefix of theFileToIdConverterexample_1is the path.jsonis the extension of theFileToIdConverter
So the resulting Identifier will be mymod:example_1. Other examples:
assets/mymod/mymod/my_listener/subfolder/example_2.json->mymod:subfolder/example_2assets/othermod/mymod/my_listener/example_3.json->othermod:example_3
Besides the constructor and the FileToIdConverter#json() helper, there is an additional helper FileToIdConverter#registry() that accepts a ResourceKey<? extends Registry<?>> and converts the registry key to a namespace-and-path string, like the ones above.
In order to avoid conflicts where two mods add a registry that is named the same, it is strongly recommended to prefix your reload listener's folder with a folder named after the mod id, as seen above with mymod/my_listener. The #registry() helper does this for you automatically.
ResourceManagerReloadListener
ResourceManagerReloadListener is a special utility interface that runs once the reload itself has completed, providing the fully-populated ResourceManager in its only method #onResourceManagerReload(). Classes implementing this interface mainly do post-reload cleanup work, cache building or similar.
Shared Reloading State
As mentioned before, reloading happens on multiple threads since reload listeners are generally unrelated to one another. If you need to access another reload listener's values, you must set a shared state. To do so, in your reload listener, override the default prepareSharedState() method:
// Create a record (or class) holding our data to pass to another listener
// Instead of Object, use whatever actual type you need
public record MyPendingResources(CompletableFuture<Object> future) {}
// Can also extend/implement any subclass/subinterface of PreparableReloadListener
public class MyReloadListener implements PreparableReloadListener {
// other stuff here
// Create a StateKey with our record as the type
public static final StateKey<MyPendingResources> STATE_KEY = new StateKey<>();
// Override prepareSharedState() to add our pending resources
@Override
public void prepareSharedState(SharedState sharedState) {
sharedState.set(STATE_KEY, new MyPendingResources(/* any data here */));
}
// Again, instead of Object, use (and return) whatever type you need
private Object prepare(ResourceManager resourceManager) {
return new Object();
}
// Then, use in reload() like so:
@Override
public CompletableFuture<Void> reload(SharedState sharedState, Executor taskExecutor, PreparationBarrier barrier, Executor reloadExecutor) {
// Begin by calling prepare():
return CompletableFuture.supplyAsync(() -> prepare(sharedState.resourceManager()), taskExecutor)
// Set the shared state:
.whenComplete((data, error) -> {
// Again, use whatever type you need instead of Object
CompletableFuture<Object> future = sharedState.get(STATE_KEY).future();
if (data != null) {
future.complete(data);
} else {
future.completeExceptionally(error);
}
})
// Wait for the preparation and proceed to the application stage
.thenCompose(preparationBarrier::wait)
.thenAcceptAsync(preparations -> {
// Application stage here
}, reloadExecutor);
}
}
Then, in a dependent reload listener, access the data like so:
public class MyDependentReloadListener implements PreparableReloadListener {
@Override
public CompletableFuture<Void> reload(SharedState sharedState, Executor taskExecutor, PreparationBarrier preparationBarrier, Executor reloadExecutor) {
return sharedState.get(MyReloadListener.STATE_KEY)
.future()
.thenApplyAsync(this::prepare, taskExecutor)
.thenCompose(preparationBarrier::wait)
.thenAcceptAsync(this::apply, reloadExecutor);
}
// Preparation stage; yet again, use whatever type you need instead of Object
private Object prepare(Object data) {
return data;
}
// Application stage
private void apply(Object data) {
// ...
}
}