You are viewing a potentially older version of this package. View all versions.
WUYACHIYU-WhySoLaggy-1.0.4 icon

WhySoLaggy

Find which mod causes lag and who is bombing your room. Profilers, RPC + abuse detectors, structured CSV+JSONL logs, Harmony conflict scan, JSON-driven field probe.

Date uploaded 4 days ago
Version 1.0.4
Download link WUYACHIYU-WhySoLaggy-1.0.4.zip
Downloads 983
Dependency string WUYACHIYU-WhySoLaggy-1.0.4

This mod requires the following mods to function

BepInEx-BepInExPack_PEAK-5.4.2403 icon
BepInEx-BepInExPack_PEAK

BepInEx pack for PEAK. Preconfigured and ready to use.

Preferred version: 5.4.2403

README

WhySoLaggy

Find out which mod is making your game lag, and who is bombing — or sneakily feeding — your room.

1.0.4 focuses on quiet-by-default, structured data and targeted diagnostics: profilers are off until you need them, while the network and RPC monitors preserve bounded, recent evidence for post-session analysis.


Quick start

  1. Install BepInExPack PEAK.
  2. Drop WhySoLaggy.dll into PEAK/BepInEx/plugins/.
  3. Launch the game, play normally, then exit.
  4. Open PEAK/BepInEx/ and look at:
    • WhySoLaggy.log — performance data (quiet by default, see Log Verbosity)
    • WhySoLaggy_Abuse.log — abuse alerts + RPC reports + feeding-chain traces
    • whysolaggy_data.csv — every event in 87 fixed columns
    • whysolaggy_events.jsonl — every event as JSON (one per line)
    • harmony_patches.csv — every Harmony patch in the process (written once at startup)

Zero extra setup needed. All advanced features are opt-in.


Features

Each feature has a default state. Features marked opt-in cost zero when disabled.

1. FPS Monitor — always on

  • Tracks frame time every frame; any frame exceeding SpikeThresholdMs (default 50ms) is logged as a spike and written as a SpikeFrame event.
  • Every ReportIntervalSeconds (default 10s) emits an FpsReport event with average/p95/p99 frame ms, window average, and (if memory monitor is on) AllocRateKBps.
  • Overhead: one Time.unscaledDeltaTime read per frame. Negligible.

2. Plugin Profiler — opt-in (default off in 1.0.4)

  • When EnablePluginProfiling=true, Harmony-patches Update/LateUpdate/FixedUpdate on every BepInEx plugin's MonoBehaviours and records their per-frame cost.
  • Periodic report lists the slowest plugins by average ms, plus total calls.
  • Use IgnorePluginGuids (comma-separated) to permanently mute noisy plugins. Example:
    IgnorePluginGuids = com.example.ui-mod,com.example.minimap
    
  • Why off by default: instrumenting every mod's Update adds measurable self-overhead. Turn on only when diagnosing.

3. Patch Profiler — opt-in (default off in 1.0.4)

  • When EnablePatchProfiling=true, wraps every Harmony-patched target method with prefix/postfix timing.
  • Throttling: after a method has been sampled >20 times and its average sits below MinReportMs (default 0.1ms), only every 10th call is recorded afterwards. Cuts self-overhead by ~70–90% in busy scenes.
  • IgnorePatchMethods lets you skip known noisy methods:
    IgnorePatchMethods = Player.Update,Camera.LateUpdate
    
  • Null-safe: a null __originalMethod is logged as "(unknown)" instead of throwing inside Harmony.

4. Room-Bombing Detection — always on

  • Monitors three per-second rates: Instantiate, Destroy, RPC. Also tracks absolute scene object count for spike detection.
  • When any rate exceeds its threshold, an ⚠ ABUSE ALERT is written to WhySoLaggy_Abuse.log together with the offending player's nickname + ActorNumber and the current top-5 RPC method names for context.
  • A red on-screen banner shows the alert for 12s (auto-fades). Language follows system locale (中/EN).
  • Observation only — never kicks, blocks or interferes.
  • 1.0.4 — two extra alarms layered on top (see also the Master-side diagnostics section):
    • ActorMethodHotspot — the same actor sent the same RPC ≥ ActorMethodRateThreshold times in one check window (default 20/s). Pinpoints which client is flooding which method, not just the total.
    • OwnershipGrab — one actor pulled ≥ OwnershipGrabRateThreshold PhotonView ownerships in one window (default 10/s). Surfaces silent "I own everything now" griefing patterns.
  • InstantiateTrace — every PhotonNetwork.Instantiate is prefix-hooked; first 3 calls per prefab + one sample per 5s window capture a filtered caller stack (TraceCaller, TraceStack) so you can see which mod or script is producing the spawn. An Instantiate flood automatically forces a stack capture on the next call.

Default thresholds (per second): Instantiate 15, Destroy 20, RPC 50, ObjectSpike +30, ActorMethodHotspot 20, OwnershipGrab 10.

5. Full RPC Monitor — always on

