-
Notifications
You must be signed in to change notification settings - Fork 0
Rubric Sync
Rubric replicates server-authoritative config values to connected clients over the loader's custom-payload channel — Fabric's PayloadTypeRegistry or NeoForge's PayloadRegistrar. This page covers the model, the annotation, and the on-the-wire lifecycle.
Every entry has a sync scope, set via @Sync (see Annotations):
-
CLIENT— client-owned, local only, never leaves the client's disk. The default. -
SERVER— server-authoritative. Pushed to clients for the connected session. The client cannot override it, and it is not written to the client's<config>.toml. -
COMMON— server-authoritative when connected; falls back to the client's local defaults in singleplayer.
The scope is set at the entry level (or the type level as the default for every entry in that config). Configs are never wholesale-synced; only the entries whose scope demands it cross the wire.
- A
CLIENT-scoped entry — say, a UI opacity — should never be overwritten by a server. The player configured it once and expects it to stick. - A
SERVER-scoped entry — say, a game rule expressed as a config value, or a tick interval — has to match on server and client. Rubric makes the server the source of truth and pushes the value on connect. - A
COMMONentry behaves likeSERVERwhen the world is multiplayer and likeCLIENTwhen it's not. This is what you want for values that are game-rule-like but also fine to just set locally in singleplayer.
-
Server startup. The loader entry class (
RubricFabricMod.onInitializeon Fabric,RubricNeoForgeMod's constructor on NG) registers the packet channel and constructs aSyncService(Role.SERVER, transport, codecs, gate). -
Client connects. On Fabric,
ServerPlayConnectionEvents.JOINfires; on NG,PlayerEvent.PlayerLoggedInEvent. Both callsyncService.onClientConnected(player), which sends the client:- A
Handshakewith the server's protocol version and known config IDs. - A
Snapshotper registered config, containing everySERVER/COMMONentry (nothingCLIENT-scoped ever leaves the server).
- A
-
Client applies.
SyncService(Role.CLIENT)receives via Fabric'sClientPlayNetworking.registerGlobalReceiveror NG'sPayloadRegistrar.playToClienthandler. Each payload runs throughInboundValidator(size caps, per-entry validators, codec decode) thenScopeEnforcer.applyAuthoritative(...)writes the in-scope entries onto the local POJO. Every applied path is markedOrigin.FROM_REMOTE, so a latermanager.save()doesn't persist it to the client's file. -
Delta broadcast. If a
SERVER/COMMONvalue changes on the server after clients are already connected (amanager.set(...)call, or an admin edit through the GUI), the server broadcasts aDeltato every connected client. Applied identically to the initial snapshot. -
Client edits. If the client is allowed to edit a
SERVER/COMMONentry (see permission gate below), the change is sent up as aClientEdit. Server validates, applies, and broadcasts the resultingDeltaback to all clients. On acceptance the server marks the path asLOCAL_EDITandsave()persists it.
Client edits pass through PermissionGate.canEdit(actor, requiredLevel) on the server. The default gate is DENY_ALL; each loader integration ships its own implementation:
-
Fabric —
FabricPermissionGatehonors Fabric'sPermissionCheckEvent(or a fallback op-level check). -
NeoForge —
NeoForgePermissionGatecallsServerPlayer.hasPermissions(level).
If your mod wants to expose an admin panel that edits server-authoritative values, either supply your own gate to installServer(...) in the bootstrap, or (on Fabric) override the ambient gate:
FabricPermissionGate.override((actor, level) -> actor.hasPermissionLevel(level));Rubric currently gates on op-level 2 (CLIENT_EDIT_REQUIRED_LEVEL) — enough for /gamerule-style privileges.
Every payload carries a ProtocolVersion. Servers and clients on mismatched versions:
- With
sync.requireProtocolMatch = true(the default) — fail-closed: the payload is rejected and the session logs a warning. Recommended: mismatches usually mean the server is on a newer/older Rubric that changed the wire. - With
sync.requireProtocolMatch = false— mismatched payloads are skipped with a log warning; other payloads continue. Handy for gradual rollouts but risky.
See Self-config for sync.* knobs.
sync.payloadMaxBytes caps any single sync payload. Snapshots larger than this are rejected on the receiving side. Default 1 MiB; hard cap 16 MiB. Purpose: defend against a hostile or misconfigured server sending an unbounded payload.
The integrated server + client run in one JVM. Both transports short-circuit on the loopback path — FabricNetworkTransport.isIntegratedHost on Fabric, the identical check inside NeoForgeNetworkTransport on NG — so no packets are actually pushed. SERVER-scoped values behave as if the client set them locally.
Two useful hooks:
-
manager.getEvents().subscribe(event -> ...)— fires per changed path on both local and remote sources. Checkmanager.originOf(path)if you need to distinguish. -
manager.addReloadListener((previous, current) -> ...)— fires after every non-initialload()on the main thread, with the previous and current POJO. Doesn't fire per remote-delta application (which touches individual paths, not the whole state).
-
Server has no config Rubric-registered but client does — no snapshot arrives for that config. Client's
SERVER/COMMONvalues stay at whatever the local defaults are. Not synced. -
Client rejects a value in
InboundValidator— that path is skipped; other accepted paths still apply. Rejection is logged, no exception raised. -
ScreenProviderwrites aSERVER/COMMONvalue locally without a permission — server denies theClientEdit. No local error until the nextDeltareverts it. A future release will.available(false)such widgets when the client is connected but not permitted — see the frontend gaps documents.
Rubric
Reference
Runtime
GUI
Help