NetKit
Shared Photon co-op transport layer for Outward BepInEx mods: channels over one relay (or the RaiseEvent bus), a hello/peer ledger with absence detection, per-channel counters/trace/heartbeat, and PUN diagnostics.NetKit
📖 Full documentation: NetKit wiki page
The shared Photon co-op transport layer for the Outward mod kits. It owns the transport shell
CompanionKit's NetBus and SpawnKit's SpawnNet used to duplicate (~65–75% line-identical): relay
attach, send helpers, the one hello + peer ledger + absence detector, per-channel+verb counters and
trace, join/leave hooks, heartbeat, and the PUN diagnostics. Message semantics (verbs, codecs,
authorization, flush policies) stay in the consumer mods.
Full design + decisions: docs/netkit-plan.md. Background: docs/photon-layer-research-2026-07-18.md.
What it owns
- Channels, not per-mod buses. One shared
NK_Buswire envelope(channel, verb, seq, extra, payload)carries every channel; the demux ischannel → verb. - One hello / one absence detector.
nk.helloon join/connect/guest-scene-ready carries the proto version + the registered channel→version map + optional per-channel extension strings. Peer state surfaces per channel (OnPeerReady/OnPeerLost/IsPeerReady/ReadyCount). The 10 s "peer appears UNMODDED" warning lives here once. - Diagnostics under the owning channel's log tag: counters, ring-buffer trace, heartbeat
(all
PhotonNetwork.time-stamped), the PUN log-signature watcher + per-id unknown-view table.netdumpreports transport/attach/peers/counters;selftestruns the loopback + Core checks. The per-channel heartbeat line carries cumulativetx=/rx=/drops=totals (afterpt=, before the consumer fragment) so the passive stream survives when the interactive dumps are unreachable.
Dev verbs (BepInEx/config/NetKit_cmd.txt)
netdump— co-op census on this machine (transport/attach, per-channel verbs/peers/counters/ ring buffer, hello ledger, PUN-signature + unknown-view tables,helloMuted).selftest—[SELFTEST] PASS/FAIL … DONE: hello codec, compat calc, peer-ledger timing, counters, and (in a room) the transport loopback.netmute [on|off|status](bare = status) — ON suppresses OUTGOINGnk.hellosends so this box reads as UNMODDED to peers; the staging tool for incompatible-peer testplan rows without renaming a DLL mid-session. Incoming handling unchanged. Session-only (not persisted), default off.- Pure half in
core/NetKit.Core(unit-tested, zero game refs): hello codec (lossless escaping), channel-version compat, the peer-ledger timing, counters/trace models, heartbeat formatter, unknown-view table.
API sketch (game side)
var ch = Net.RegisterChannel("sk", "0.4.0", new ChannelOptions {
LogTag = "SKNET", HelloExtension = () => …, HeartbeatFragment = () => … });
ch.Register("spawn", msg => …); // msg: Verb, Payload, Extra, SenderActor,
// SenderIsMaster, SenderIsSelf
ch.SendToMaster("hit", payload, extra); // bool; also SendToOthers/SendToAll/SendToPlayer
ch.SendToAllLoopback("nk.test", …); // selftest
ch.OnPeerReady += info => …; // info: Actor, ChannelVersion, Extension, IsMaster
ch.CountDrop("spawn", "no-row"); // consumer-reported drops
Net.Attached / Net.InRoom / Net.IsMaster / Net.IsGuestInRoom
Send guards return false + drop-count (never throw). Unknown verb on receive = warn-once +
counter. Handlers run inside try/catch with per-verb error counters.
Two backends (one internal transport interface)
- Rpc (default): the
NK_Bus[PunRPC]relay piggybacked onCharacterManager'sPhotonView(viewID 999).RefreshRpcMonoBehaviourCache()after attach is a harmless no-op precaution — the shippedUseRpcMonoBehaviourCacheis false, so it is not "mandatory". - Event (opt-in):
PhotonNetwork.RaiseEventon one configurable code ([Net] EventCode, default 177) — no GameObject, no view. Implemented + selftest-covered but config-gated OFF ([Net] Transport = Rpc) until its offline loopback + two-box behavior are live-verified.
Example configuration
BepInEx/config/cobalt.netkit.cfg — created on first launch. Excerpt (defaults):
[Net]
## Transport backend: Rpc (default) or Event.
Transport = Rpc
## RaiseEvent code used by the Event backend.
EventCode = 177
## Seconds between per-channel heartbeat lines.
HeartbeatSeconds = 30
## Seconds before a silent peer is warned as "appears UNMODDED".
HelloWarnSeconds = 10
## Per-message send/receive logging under each channel's tag.
VerboseNet = true
## Photon DisconnectTimeout override in ms; 0 = leave the game's default.
DisconnectTimeoutMs = 0
The dev command channel is BepInEx/config/NetKit_cmd.txt (the verbs above). NetKit ships no
config-override data tables.
Cutover note
New builds speak NK_Bus only. An old-build peer (CK_Bus/SK_Bus) looks unmodded to the new
hello and is refused co-op cooperation by the existing incompatible-peer machinery — acceptable
because the mods ship as one bundle (Outward-Mods-latest.zip). W2/W3 refactor CompanionKit's
NetBus and SpawnKit's SpawnNet onto channels ck / sk; nothing ships live-verified until a
real two-box session per project rules.
