NetKit
Shared Photon co-op transport layer for Outward BepInEx mods: channels over one relay, a hello/peer ledger, per-channel counters/heartbeat, PUN diagnostics, and a replicated-record store for consumer
About this mod Author’s original text
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.
Requires: BepInEx 5 (BepInExPack_Outward) and ForgeKit — a hard dependency
([BepInDependency]), so BepInEx refuses to load NetKit if ForgeKit/ isn't in BepInEx/plugins.
No SideLoader; NetKit builds on a clean runner.
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_Bus wire envelope
(channel, verb, seq, extra, payload) carries every channel; the demux is channel → verb.
- One hello / one absence detector. nk.hello on 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.
netdump reports transport/attach/peers/counters; selftest runs the loopback + Core checks.
The per-channel heartbeat line carries cumulative tx=/rx=/drops= totals (after pt=, 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 OUTGOING nk.hello sends 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 on CharacterManager's PhotonView
(viewID 999). RefreshRpcMonoBehaviourCache() after attach is a harmless no-op precaution —
the shipped UseRpcMonoBehaviourCache is false, so it is not "mandatory".
- Event (opt-in): PhotonNetwork.RaiseEvent on 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
Install & use Install: BepInEx plugin
- The archive from playbuff is the mod itself — usually a BepInEx plugin (a .dll or a plugin folder once unzipped).
- Recommended: use a community mod manager (such as r2modman) — pick this game, import the mod, and the manager sets up BepInEx and its dependencies for you.
- Manual route: install the BepInEx loader in the game folder, then drop the unzipped .dll or plugin folder into BepInEx/plugins/.
- Launch the game — the BepInEx console logs every plugin it loads; seeing this mod in the log means it is installed.
Mods with listed dependencies need those installed too, or they will not load. Options are usually changed in-game or in the plugin's config file.
Install these first, or the mod will not work: