Reload listeners - #368
Conversation
Deploying with Cloudflare Pages
|
|
|
||
| In some situations, integrating with the [existing resource systems][resources] 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, loaded into a global `ResourceManager` and usually reference-copied into other places for easier access. It will then keep the contents until the next reload, at which point the contents will be discarded and the cycle starts anew. |
There was a problem hiding this comment.
It may be worth being more specific here by saying the pack repository since it could expand into the subpacks we briefly touch on the datagen docs.
| 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. | ||
|
|
||
| :::warning | ||
| On the server [side][sides], the more robust [datapack registry][datapackregistries] or [data map][datamaps] systems should be preferred over a reload listener, if possible. |
There was a problem hiding this comment.
I would mark this as info instead of warning because there's nothing wrong with using a custom reload listener instead. There is also nothing inherently more robust about the two alternatives, they are just existing systems that may or may not work for a given use case.
| |----------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------| | ||
| | **Loads From** | Resource packs (`assets` folder) | Data packs (`data` folder) | | ||
| | **First Reload**<br/>(Physical Client) | - Startup | - Creating a new world<br/>("Preparing for world creation...")<br/>- Joining an existing world<br/>- Joining a server | | ||
| | **Subsequent Reloads**<br/>(Physical Client) | - Changing resource packs in the Options menu<br/>- Downloading a server's custom resource pack on server join<br/>- Pressing F3+T | - Leaving a world or server<br/>(unloads the listener) | |
There was a problem hiding this comment.
- Leaving a world or server (unloads the listener)
This is wrong or at the very least incredibly confusing. Leaving a world or server does unload the data loaded during the last server-side resource reload but it does not touch the reload listeners at all. If a listener is neither kept in a field nor in Neo's "retained listeners" (more on that later) then it goes out of scope right after the reload completes, if it's retained then it goes out of scope with the data and if it's kept in a field then it stays around forever.
| ```java | ||
| // For client-side reload listeners | ||
| @SubscribeEvent // on the game event bus only on the physical client | ||
| public static void addClientReloadListeners(AddClientReloadListenersEvent event) { | ||
| event.addListener(MyReloadListener.ID, MyReloadListener.INSTANCE); | ||
| } | ||
|
|
||
| // For server-side reload listeners | ||
| @SubscribeEvent // on the game event bus | ||
| public static void addServerReloadListeners(AddServerReloadListenersEvent event) { | ||
| event.addListener(MyReloadListener.ID, MyReloadListener.INSTANCE); | ||
| } | ||
| ``` |
There was a problem hiding this comment.
Despite the additional warning below, this example should use different class names for server and client reload listeners to avoid confusion.
Server reload listeners should also ideally not be kept in a static field and instead use AddServerReloadListenersEvent#addRetainedListener() and later retrieve the listener from ReloadableServerResources#getListener(). Storing a server reload listener in a static field risks leaking loaded data, requiring manual cleanup, as well as reaching across sides.
| ```java | ||
| // Create a record (or class) holding our data to pass to another listener | ||
| public record MyPendingResources(/* any data here */) {} | ||
|
|
||
| // 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(PreparableReloadListener.SharedState currentReload) { | ||
| currentReload.set(STATE_KEY, new MyPendingResources(/* any data here */)); | ||
| } | ||
|
|
||
| // Then, use in reload() like so: | ||
| @Override | ||
| public CompletableFuture<Void> reload(SharedState currentReload, Executor taskExecutor, PreparationBarrier barrier, Executor reloadExecutor) { | ||
| MyPendingResources pending = currentReload.get(STATE_KEY); | ||
| // do the reload here, using `pending` | ||
| } | ||
| } | ||
| ``` |
There was a problem hiding this comment.
This example is incomplete (doesn't show how to "wait" for the other listener's data) and confusing (the state is provided and used by the same listener).
The shared object basically must be a "dangling" CompletableFuture (i.e. Neo's AnimationLoader) or map thereof (vanilla's AtlasManager). The dependent listener retrieves this future from the SharedState and incorporates it into its future chain to wait on it (see use of AtlasManager.PENDING_STITCH in ModelManager#reload()). The listener being depended on produces the data, retrieves the future from the SharedState and manually complete()s it (see AnimationLoader#reload()) which effectively "wakes up" the dependent listener.
The statement below this comment is incorrect (the SharedState is just a "dumb" map, it performs no synchronization whatsoever) and should be replaced with a rough explanation of this.
Adds reload listener docs. Since this covers both client and server (as they are almost the same), they are placed in the root Resources folder.
Opening as draft as it is not yet complete. At the time of writing, the second half of the article, as well as any and all links, are missing.EDIT: Done.Closes #306 .
Preview URL: https://pr-368.neoforged-docs-previews.pages.dev