Hooks PhotonNetwork.ExecuteRpc and tracks every RPC.

  • Two buffers per method:
    • _windowByMethod — 1-second rolling count, powers the abuse alert and Top-N list
    • _watchedByMethod[m].Records — 32-entry ring buffer per watched method, survives across windows, holds full detail records
  • Built-in watched methods cover feeding / healing / status / death / revive / teleport / physics impulse / grab-kick-carry / item pickup-drop-throw / inventory / prefab spawn / end-game / ownership / tornado capture / campfire. They record sender, target GameObject + scene path, parsed arguments, payload bytes.
  • Feeding-chain tracing: SendFeedDataRPC → RemoveFeedDataRPC → GetFedItemRPC → Consume preserved in order, so you can see "who fed whom with which item".
  • Thread-safe: Photon callback work only adds records to a bounded queue. All dictionary/PhotonView work happens on the main thread, batched at PumpBatchSize (default 32/frame); when full, the oldest record is discarded.
  • Add custom watched methods without recompiling:
    [RpcMonitor]
    ExtraWatchMethods = MyModRPC_Foo,AnotherModRPC_Bar
    
  • The queue is bounded and the pump is capped, so a burst cannot create unbounded diagnostic memory use.

6. Structured Logging (CSV + JSONL) — always on

Every event — FpsReport, PluginTiming, PatchTiming, SpikeFrame, AbuseAlert, RpcCall, PeriodicReport, HarmonyPatchMap, MethodTrace, FieldProbe, InstantiateTrace, OwnershipChange — is also written to:

  • BepInEx/whysolaggy_data.csv — 87 fixed columns, Excel/Power-BI friendly
  • BepInEx/whysolaggy_events.jsonl — one JSON object per line, perfect for jq / any AI

Example whysolaggy_events.jsonl line:

{"ts":"2026-04-24T15:30:12.451","type":"RpcCall","method":"SendFeedDataRPC","sender":"PlayerB#2","targetPath":"Mushroom Lace(Clone)","payloadBytes":20,"detail":"喂食者=PlayerB#20013, 被喂者=PlayerC#30013"}

Automatic rotation: when a file exceeds MaxLogFileSizeMB (default 10MB) it's renamed with a timestamp suffix (e.g. whysolaggy_data_20260424_1530.csv) and a fresh file is created. CSV and JSONL rotation files are independently cleaned up at a maximum of 30 files and 350MB by default.

Flushes once per second, and also at periodic reports and shutdown, so a crash loses at most the current short buffer.

7. Startup Harmony Conflict Scan — always on

Once at startup, the mod snapshots every Harmony-patched method in the process to:

  • BepInEx/harmony_patches.csv — columns: TargetMethod, PatchType, OwnerHarmonyId, Priority
  • structured log (HarmonyPatchMap events)

If the same target method is patched by two or more different Harmony IDs, a warning lands in WhySoLaggy_Abuse.log and on the dashboard. This instantly surfaces silent mod conflicts.

Sample harmony_patches.csv row:

CharacterAfflictions.AddStatus,Prefix,com.wuyachiyu.Lantern,400
CharacterAfflictions.AddStatus,Prefix,com.otherauthor.SomeMod,400   ← CONFLICT

Zero runtime cost after the initial scan.

8. Method Stack Tracer — opt-in (default disabled)

Attach a stack-capture prefix to arbitrary methods without writing code.

[MethodTracer]
TraceMethodNames = Player.Update,ColdComponent.Apply
TraceMaxDepth = 5
TraceRateLimit = 100

For each matched invocation the tracer records timestamp, the top N frames (Unity / HarmonyLib / MonoMod frames filtered out), the immediate caller, and a short argument summary — written as a MethodTrace event.

Per-method rate limiter prevents high-frequency methods from flooding the log; the first overflow logs a single warning and the rest are silently dropped. Empty TraceMethodNames = hook never installed, zero cost.

9. FieldProbe — opt-in (default disabled), JSON-driven

The FieldProbe feature can reflectively snapshot any field, parameter or return value at any method, controlled entirely by a JSON rules file. Useful when you need to prove or disprove a specific hypothesis (e.g. "is this Cold-status change going through AddStatus or SetStatus?") without recompiling.

Enable:

[FieldProbe]
EnableFieldProbe = true
RulesFile = WhySoLaggy.fieldprobe.json   # resolved under BepInEx/config/
DefaultRateLimit = 60
DefaultMaxValueLen = 128
DefaultIncludeStack = false
DefaultStackMaxDepth = 5

Rules file schema (place the file as BepInEx/config/WhySoLaggy.fieldprobe.json):

