-
Notifications
You must be signed in to change notification settings - Fork 0
Networking and Data Sync
Package: dev.goshi.omnimixin.api.sync
This subsystem is different in shape from every other subsystem in
OmniMixin. Everywhere else, you're reacting to something vanilla is already
doing. Here, you're declaring a brand new piece of state — a SyncedValue
— that OmniMixin manages entirely on your behalf: storing it on the entity,
saving it to NBT so it survives a chunk unload/reload or a server restart,
and pushing it to nearby clients automatically whenever it changes.
If you've ever hand-rolled a TrackedData<T>/DataTracker field, or
written your own S2C packet just to keep one number synced between server
and client, this is meant to replace that boilerplate entirely.
A SyncedValue<T> is a public static final field, built once via its
builder:
package com.example.mymod;
import dev.goshi.omnimixin.api.sync.SyncedType;
import dev.goshi.omnimixin.api.sync.SyncedValue;
import net.minecraft.util.Identifier;
public final class MyValues {
public static final SyncedValue<Float> MANA = SyncedValue
.<Float>builder(Identifier.of("mymod", "mana"))
.type(SyncedType.FLOAT)
.defaultValue(100.0f)
.syncRange(64.0)
.build();
private MyValues() {
}
}Build it once, somewhere that's guaranteed to run during mod init (a static
field on a class you reference from onInitialize() is the usual pattern —
the Identifier doubles as the value's unique registration key, so it must
be built exactly once).
SyncedType currently supports FLOAT, DOUBLE, INT, BOOLEAN, and
STRING. There's no support yet for arbitrary objects, NBT compounds, or
collections — if you need something more complex than these five primitive
types, you'll need your own packet for now.
syncRange is the radius, in blocks, within which a client needs to be
tracking the entity to receive updates for this value. It defaults to
64.0 if you don't set it. A smaller range means less network traffic for
values that only matter up close (a "charge-up" visual effect, say);
Double.MAX_VALUE effectively means "always sync to anyone tracking this
entity, however far the tracking range itself allows."
Once declared, a SyncedValue<T> reads and writes against any Entity:
float currentMana = MyValues.MANA.get(entity);
MyValues.MANA.set(entity, currentMana - 10.0f);get(entity) returns the stored value, or the declared defaultValue() if
nothing's been set yet (including for an entity that just spawned and has
never had this value written). set(entity, value) does three things in
one call:
- Stores the new value on the entity (so future
get()calls see it). - Marks it to be written to NBT the next time this entity saves, so it survives a chunk unload, world save, or server restart.
- Immediately pushes the new value out over the network to every client
currently tracking that entity within
syncRange().
You don't need to call anything else, register any packet handler, or write
any NBT code yourself — all of that is wired up automatically the moment
you declare the SyncedValue.
package com.example.mymod;
import dev.goshi.omnimixin.api.sync.SyncedType;
import dev.goshi.omnimixin.api.sync.SyncedValue;
import net.fabricmc.api.ModInitializer;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.util.Identifier;
public final class ManaMod implements ModInitializer {
public static final SyncedValue<Float> MANA = SyncedValue
.<Float>builder(Identifier.of("manamod", "mana"))
.type(SyncedType.FLOAT)
.defaultValue(50.0f)
.syncRange(32.0)
.build();
@Override
public void onInitialize() {
// Regenerate 1 mana/second for every player, server-side.
ServerTickEvents.END_SERVER_TICK.register(server -> {
if (server.getTicks() % 20 != 0) {
return;
}
for (PlayerEntity player : server.getPlayerManager().getPlayerList()) {
float current = MANA.get(player);
if (current < 100.0f) {
MANA.set(player, Math.min(100.0f, current + 1.0f));
}
}
});
}
}On the client, any mod (yours or someone else's) can read
MANA.get(somePlayerEntity) for a locally-tracked player and get an
up-to-date value — no packet handling code required on the client side
either, since OmniMixinClient already listens for and applies these
updates for every registered SyncedValue automatically.
You don't need to know this to use SyncedValue, but it's useful for
reasoning about behavior:
- Values are only written to NBT for the entities that actually have a
non-default value set — an entity that's never had
set()called for a givenSyncedValuedoesn't carry any extra NBT weight. - The network push in
set()is a no-op on the logical client — a client callingset()on its own local copy only updates its own local, non-authoritative value, since only the server is allowed to actually broadcast a synced value's new state. Don't rely on a client-sideset()call propagating anywhere. - Sync pushes ride Fabric's standard S2C custom payload channel, so they play nicely with everything else on the connection — there's no separate connection or protocol involved.
SyncedValue is built for simple, frequently-read, frequently-changing
numeric or string state that needs to be visible client-side — health bars,
resource meters, cooldown timers, and similar. For anything structural
(an inventory, a list of quests, nested data), you're better served by a
proper PersistentState/component library and your own packet, since
SyncedValue intentionally doesn't support complex types — see [[Known
Limitations]].