-
Notifications
You must be signed in to change notification settings - Fork 0
Networking
A game-version-agnostic, loader-agnostic packet framework. You define a packet as a plain Java class
and register a handler; Eunomia does the rest — on Fabric, NeoForge and Paper/Bukkit/Purpur, from
Minecraft 1.20.1 to 26.x, with no Fabric API dependency and no CustomPacketPayload/StreamCodec
boilerplate in your code.
A packet payload is just a POJO (public fields, a no-arg constructor). Declare a PacketType for it:
public class SyncPayload {
public String key;
public int value;
public SyncPayload() {}
public SyncPayload(String key, int value) { this.key = key; this.value = value; }
}
public static final PacketType<SyncPayload> SYNC =
PacketType.serverbound("mymod", "sync", SyncPayload.class); // or .clientbound / .bidirectionalThe namespace:path is the channel identity, shared verbatim by every platform.
// Server side (runs in Eunomia.init on the loaders, onEnable on Paper):
CommunicationManager.onServerReceive(SYNC, (payload, ctx) -> {
myStore.put(ctx.senderId(), payload);
ctx.reply(ACK, new AckPayload("stored")); // reply to just the sender
CommunicationManager.broadcastExcept(ctx.senderId(), SYNC, payload); // fan out to everyone else
});
// Client side (runs in EunomiaClient.init):
CommunicationManager.onClientReceive(ACK, (ack, ctx) -> applyAck(ack));That is the whole surface for adding a packet + handler — one call each. See
ExampleServerHandlers / ExampleClientHandlers and the eunomia:example_ping / example_pong /
permission packets for a working reference.
CommunicationManager.sendToServer(SYNC, new SyncPayload("hp", 20)); // client -> server
CommunicationManager.sendToPlayer(uuid, ACK, new AckPayload("hi")); // server -> one client
CommunicationManager.broadcast(ACK, snapshot); // server -> allDirection is enforced: sending a serverbound packet to a client (or vice-versa) throws, catching the
mistake at the call site.
A client can ask, per connection, whether the server it joined runs Eunomia and whether it has a receiver for a specific packet — the decision point for falling back to a custom communications server:
CommunicationManager.serverCapabilities().onResolved(caps -> {
if (!caps.isPresent()) {
// The MC server does not run Eunomia at all.
} else if (!caps.supports(MyPackets.SYNC)) {
// Eunomia is present but this mod's server half is not installed / has no SYNC handler.
}
});The handshake runs automatically on join (a eunomia:hello probe answered by eunomia:hello_ack
carrying the server's receiver channels); "resolved" means an ACK arrived or the probe timed out.
A mod's per-player settings object (Armor Hider's PlayerConfig) is just a ConfigurationItem that also
carries the owning player's UUID — a PlayerLinkedConfigurationItem. Extend PlayerLinkedConfigurationItemBase
to get the id + change-flag plumbing for free, then let Eunomia keep the server-side map for you:
public final class MyConfig extends PlayerLinkedConfigurationItemBase<MyConfig> {
public int level;
public MyConfig() {}
public MyConfig(UUID id, int level) { super(id); this.level = level; }
// ... the remaining ConfigurationItem methods (value/default, schema, migrate, codec) ...
}
public static final PacketType<MyConfig> SYNC =
PacketType.serverbound("mymod", "config", MyConfig.class);
// One store, one wiring call: every SYNC a client sends is stored under its authenticated UUID.
var store = new ServerSidePlayerConfigStorage<>(MyConfig.class, id -> new MyConfig(id, 0))
.handleOn(SYNC);
store.loadFrom(Path.of("config", "mymod-players.json")); // survives restarts; empty if absentLookups are UUID-only — there is no name index. The store never returns null for a good query
(getOrCreate / getOrDefault fall back through the factory), and both put and load-time healing re-stamp
each config's own id to match the map key it lives under, so a client cannot store settings under someone
else's id. toJson / applyJson / saveTo / loadFrom round-trip through the same Eunomia Gson (and the
same config type adapters) that serialize the payload on the wire.
:core (plain Java, no Minecraft) PacketType, CommunicationManager, PayloadCodec (gzip+json),
the handshake, the example packets. One artifact, every version.
│
├── :common / :fabric / :neoforge loader adapter: EunomiaPayload + StreamCodec, the payload-packet
│ codec-injection mixins, the dispatch mixins, MC transports.
│
└── :paper Bukkit plugin: plugin-messaging transport, force-subscribe on
join, reusing :core for the exact same definitions + resolution.
The single CommunicationManager owns registration, routing and dispatch and is transport-agnostic:
platforms install a registration listener and a transport, and feed inbound bytes to dispatch*Raw.
That is exactly the seam a future non-game (HTTP) server plugs into — it registers the same
PacketTypes, feeds request bodies to dispatchServerboundRaw, and installs its own transport, so a
client whose MC server lacks the mod can be pointed at it instead.
One wire format everywhere (PayloadCodec = gzip(json)), so a payload a Fabric client puts on the
wire is byte-for-byte what the Paper plugin decodes.
-
:core—PayloadCodecTest,CommunicationManagerTest,LoopbackHandshakeTest,ServerHandshakeTest: the full routing/codec/handler/handshake logic through real wire bytes. -
:paper—PaperWireContractTest: the plugin speaks the same channels and format as the loaders. -
:smoke— the FCGT client gametest for live end-to-end validation (seejava/smoke/README.md).
The Gradle build lives under
java/(all subprojects, the wrapper and the build config). Run it from there:cd java && ./gradlew :core:test. Gradle module paths (:core,:paper, …) are unaffected by the folder; only filesystem paths gain thejava/prefix.