{
  "_schema": "WhySoLaggy.FieldProbe v1",
  "_doc": "target=Type.Method; fields roots: __instance / __argN / __args / __result / __exception / TypeName; operators .Member ?.Member [N] .Count .Length",

  "enabled": true,
  "rateLimitPerRule": 60,
  "maxValueLen": 128,
  "includeStack": false,
  "stackMaxDepth": 5,

  "rules": [
    {
      "note": "Check which lantern prefab passes the slot filter",
      "target": "LanternHelper.FindLitLanternSlot",
      "fields": [
        "__arg0?.name",
        "__result",
        "__result?.prefab?.name"
      ],
      "includeStack": true,
      "rateLimit": 30
    },
    {
      "note": "Milk-invincibility: is the short-circuit firing?",
      "target": "CharacterAfflictions.AddStatus",
      "fields": [
        "__arg0",
        "__arg1",
        "__instance.character.data.isInvincibleMilk",
        "__result"
      ],
      "rateLimit": 120
    },
    {
      "note": "Which StatusField is applying Cold?",
      "target": "StatusField.Update",
      "fields": [
        "__instance.gameObject.name",
        "__instance.statusType",
        "__instance.statusAmountPerSecond"
      ],
      "rateLimit": 10
    }
  ]
}

Expression DSL:

Root Meaning
__instance Harmony __instance (the this)
__arg0, __arg1, … Positional parameters
__args Full argument array summary
__result Return value (postfix)
__exception Caught exception (postfix, may be null)
SomeTypeName Fully-qualified type name to reach statics

Operators: .Member (field/property), ?.Member (null-safe), [N] (array/list index), .Count, .Length.

Per-rule knobs: rateLimit, maxValueLen, includeStack, stackMaxDepth, enabled, note. All optional — fall back to the global defaults above.

Each invocation emits one FieldProbe event containing the target name and every expression's evaluated value (or an error tag). Set "enabled": false on a rule to keep it in the file but skip it.

A full working sample (used to diagnose the Faerie-Lantern and Milk-Invincibility bugs during development) ships as WhySoLaggy.fieldprobe.json in the mod source; copy it into BepInEx/config/ and tweak as needed.

Zero overhead when EnableFieldProbe=false.

10. On-Screen Dashboard — opt-in (default off)

Set [UI] ShowDashboard = true to draw a draggable GUILayout window showing:

  • live FPS, frame time, window-average frame ms
  • GC allocation rate (KB/s, if memory monitor is on)
  • current watched-RPC summary
  • most recent abuse alert

When off, the entire IMGUI draw path is skipped — no cost.

11. GC Allocation + Ping — always on

  • EnableMemoryMonitor (default on): samples Profiler.GetTotalAllocatedMemoryLong() once per second, exposes the rate as AllocRateKBps on every FpsReport. Tells you at a glance whether a spike is CPU-bound or GC-bound.
  • Periodic reports + AbuseAlert events also include PhotonNetwork.GetPing(), so you can correlate lag with network RTT.

12. Log Verbosity — Minimal by default in 1.0.4

[Logging]
LogVerbosity = Minimal   # options: Minimal | Normal
Level WhySoLaggy.log WhySoLaggy_Abuse.log
Minimal (default) nothing abuse alerts only (alerts always land, even in Minimal)
Normal + periodic FPS/RPC reports + periodic RPC reports, feeding-chain detail

The CSV/JSONL structured logs are never gated by verbosity — they always capture everything for post-hoc analysis. Verbosity only affects the human-readable text logs.

13. Master-side Relay Diagnostics — always on (1.0.4, beta)

PEAK runs a Master-relay model: every client RPC goes client → Master → targets, so on clients the visible sender is almost always the Master. On a Master install, WhySoLaggy unpacks the Photon event payload itself to recover the real origin:

  • RemoteRpcTrace remains as a compatibility enum value, but watched RPCs are written as RpcCall in 1.0.4.
  • Master-side SuspectedRequester* — when PhotonNetwork.Instantiate fires on the Master, the tracer can correlate the most recent client-side request and write requester fields into the InstantiateTrace row.
  • OwnershipChange — decodes EventCode 209 (request), 210 (transfer), and 212 (batch update). Request and transfer counts are kept separate; batch updates are audited but do not enter rate thresholds.
  • ActorMethodHotspot — per-actor per-method RPC counter with its own fast window (CheckInterval) and slow window (ReportInterval). Top-N Actor×Method rows are added to every periodic report.

Master-side requester and ownership attribution remains environment-dependent and should be treated as diagnostic evidence to correlate with the host session. On a client install, unavailable relay fields remain dormant.


Output files

All files live under PEAK/BepInEx/.

File When written Contents
WhySoLaggy.log session Human-readable perf log (gated by LogVerbosity)
WhySoLaggy_Abuse.log session Human-readable abuse + RPC log (gated by LogVerbosity)
whysolaggy_data.csv session 87-column event stream, auto-rotated at MaxLogFileSizeMB
whysolaggy_events.jsonl session JSON-per-line event stream, auto-rotated
harmony_patches.csv startup once Every Harmony patch in the process

Configuration (key items)

Open BepInEx/config/com.wuyachiyu.WhySoLaggy.cfg after first launch (or use any ModConfig UI).

[General]

