Skip to main content
Version: Minecraft 26.2

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.

Only one system can be replaced today

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
SystemIdSystem ID, in the form namespace:path (for example poly:game_brand)
PolySystemMarks a system contract interface. Every contract extends it
SystemProvider<T>The class that creates the implementation. Implement only T create(SystemContext context)
SystemContextInformation that create receives: systemId(), side(), providerModId() (poly for the default implementation)
SystemLookupLooks up the active implementation. get(Class<T> contract)
brand.GameBrandSystemContract 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.

src/main/resources/poly.mod.json (excerpt)
{
"capabilities": ["system.provider.replace"],
"replacesSystems": [
{ "system": "poly:game_brand", "provider": "com.example.poly.brand.ExampleBrandProvider" }
]
}
FieldDescription
systemID of the system to replace
providerName of the class that implements SystemProvider. It needs a public no-argument constructor
overrideOptional. Default false. See Conflicts
  • To use replacesSystems, you need the capability system.provider.replace. Without it you get POLY-METADATA-012.
  • In code, you can only look up the active implementation. ModContext.systems() in the entrypoint returns a SystemLookup.
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​

StageWhat happensError
Conflict resolutionPoly Mod Loader picks the active provider for each system from every mod's replacesSystems. No mod code has run yetPOLY-SYSTEM-001, 002, 004, 005
System registrationPoly 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 rulesPOLY-SYSTEM-003
EntrypointFrom here on, you can look up the active implementation with mod.systems()—
Game runningHooks in the version adapter call the active implementation every time the game asksPOLY-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);
}
ItemRule
Default implementationReturns poly
ReplacementFull replacement (REPLACE) only. There are no extension points
ThreadCalled on any thread, many times
StateMust be stateless and fast. This system owns no data
Result1–64 printable ASCII characters. No leading or trailing whitespace
ValidationBefore 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​

src/main/java/com/example/poly/brand/ExampleBrandProvider.java
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";
}
}
  • GameBrandSystem has 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​

src/main/resources/poly.mod.json
{
"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.

SituationResult
0 replacement declarationsDefault implementation
1 replacement declarationThat provider
2 or more, and one mod has override: true and depends (transitively) on all other replacing modsThat mod wins. The startup log records the overridden mods as overriding [...]
Any other case with 2 or moreStartup 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 conditionsPOLY-SYSTEM-005 for each such mod, together with the POLY-SYSTEM-001 above

To win with override, list the other mod in depends.

A mod that deliberately overrides another brand replacement (excerpt)
{
"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": true in that mod's replacesSystems entry.

Error codes​

CodeWhenResult
POLY-METADATA-012replacesSystems is used but the system.provider.replace capability is missingStartup stops
POLY-SYSTEM-001Two or more mods replace the same system and the winner cannot be decidedStartup stops
POLY-SYSTEM-002Unknown system IDStartup stops
POLY-SYSTEM-003The 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 validationStartup stops
POLY-SYSTEM-004System that cannot be replacedStartup stops
POLY-SYSTEM-005Invalid overrideStartup stops
POLY-SYSTEM-006The game asked for the brand before Poly Mod Loader connected the systemIllegalStateException

The full list is in Error codes.

Current limits​

  • poly:game_brand is 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 (runClient and others) always include custom-system-mod, so when you develop in the Poly Mod Loader repository, the game brand shows as poly/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.

Buy me a coffee