System replacement
A Poly Mod Loader system is a game feature that can be replaced. Each system has exactly one active implementation. It is either Poly Mod Loader's default implementation or a replacement that one mod explicitly declares.
A mod does not reference Minecraft classes. It only implements a contract interface. Poly Mod Loader's version adapter connects it to the game, so game code does not know which mod replaced the system.
The only system today is poly:game_brand (the brand string the game reports). All APIs are PREVIEW.
Concepts
Type (dev.polymodloader.api.system.…) | Description |
|---|---|
SystemId | System ID, in the form namespace:path (for example poly:game_brand) |
PolySystem | Marks a system contract interface. Every contract extends it |
SystemProvider<T> | The class that creates the implementation. Implement only T create(SystemContext context) |
SystemContext | Information that create receives: systemId(), side(), providerModId() (poly for the default implementation) |
SystemLookup | Looks up the active implementation. get(Class<T> contract) |
brand.GameBrandSystem | Contract of the first system, poly:game_brand |
How to declare
Declare a replacement only in poly.mod.json. There is no API to replace a system from code at run time. This is because it must be possible to know who changes what before any mod code runs.
{
"capabilities": ["system.provider.replace"],
"replacesSystems": [
{ "system": "poly:game_brand", "provider": "com.example.poly.brand.ExampleBrandProvider" }
]
}
| Field | Description |
|---|---|
system | ID of the system to replace |
provider | Name of the class that implements SystemProvider. It needs a public no-argument constructor |
override | Optional. Default false. See Conflicts |
- To use
replacesSystems, you need the capabilitysystem.provider.replace. Without it you getPOLY-METADATA-012. - In code, you can only look up the active implementation.
ModContext.systems()in the entrypoint returns aSystemLookup.
GameBrandSystem brand = mod.systems().get(GameBrandSystem.class);
LOGGER.info("brand on this side: {}", brand.brand(mod.side()));
All fields are in the poly.mod.json schema.
When it runs
| Stage | What happens | Error |
|---|---|---|
| Conflict resolution | Poly Mod Loader picks the active provider for each system from every mod's replacesSystems. No mod code has run yet | POLY-SYSTEM-001, 002, 004, 005 |
| System registration | Poly Mod Loader creates the provider and calls create once on the main thread. It validates the result against the contract and the system's own rules | POLY-SYSTEM-003 |
| Entrypoint | From here on, you can look up the active implementation with mod.systems() | — |
| Game running | Hooks in the version adapter call the active implementation every time the game asks | POLY-SYSTEM-006 |
create runs before the entrypoint of every mod. So a provider cannot rely on state prepared in an entrypoint. The full order is in Lifecycle and threads.
If validation fails, Poly Mod Loader does not silently fall back to the default implementation. It stops startup and reports the problem.
The GameBrandSystem contract
poly:game_brand decides what the game reports itself as. It appears in the "modded" mark in the window title, in crash reports, and in the server brand sent to clients.
public interface GameBrandSystem extends PolySystem {
SystemId ID = new SystemId("poly:game_brand");
String brand(GameSide side);
}
| Item | Rule |
|---|---|
| Default implementation | Returns poly |
| Replacement | Full replacement (REPLACE) only. There are no extension points |
| Thread | Called on any thread, many times |
| State | Must be stateless and fast. This system owns no data |
| Result | 1–64 printable ASCII characters. No leading or trailing whitespace |
| Validation | Before startup, Poly Mod Loader checks the values for both CLIENT and DEDICATED_SERVER. A mismatch gives POLY-SYSTEM-003 |
brand receives side, so you can return different values for the client and the server. Both values must follow the rules, whichever side is running.
Walkthrough: change the game brand
examples/custom-system-mod in the Poly Mod Loader repository changes the brand to poly/example. This mod has no entrypoint. It works with only the replacement declaration and the provider class.
1. Dependencies
The compileOnly dependency poly-lifecycle-api in your mod project includes the system API (poly-system-api). You do not need to add anything. The example inside the repository uses only compileOnly(project(":poly-system-api")).
2. The provider
package com.example.poly.brand;
import dev.polymodloader.api.system.SystemContext;
import dev.polymodloader.api.system.SystemProvider;
import dev.polymodloader.api.system.brand.GameBrandSystem;
/**
* Replaces Poly Mod Loader's {@code poly:game_brand} system. Minecraft then reports {@code poly/example} as its
* brand, without this mod referencing any Minecraft class.
*/
public final class ExampleBrandProvider implements SystemProvider<GameBrandSystem> {
@Override
public GameBrandSystem create(SystemContext context) {
return side -> "poly/example";
}
}
GameBrandSystemhas only one method, so you can return a lambda.- The lambda is stateless, returns the same value every time, and is safe to call from any thread.
- The class is public and has a no-argument constructor (the default constructor).
3. The declaration
{
"schemaVersion": 1,
"id": "custom_system_mod",
"version": "0.1.0",
"name": "Custom System Mod",
"environment": "both",
"minecraft": [
"26.*"
],
"depends": {
"poly": ">=0.1.0-alpha <0.2.0"
},
"capabilities": ["system.provider.replace"],
"replacesSystems": [
{ "system": "poly:game_brand", "provider": "com.example.poly.brand.ExampleBrandProvider" }
]
}
4. Check it
When you run the game, the startup log shows the active system and provider.
[Poly Mod Loader] System poly:game_brand: com.example.poly.brand.ExampleBrandProvider from custom_system_mod
The first time the game asks for the brand, it writes the following line once for each side.
[Poly Mod Loader] poly:game_brand reported 'poly/example' to Minecraft (CLIENT)
The active system and provider are also recorded in .poly/startup-report.json. If nothing is replaced, the startup log ends with … from poly (default).
In the Poly Mod Loader repository, clientWorldReloadTest checks this client log.
Conflicts
If two mods replace the same system, Poly Mod Loader does not guess. It decides with these rules before any mod code runs.
| Situation | Result |
|---|---|
| 0 replacement declarations | Default implementation |
| 1 replacement declaration | That provider |
2 or more, and one mod has override: true and depends (transitively) on all other replacing mods | That mod wins. The startup log records the overridden mods as overriding [...] |
| Any other case with 2 or more | Startup stops with POLY-SYSTEM-001. It reports the mod IDs and how to fix it |
No mod wins, and a mod wrote override: true but did not meet the conditions | POLY-SYSTEM-005 for each such mod, together with the POLY-SYSTEM-001 above |
To win with override, list the other mod in depends.
{
"id": "my_brand_mod",
"depends": {
"poly": ">=0.1.0-alpha <0.2.0",
"custom_system_mod": "*"
},
"capabilities": ["system.provider.replace"],
"replacesSystems": [
{ "system": "poly:game_brand", "provider": "com.example.mybrand.MyBrandProvider", "override": true }
]
}
If you get POLY-SYSTEM-001, choose one of these.
- Keep only one replacing mod.
- Make one mod depend on all the other replacing mods, and write
"override": truein that mod'sreplacesSystemsentry.
Error codes
| Code | When | Result |
|---|---|---|
POLY-METADATA-012 | replacesSystems is used but the system.provider.replace capability is missing | Startup stops |
POLY-SYSTEM-001 | Two or more mods replace the same system and the winner cannot be decided | Startup stops |
POLY-SYSTEM-002 | Unknown system ID | Startup stops |
POLY-SYSTEM-003 | The provider class cannot be created (does not implement SystemProvider, no public no-argument constructor), create threw, returned null, does not implement the contract, or failed system-specific validation | Startup stops |
POLY-SYSTEM-004 | System that cannot be replaced | Startup stops |
POLY-SYSTEM-005 | Invalid override | Startup stops |
POLY-SYSTEM-006 | The game asked for the brand before Poly Mod Loader connected the system | IllegalStateException |
The full list is in Error codes.
Current limits
poly:game_brandis the only system.- Only full replacement of a system is supported. There is no extension (
EXTEND) that adds to the default implementation. - There are no systems with several replacement points yet (for example, replacing recipe registration, lookup, and sync separately).
- There are no dependencies between systems or initialization and shutdown order yet.
- The transform rule that connects the brand to the game is optional. If the game jar differs from what is expected, Poly Mod Loader does not stop startup. It only turns that rule off. In that case, the game reports its original brand.
- Development runs (
runClientand others) always includecustom-system-mod, so when you develop in the Poly Mod Loader repository, the game brand shows aspoly/example.
Poly Mod Loader decides which kinds of systems can be replaced. Today there is only poly:game_brand, and this page will add more as they appear.