Networking¶
Archie's networking layer provides a type-safe, annotation-driven API for sending packets between client and server. All packets are serialized with CBOR via kotlinx.serialization, so no manual buffer reading/writing is required.
NetworkChannel¶
NetworkChannel is the entry point. A single channel instance can handle any number of
server-bound and client-bound packet types.
// Create the channel (usually a top-level object or companion property)
val CHANNEL = NetworkChannel(MyMod.MOD % "main")
Defining packets¶
Packet types must be Kotlin data classes annotated with @Serializable.
@Serializable
data class SyncEnergyPacket(val energy: Int, val pos: SBlockPos)
@Serializable
data class RequestDataPacket(val id: Int)
Registering handlers¶
Register handlers before calling register(). The reified serverbound<T>/clientbound<T>
overloads infer the packet class from the type parameter, so you don't need to pass ::class
yourself:
// Server receives this packet from the client
CHANNEL.serverbound<RequestDataPacket> { packet, ctx ->
val player = ctx.player as ServerPlayer
val data = fetchData(packet.id)
CHANNEL.toPlayer(player, SyncEnergyPacket(data.energy, data.pos))
}
// Client receives this packet from the server
CHANNEL.clientbound<SyncEnergyPacket> { packet, ctx ->
ClientEnergyCache.update(packet.pos, packet.energy)
}
// Called once during mod init
CHANNEL.register()
A KClass-based overload (serverbound(RequestDataPacket::class) { ... }) is also available if
you already have the class reference in hand; both forms register the same way.
Validation¶
serverbound/clientbound validate the packet class as soon as you register it, throwing
IllegalArgumentException if:
- the class isn't a Kotlin data class,
- the class isn't annotated
@Serializable(or otherwise has no serializer), or - that exact class was already registered on that side of the channel.
Sending an unregistered packet type (via toServer/toPlayer/etc.) throws IllegalStateException
instead, since that failure can only be detected at send time.
Sending packets¶
| Method | Description |
|---|---|
toServer(packet) |
Client → Server |
toPlayer(player, packet) |
Server → specific player |
toPlayers(list, packet) |
Server → list of players |
toAllPlayers(packet) |
Server → every connected player |
toPlayersInDimension(level, packet) |
Server → all players in a dimension |
toNearPlayers(level, exclude, x, y, z, radius, packet) |
Server → players within radius |
toPlayersTrackingEntity(entity, self, packet) |
Server → players loading an entity |
toPlayersTrackingChunk(level, pos, packet) |
Server → players loading a chunk |
IPacketContext¶
The IPacketContext interface is passed to every packet handler and exposes:
| Property | Type | Description |
|---|---|---|
player |
Player |
The player associated with the packet |
registryAccess |
RegistryAccess |
Dynamic registry access |
minecraft |
Minecraft |
Client-side Minecraft instance (client handlers only) |
Minecraft type serializers¶
For serializing Minecraft types inside packets, use the provided S-prefixed type aliases (each
one is already @Contextual-annotated, so you don't add @Contextual yourself):
@Serializable
data class TeleportPacket(
val destination: SBlockPos,
val dimension: SResourceLocation,
)
Available contextual serializers: BlockPos, ChunkPos, GlobalPos, Vec3, Vec3i,
BlockHitResult, ResourceLocation, ItemStack, FriendlyByteBuf — see
Serialization for the full alias table.
Config sync¶
ConfigSpec.Server configs (see Config) use a NetworkChannel of their own,
internally, to sync per-world server config values to clients — the same channel mechanism
described above, just with the ConfigSpec itself as the payload instead of a hand-written
packet class. This is wired up automatically by ConfigSpec; you don't register anything with
it yourself. See Config § Server configs sync over the network
for how it behaves.