You are viewing a potentially older version of this package. View all versions.
CeruleanCutlass-NetKit-0.2.3 icon

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 state mirroring.

Date uploaded 2 days ago
Version 0.2.3
Download link CeruleanCutlass-NetKit-0.2.3.zip
Downloads 46
Dependency string CeruleanCutlass-NetKit-0.2.3

This mod requires the following mods to function

BepInEx-BepInExPack_Outward-5.4.19 icon
BepInEx-BepInExPack_Outward

BepInEx pack for Outward.

Preferred version: 5.4.19
CeruleanCutlass-ForgeKit-0.4.2 icon
CeruleanCutlass-ForgeKit

Dependency-free dev-tooling for Outward BepInEx mods: file-driven dev command loop, self-test harness, on-screen toasts, player-ready lifecycle wait, embedded/override table loaders, and a shared dev-verb pack (movement/combat/skill/status probes).

Preferred version: 0.4.2

README

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/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.

Verification state (docs/netkit-testplan.md). The core transport surface is live-proven β€” relay attach, the hello/peer ledger, channel demux and the counters all PASSed across real two-box sessions between 2026-07-27 and 07-31. Still open: the RaiseEvent transport backend (V10/V11 β€” which is why [Net] Transport ships as Rpc), plus V9/V9b/V9c and NK-F2b. Don't read "NetKit is verified" as covering the Event backend, and don't read the open items as covering the RPC path.

CHANGELOG

NetKit changelog

0.2.3 β€” 2026-08-11

  • Phantom view-ID root cause + fixes: NetKit hb per-id attribution (top=[idΓ—n]) + unknown-view RUNAWAY detector, DonorPhotonGuard duplicate-registration veto over live scene-baked views (DuplicateViewPolicy, unit-tested), V-PARKLEAK bounded-window acceptance + pt-stamped mute line; analysis doc + NK-PHANTOM1/2 retest rows β€” built, retest owed
  • Fable-review fixes: viewID watermark reads outstanding ids (latched), corpse-release fallback never parks a neutralized view, FarCache InFlight can't leak, visual-pass retries count as busy, Notify header placement
  • SpawnKit perf wave: hot-loop allocs removed, adaptive replica enforce, prune throttle, viewID watermark + opt-in corpse release, AI sleep radius, caps 8->12
  • Merge fix/sa-0808-spawn: spawn recovery wave (SA 2026-08-08 Β§8)
  • SA-0808 Wave C code half: spawn recovery β€” census, ghost fix-at-cause, quest-gate clear, TerrainManager guard, abandon quarantine
  • NetKit hardening wave (static-analysis 2026-08-08 Β§7 items 1-7 + P1-6/P2-8)
  • Fix duplicate field/const definitions in NetChannel.cs from the netkit-cloudward merge
  • Merge branch 'cleanup/netkit-cloudward-2026-08-02'
  • ck.proxy.pos: the guest's puppet becomes the pet's one position authority (MP-PETAIMDRIFT)
  • Phase-2 view-lease migration: SpawnKit's view lifecycle moves into NetKit
  • Solo-leg live results: core MP10 fix PASSES (zero refusals, clean census, viewID belt proven live); fix the caid false-positive TRIPWIRE it exposed
  • Docs: MP10 fix wave 2 (root cause + NetKit.Views + contamination watchdog)
  • Review fixes M1 + m1-m4: ghost bars-before-Character, per-view neutralize, self-contained disarm
  • NetKit.Views clone hygiene: neutralize-first, honest tripwires, disarm-not-refuse
  • W4: fixes for everything D1 found, plus the join-race P1
  • MP fix waves W1/W2/W3, and what Block A found when we ran them
  • Log levels: a per-mod [Diag] LogLevel, gating at the source
  • NetKit + CompanionKit: GetComponent ?? AddComponent, without the eager trap (UNT0007)
  • Cloudward + NetKit: say it when a decision was downgraded
  • Cloudward + NetKit: five ways a quiet failure became a loud one

Unreleased

  • New (built, NOT live-verified): ChannelOptions.QuietVerbs β€” verbs a channel declares quiet skip the per-send/per-recv LogInfo line AND the 32-entry TraceRing write, while their COUNTERS still count (sends/recvs/drops stay in netdump). Built for streaming verbs (CompanionKit's ~5 Hz ck.proxy.pos would wrap the whole trace ring in ~6 s and destroy the cross-verb forensic window, and would drown a VerboseNet log). Pure membership decision is NetKit.Core.QuietVerbSet (ordinal, null/empty-tolerant, unit-tested).
  • New: NetKit.Views β€” a shared clone/view-hygiene API, positioned as NetKit owning the Photon half of "clean up a live clone" so consumers stop hand-rolling their own tripwires against it. Neutralize performs the always-legal PhotonView field writes (removedFromLocalViewList, viewID=0, sync mode Off, send group 254) BEFORE any destroy is attempted, so even a refused destroy can no longer let a stray view evict the real replica underneath it. VerifyClean / CountNetwork / DisarmSurvivors round out an honest report of what actually survived a strip, rather than assuming a logged destroy call succeeded.
  • New: core/NetKit.Core/ViewHygiene.cs β€” the pure compute half (ViewFacts/SurvivorFacts/ Describe/IsClean), including the one shared RefusalNote const for "Unity silently refuses this class of destroy call here" so the explanation can't drift between call sites that quote it.
  • New: NetKit.ViewLease β€” the view LIFECYCLE complement to Views (phase 2 of the hygiene migration; built, NOT live-verified): Mint/Bind allocate or adopt a viewID onto an inactive clone's own PhotonView (single-view semantics; no-view is a returned fact, never a throw β€” the consumer owns the warn-vs-refuse policy), Release/DeferRelease/SweepPending park a minted id next to its body and hand it back only once the body is destroyed (PUN warns on a live-view release), and MuteView + MutedGroup (253) stop a registered view from streaming WITHOUT deregistering it β€” deliberately the opposite doctrine of Views.Neutralize (254), the header explains why both groups coexist. One instance per consumer: the ledger, the log tag and the once-per-session latches are consumer-scoped, and every emitted line is byte-stable with the pre-migration SpawnKit implementation; consumer-dialect notices surface through callbacks.
  • New: core/NetKit.Core/ViewLease.cs β€” the pure half of the lease (LeasePolicy with the 300s age-out rule and the aged-out re-check verdict, ViewLedger/LeaseEntry the pending-release ledger, MuteResult), unit-tested for the first time incl. the byte-stable forensics dump format (ViewLeaseTests).

0.2.2 β€” 2026-08-02

  • Cloudward + NetKit: say it when a decision was downgraded
  • Cloudward + NetKit: five ways a quiet failure became a loud one
  • Docs sweep: archive, condense, and validate the whole documentation tree

0.2.1 β€” 2026-07-30

  • Session resilience: unstick verb, [LOADGATE] watchdog, hardened goto
  • Hyena/Pearlbird tuning wave: HAO taunt, gifts, bone relic, feed rule