Key Default Description
SpikeThresholdMs 50 Frame ms counting as a spike (16–200)
ReportIntervalSeconds 10 Seconds between FpsReport (5–60)
EnablePluginProfiling false Per-plugin Update timing. Opt-in.
EnablePatchProfiling false Per-patch method timing. Opt-in.
TopMethodCount 10 Top-N for perf reports (3–30)
MinReportMs 0.1 Patch-profiler low-cost filter
IgnorePluginGuids (empty) CSV of plugin GUIDs to skip
IgnorePatchMethods (empty) CSV of Type.Method names to skip
EnableMemoryMonitor true Sample GC alloc rate

[AbuseDetection]

Key Default
EnableAbuseDetection true
CheckIntervalSeconds 1.0
ReportIntervalSeconds 30.0
InstantiateRateThreshold 15
DestroyRateThreshold 20
RpcRateThreshold 50
ObjectSpikeThreshold 30
ActorMethodRateThreshold 20
OwnershipGrabRateThreshold 10
OwnershipRequestRateThreshold 20
AlertCooldownSeconds 10

[RpcMonitor]

Key Default Description
EnableRpcMonitor true
TopMethodCount 10 Top-N in periodic report
WatchedRecordPerMethodCapacity 32 Ring buffer size per watched method
WatchedShowPerMethod 6 Records printed per watched method in report
ExtraWatchMethods (empty) CSV of extra method names to watch
PumpBatchSize 32 Queue items consumed per frame
QueueCapacity 2048 Maximum queued records; oldest records are discarded first

[Logging]

Key Default
LogVerbosity Minimal
MaxLogFileSizeMB 10
MaxRotatedFiles 30
MaxRotatedStorageMB 350

[UI]

Key Default
ShowDashboard false

[MethodTracer]

Key Default
TraceMethodNames (empty)
TraceMaxDepth 5
TraceRateLimit 100

[FieldProbe]

Key Default
EnableFieldProbe false
RulesFile WhySoLaggy.fieldprobe.json
DefaultRateLimit 60
DefaultMaxValueLen 128
DefaultIncludeStack false
DefaultStackMaxDepth 5

Upgrading from an older version? BepInEx only applies new default values when a key is missing. Existing values are preserved. Delete com.wuyachiyu.WhySoLaggy.cfg only if you want a clean configuration, or add the new keys manually.


How to analyse the logs

The recommended workflow is tag-first: every event (native WhySoLaggy or any mod cooperating with it, e.g. LanternShootZombiesNight) now lands with a bracketed tag like [LitSync], [WARMTH_LOG], [FuelMath], [RPC_MON], ⚠ ABUSE ALERT. Slice by tag first, only then zoom out.

Option A — grep / jq by tag (recommended)

Fastest path for single-question diagnostics. One pass, zero tooling beyond the shell.

Text logs (Windows PowerShell / rg / grep):

# every abuse alert in this session
Select-String -Path BepInEx\WhySoLaggy_Abuse.log -Pattern '⚠ ABUSE ALERT'

# every lantern lit-state change on the host
Select-String -Path BepInEx\LogOutput.log        -Pattern '\[LitSync\]'

# every failed warmth tick on the client
Select-String -Path BepInEx\LogOutput.log        -Pattern 'WARMTH_LOG.*FAILED'

Structured logs (whysolaggy_events.jsonl + jq):

# top RPC senders
jq -r 'select(.type=="RpcCall") | .sender' whysolaggy_events.jsonl | sort | uniq -c | sort -rn

# all FieldProbe hits for one rule
jq 'select(.type=="FieldProbe" and .target=="StatusField.Update")' whysolaggy_events.jsonl

# frame spikes worse than 100ms
jq 'select(.type=="SpikeFrame" and .frameMs>100)' whysolaggy_events.jsonl

Option B — open the CSV in Excel / Power BI / DuckDB

whysolaggy_data.csv has 87 fixed columns. Pivot by type, filter by method / sender, chart frameMs over time. Great for multi-session comparison and for sharing one screenshot with teammates.

Option C — hand a curated slice to an AI

Still useful, but only after you've narrowed the file down. Dumping a full multi-MB .log into a chat wastes tokens and buries the signal. Typical recipe:

  1. Select-String / jq to extract the 50–500 lines around the incident.
  2. Paste that slice plus this prompt:

    "Lines below are tagged PEAK session logs ([LitSync] = lantern sync, [WARMTH_LOG] = warmth tick, ⚠ ABUSE ALERT = suspected flood). Tell me what went wrong, for which viewId, and in what order."

  3. JSONL paste-through works the same — each line is already a self-describing object.

Option D — read the plain-text log yourself

  • WhySoLaggy.log: lines with ! or !!! mark spikes; the ms number shows severity.
  • WhySoLaggy_Abuse.log:
    • ⚠ ABUSE ALERT — suspect nickname + ActorNumber follow
    • [RPC_MON] — periodic RPC stats + watched detail
    • SendFeedDataRPC detail: 喂食者=X#123 → 被喂者=Y#456, 物品=Z means X fed Y with Z
  • Any cooperating mod log (e.g. LogOutput.log with lantern tags): search by [TagName] — see the real-world case section below for what the tags look like in practice.

Sample logs — real data (names redacted)

Real snippets from a 6-player session. Nicknames replaced with PlayerAPlayerG (PlayerA = Master). The buffer format has since shifted to per-method rings, but the information shown is the same.

Startup

[21:47:25.473] [RPC_MON] Hooked PhotonNetwork.ExecuteRpc successfully
[21:47:25.473] [RPC_MON] RpcMonitor initialized (watched methods: 27)   # historical snippet — 1.0.3 ships 65+

Abuse alert with suspect + top RPC methods

[21:47:44.510] ⚠ ABUSE ALERT: RPC flood! Rate: 85.0/s (threshold: 50/s)
[21:47:44.511] [ABUSE]   Top RPC sources (by ActorNumber):
          PlayerA#1: 42x
          PlayerB#2: 10x
          PlayerC#3: 8x
[21:47:44.511] [RPC_MON]   Current-window top RPC methods:
          SyncInventoryRPC: 25x
          SetItemInstanceDataRPC: 9x
          SetCharacterIdle_RPC: 6x

30s periodic report — top methods with payload + top senders

[21:47:55.246] [RPC_MON]   Top 10 RPC methods this period:
          SyncStatusesRPC: 82x  avg=72B max=72B total=5904B  [PlayerB#2:28, PlayerA#1:15, PlayerE#7:15]
          SyncInventoryRPC: 76x  avg=59B max=83B total=4531B  [PlayerA#1:56, PlayerE#7:5, PlayerB#2:5]
          ReceivePluginsFromHostRPC: 9x  avg=1392B max=1392B total=12528B  [PlayerA#1:9]

Watched method detail — sender, target GameObject path, parsed args

[37.6s] PlayerA#1 → RPC_SetThrownData  payload=24B
   target=(PlayerA#187)  path=C_Pawn W(Clone)
   detail=投掷者=PlayerA#10002, 力度=0.00

Feeding-chain trace — "who fed whom with which item"

[668.3s] PlayerC#3 → Consume  payload=20B
   target=(PlayerC#30040)  path=Mushroom Lace(Clone)
   detail=消耗者=PlayerC#30013, 物品=Mushroom Lace(Clone)#30040
[668.3s] PlayerC#3 → RemoveFeedDataRPC  payload=20B
   target=(PlayerC#30040)  path=Mushroom Lace(Clone)
   detail=喂食结束: 喂食者=PlayerC#30013

In each trace above the feeder viewID equals the consumer viewID — everyone ate their own food, no sneaky cross-player feeding. If someone ever feeds someone else, the two IDs differ and the anomaly stands out immediately.


Real-world case: lantern lit-sync + warmth diagnostics (dual-client)

Pulled from a co-op session between a Host (IsMasterClient=True) and a Client (IsMasterClient=False) running LanternShootZombiesNight on top of WhySoLaggy. The lines below come straight from BepInEx/LogOutput.log; viewId and lanternInstID are Photon/Unity internal IDs (not player-identifying) and are kept verbatim. No nicknames appear in these tags.

These tags are emitted by the lantern mod itself — WhySoLaggy's job here is to make them structured (CSV/JSONL, auto-rotated, AI-friendly) and to offer FieldProbe as a zero-recompile way to add more of them against any Type.Method target.

Host side — [LitSync] proves both sync channels fire

Channel 1 (LightLanternRPC) and Channel 2 (SetItemInstanceDataRPC → OnInstanceDataSet) each carry their own tag; the follow-up lit CHANGED line confirms the field actually flipped and the VFX GameObject (lightGO.active) matches.

[LitSync] OnInstanceDataSet: viewId=10007, FlareActive=False, lit=False, isMine=True
[LitSync] LightLanternRPC RECEIVED: viewId=10007, lit=True,  isMine=True, lightGO.active=False
[LitSync] lit CHANGED:           viewId=10007, False→True,   isMine=True, lightGO.active=True
[LitSync] LightLanternRPC RECEIVED: viewId=10007, lit=False, isMine=True, lightGO.active=True
[LitSync] lit CHANGED:           viewId=10007, True→False,   isMine=True, lightGO.active=False

Reading the trio: the RPC arrives → lit flips → lightGO.active flips the same frame. If OnInstanceDataSet ever fires with FlareActive=True but lit stays False, that's a sync bug; if lit=True but lightGO.active=False, the VFX layer is desynced.

Client side — warmth lifecycle readable at a glance

[HuddleWarmth] STARTED: nearby=1, multiplier=0.5x, warmth=2.5s, interval=7s
[WARMTH_LOG]   source=HuddleWarmth | nearby=1 | warmth=+2.5s | result=FAILED(no lit lantern)
[HuddleWarmth] STOPPED: nearby=0 < min=1
[HuddleWarmth] STARTED: nearby=1, multiplier=0.5x, warmth=2.5s, interval=7s
[FuelMath]     currentFuel=118.484, delta=2.500, rawNew=120.984, maxFuel=120.000, overflow=0.984, lanternInstID=-45672
[WARMTH_LOG]   source=HuddleWarmth | nearby=1 | multiplier=0.5x | warmth=+2.5s | interval=7s | result=SUCCESS

Three states in ten lines: first tick fails because the client hadn't lit the lantern yet (FAILED(no lit lantern)); the pair then breaks radius and the tracker correctly STOPPED; once they regroup and the lantern is lit, [FuelMath] quantifies the cap-waste (overflow=0.984) and the next WARMTH_LOG shows result=SUCCESS.

Client side — alternative warmth source with distance + overflow

[FuelMath]   currentFuel=116.986, delta=5.000, rawNew=121.986, maxFuel=120.000, overflow=1.986, lanternInstID=-46484
[WARMTH_LOG] source=BugleRestore | warmth=+5.0s | dist=9.3m  | result=SUCCESS
[FuelMath]   currentFuel=120.000, delta=5.000, rawNew=125.000, maxFuel=120.000, overflow=5.000, lanternInstID=-47052
[WARMTH_LOG] source=BugleRestore | warmth=+5.0s | dist=10.3m | result=SUCCESS

The source= + dist= + overflow= triad makes every warmth event post-hoc auditable: which mechanic fired it, how far the recipient was, and how much fuel (if any) was wasted at the cap. Pair this with WhySoLaggy's whysolaggy_events.jsonl and you can replay a full session in jq, Excel, or an AI chat without ever re-running the game.

Want these exact fields without touching source? A FieldProbe rule against LanternHelper.AddPlayerLanternFuel / HuddleWarmth.Update / StatusField.Update produces one FieldProbe event per call with the same level of detail — shipped through the same CSV/JSONL pipeline.


Installation

  1. Install BepInExPack PEAK (v5.4.2403+).
  2. Drop WhySoLaggy.dll into PEAK/BepInEx/plugins/.
  3. (Optional) For FieldProbe, place your WhySoLaggy.fieldprobe.json under PEAK/BepInEx/config/ and set EnableFieldProbe=true.

No other mods required.


Deployment: Master vs Client

WhySoLaggy works on any install, but who sees what differs between a host (Master Client) and a regular client. Quick guide:

Feature Client install Master install
FPS / SpikeFrame / GC alloc rate ✅ full ✅ full
Plugin / Patch Profiler ✅ full (local plugins) ✅ full (local plugins)
Local Instantiate / Destroy / RPC rate ✅ full ✅ full
InstantiateTrace (caller stack) ✅ full (local spawns) ✅ full (all spawns pass here)
Full RPC Monitor + watched records ✅ full (sender usually = Master) ✅ full (sender = real actor)
FieldProbe / MethodTracer / Harmony scan ✅ full ✅ full
Abuse alerts + red on-screen banner ✅ full ✅ full
On-Screen Dashboard ✅ full ✅ full
RemoteRpcTrace (real client actor) ⚪️ dormant 🔍 Master-only, beta
Instantiate SuspectedRequester* ⚪️ dormant 🔍 Master-only, beta
OwnershipChange / OwnershipGrab alarm ⚪️ dormant 🔍 Master-only, beta
ActorMethodHotspot alarm 🟡 counts but sender = Master 🔍 full Actor×Method, beta

TL;DR

  • Client-only install — you get the full performance lab, local-spawn forensics and RPC sample buffer; the "real client sender" stays hidden because Master has already rewritten it.
  • Master install — you additionally get the four relay-decoding diagnostics that pinpoint which client sent the RPC, grabbed the ownership or triggered the Instantiate. These are flagged beta until a hosted-session capture confirms them.
  • Dual install (both players run the mod) — ideal for forensics. Correlate the client's InstantiateTrace.TraceStack with the host's RemoteRpcTrace.SenderActor by viewID and you get a full end-to-end trail.

All Master-only paths are null-safe and wrapped in try/catch; on a client install they short-circuit at the IsMasterClient check and consume no CPU.


Performance baseline (1.0.4)

Measured on an Intel i5-12400 @ 4.4 GHz, PEAK 6-player session. All numbers are amortised per second unless noted.

Scenario CPU (ms/s) GC (KB/s) Disk (KB/s) Resident (KB)
Normal co-op, default config 1.0–1.5 2–4 1–2 ≈ 450
Item-spam session (15–30 Inst/s) 2.5–4.0 4–8 3–6 ≈ 600
Extreme RPC flood (≥ 300/s) 6–10 8–15 10–20 ≈ 800
With EnablePluginProfiling=true +3–6 ms/s +1–2
With EnablePatchProfiling=true +2–4 ms/s (throttled) +1
With ShowDashboard=true +0.3–0.6 ms/s negligible

Hot path breakdown (default config, normal session):

Component Share Notes
RpcMonitor main-thread Pump ≈ 40 % 32 items/frame, lock-free queue
StructuredLogger CSV+JSONL writer ≈ 25 % flush every second, 10MB rotation
NetworkAbuseDetector OnEvent ≈ 20 % counters + whitelist lookups
FPS tracker + memory sampler ≈ 10 % one unscaledDeltaTime / frame
Everything else (dashboard, FieldProbe idle, …) ≈ 5 %

Opt-in modules that truly cost zero when off: PluginProfiler, PatchProfiler, PerformanceDashboard, MethodTracer, FieldProbe — their hooks/IMGUI paths are not installed unless the corresponding config flag is true.

Throughput ceiling: the CSV+JSONL writer has been benchmarked at ≈ 8,000 events/s sustained before the flush queue starts lagging. In normal play the observed rate is < 50/s; extreme floods peak around 500/s.

Keep the default config on for 7×24 captures — the overhead is lower than a single cosmetic mod's Update loop. Only flip EnablePluginProfiling / EnablePatchProfiling on when actively hunting a specific offender.


Notes

  • The mod waits ~5s after launch before hooking, so other mods can finish loading first.
  • All advanced features (Plugin/Patch Profilers, Dashboard, MethodTracer, FieldProbe) are off by default in 1.0.4. Flip them on only when you need them.
  • Observation-only: nothing is kicked, blocked or modified in the simulation. Perfect for post-incident evidence.
  • Net overhead in default config is lower than 1.0.2 thanks to the Minimal verbosity + quiet profilers.

⚠ Testing status (1.0.4)

  • The 1.0.4 client-side functionality (FPS monitor, RPC monitor, local Instantiate/Destroy/RPC rates, FieldProbe, MethodTracer, Harmony conflict scan, structured logging) is fully exercised.
  • A subset of new diagnostics can only do real work when the install is on the Master Client:
    • OnRemoteRpcEvent Hashtable unpack — captures the real client Actor behind a Master-relayed RPC
    • Master-side Instantiate requester correlation — ties a PhotonNetwork.Instantiate back to the client RPC that most likely triggered it (SuspectedRequesterActor/Name/Rpc)
  • PhotonView Ownership audit — EventCode 209 / 210 / 212
    • Actor×Method hotspot detector — per-actor per-method RPC rate alarm
  • Those Master-only paths compile cleanly, self-check without exceptions and the code has been reviewed, but have not yet been exercised in an actual hosted session. Treat their output as beta until a host-side capture confirms them; client-side observations are unaffected.
  • If you host a session with this build, send the resulting whysolaggy_events.jsonl and filter type=OwnershipChange / InstantiateTrace / RpcCall when comparing client and Master evidence.

Contact & feature requests

  • Author: 乌鸦吃鱼
  • QQ Group: 1093172647

Spotted an RPC that looks suspicious but isn't on the watched list yet? Drop the method name in the QQ group — if it makes sense I'll add it in the next update. Or add it yourself via ExtraWatchMethods without waiting.

CHANGELOG

Changelog

[1.0.4] - 2026-08-30

WhySoLaggy 1.0.4 is the PEAK 2.3.a-compatible maintenance release.

Detection and attribution

  • Corrected PhotonView Ownership event classification: Request 209, Transfer 210, and batch Update 212; vacant ViewId event 211 is ignored.
  • Rate thresholds now attribute inbound Instantiate, Destroy, RPC, Ownership Request, and Ownership Transfer activity to remote actors. Batch Ownership updates remain audit-only.
  • Added alert cooldown and per-key suppression summaries so repeated warnings do not flood text logs or the on-screen notification.
  • Kept local and remote totals separate in periodic reports; malformed network payloads are warning-only.

RPC and structured logging

  • Added a bounded RPC queue with QueueCapacity=2048; when full, the oldest records are discarded and recent evidence is retained.
  • Added queue sequence, enqueue timestamp, enqueue frame, queue delay, runtime queue metrics, and flush timing to structured output.
  • Removed duplicate watched-RPC trace output. Each watched RPC produces one complete RpcCall record; RemoteRpcTrace remains only as a compatibility enum value.
  • Structured logging now buffers in memory and flushes once per second, at reports, and during shutdown.
  • CSV and JSONL rotation files are independently retained by count and total size (30 files / 350MB by default), without deleting text logs.

Profiler and compatibility fixes

  • Grouped FieldProbe rules by target method so each method receives at most one Prefix, Postfix, and Finalizer patch.
  • PatchProfiler records actual calls and uses sampled averages only for periodic total estimates; frame spikes remain precisely timed.
  • Method keys include the full type, method, and parameter signature while accepting legacy Ignore entries.
  • Zombie counts use ZombieManager.Instance.zombies.Count with one-time failure warnings per plugin lifetime.
  • Added safe bilingual ModConfig display for the WhySoLaggy title, all sections, option names, descriptions, and enum values. Persisted section/key names and values remain unchanged.
  • All monitor modules support idempotent shutdown and reload; the plugin unpatches its own Harmony patches before closing logs.

[1.0.3] - 2026-04-26

⚠ Testing status — client-side only. Every 1.0.3 feature in this changelog was exercised from a non-host (client) install. A subset of the new diagnostics can only do real work when the install is on the Master Client — specifically:

  • OnRemoteRpcEvent Hashtable unpack (capturing real client-side RPC senders after Master relay)
  • Master-side Instantiate requester correlation (SuspectedRequesterActor/Name/Rpc)
  • PhotonView Ownership audit (EventCode 210/211/215)
  • Actor×Method hotspot detector

These Master-only paths compile cleanly and self-check, but have not yet been validated in an actual hosted session. Expect potential edge-case bugs there until a host-side capture is recorded. Client-side features (FPS / RPC monitor / Instantiate rate / FieldProbe etc.) are unaffected.

Quiet-by-default

  • EnablePluginProfiling / EnablePatchProfiling default changed to false — profilers are opt-in now
  • LogVerbosity default changed to Minimal — text logs only carry abuse alerts; CSV/JSONL still capture everything
  • Fix: abuse alerts now bypass the verbosity filter, so Minimal = "alerts only, but definitely alerts"
  • Fix: removed orphan continuation lines when a report header was filtered but its follow-up lines weren't

New features

  • Structured logging: whysolaggy_data.csv (58 cols) + whysolaggy_events.jsonl, auto-rotated at MaxLogFileSizeMB
  • Startup Harmony conflict scan: harmony_patches.csv + alerts when the same method is patched by multiple mods
  • Opt-in method stack tracer ([MethodTracer]): name methods, get filtered call stacks per hit
  • FieldProbe: JSON-driven reflective snapshots of any field / parameter / return value at any method
  • On-screen dashboard (opt-in): live FPS, frame time, GC KB/s, watched-RPC summary, last abuse alert
  • GC allocation rate + Photon ping are attached to FPS / periodic reports
  • InstantiateTrace: every PhotonNetwork.Instantiate captures a filtered caller stack (3 samples per prefab + 1 per 5s window, auto-forced on flood) — finally answers "which script / mod is spawning this?"
  • Watched-method library expanded from 27 to 65+ RPC names across feeding / healing / status / death / revive / grab-kick-carry / inventory / prefab spawn / end-game / ownership / tornado / campfire
  • Master-side Relay Diagnostics (beta, Master-only): RemoteRpcTrace unpacks EventCode=200 Hashtable to recover the real client sender; Instantiate rows gain SuspectedRequesterActor/Name/Rpc/AgeMs; OwnershipChange decodes EventCode 210/211/215; OwnershipGrab and ActorMethodHotspot alarms layer on top

Performance & robustness

  • PatchProfiler throttling via MinReportMs — ~70–90% lower self-overhead on busy scenes
  • IgnorePluginGuids / IgnorePatchMethods / ExtraWatchMethods config keys
  • RPC hot path moved fully to main-thread pump (ConcurrentQueue + PumpBatchSize)
  • Lower GC pressure across all periodic report paths; better exception reporting; null-safe patch hooks

Docs

  • README: new Real-world case section with redacted dual-client lantern lit-sync + warmth diagnostics, pulled straight from a live session ([LitSync] / [HuddleWarmth] / [FuelMath] / [WARMTH_LOG] / BugleRestore)
  • README: reworked How to analyse the logs into a tag-first workflow — grep/jq by tag now recommended, CSV pivot second, AI third (with "curate the slice first" recipe), plain-text reading fourth
  • README: added six copy-paste commands (3 PowerShell Select-String, 3 jq) covering abuse alerts, lit-sync, failed warmth ticks, top RPC senders, FieldProbe hits, frame spikes

Upgrade note: BepInEx only applies new defaults to missing keys. Delete com.wuyachiyu.WhySoLaggy.cfg or flip the three keys manually to pick up the quieter defaults.

[1.0.2] - 2026-04-23

  • Full RPC monitor hooks PhotonNetwork.ExecuteRpc; counts calls, senders and payload bytes
  • Feeding-chain tracing: SendFeedDataRPC → RemoveFeedDataRPC → GetFedItemRPC → Consume
  • 27 watched high-risk methods record sender / target / parsed args / payload size
  • Per-method 32-entry ring buffer so floods can't drown out rare events
  • Abuse alerts now include the current top RPC methods
  • Hot path <500ns per RPC, no allocs, no locks

[1.0.1] - 2026-04-20

  • Room-bombing detection now correctly identifies the remote suspect
  • Real-time on-screen alert banner (auto-fades after 12s), auto EN/中 based on system locale
  • Fixed wrong attribution to the local player; fixed a thread-safety crash

[1.0.0] - 2026-04-07

  • Initial release: FPS tracker, plugin / patch profilers, room-bombing detection, dual text logs