Files
Project-M/Docs/Vault/07_Sessions/2026/2026-06-10_MC0_TuningConfig_LiveTuning.md
T
2026-06-10 15:22:30 -07:00

7.8 KiB
Raw Blame History

date, type, tags, permalink
date type tags permalink
2026-06-10 session
session
combat
mc-0
tuning
netcode
dev-tools
gamevault/07-sessions/2026/2026-06-10-mc0-tuningconfig-live-tuning

MC-0 completion — TuningConfig live-tuning singleton (dash/Charger knobs, no recompile)

Closes the last MC-0 gap flagged in 2026-06-09_MC1_Implementation ("the TuningConfig live-singleton still does not exist — dash/Charger knobs are baked consts"). Direction: Path_to_Fun (MC-0) · locks: DR-029_Path_A_Fork_Locks. Purpose: make the 2026-06-09_MC1_Build_Spec fun-gate a playtest→nudge→replay loop instead of edit→recompile→replay. All three gates passed (compile clean · 267/267 EditMode · live server==client round-trip). The MC-1 feel fun-gate is still the open operator item — this only removes the recompile friction from it.

What was built

A per-world TuningConfig IComponentData singleton holding 10 live feel knobs, mirroring the proven DevTelemetry dev-tools pattern (singleton + scalar DebugCommandRequest RPC + a broadcast report + a client readout static). Zero ghost-hash change, no re-bake; fully editor-strippable (types unconditional for RpcCollection hash parity, all systems #if UNITY_EDITOR).

  • TuningConfig + DebugTuningReport + TuningKnob (Simulation/Debug/TuningConfig.cs, UNCONDITIONAL types): 10 floats (dash: distance/iframe/recover/cooldown/sharpness; Charger: windup/lunge-speed/lunge-dur/whiff-stagger; + Grunt windup). Defaults() is the single source of truth == the historical baked consts; GruntWindupTicks references Tuning.AttackWindupTicks (not a duplicate literal — Tuning.AttackWindupTicks stays alive for TelegraphTests). Apply/Get/ClampKnob switch on a byte index (no enum on a Bursted/RPC path).
  • TuningBroadcastSystem (Server, editor-only): ensures+seeds the server singleton to Defaults() in OnCreate; every 15 ticks broadcasts the FULL config to every connection (load-bearing for MPPM thin clients + late joiners — they have no overlay, so the report is their only path to tuned values).
  • DevTuningReceiveSystem + TuningReadout (Client, editor-only): drains the authoritative report into the readout, then writes the client TuningConfig singleton from it each tick (so the PREDICTED DashSystem reads live values). The overlay mutates the readout optimistically via SetLocal (instant feel for the tuner; eventually consistent with the server within ~1 RTT).
  • DebugOp.SetTuning = 12 + the server handler (TuningConfig.Apply on the server singleton) + DebugCommandSendSystem.SetTuning(knob, value) (×1000 fixed-point over the int ArgB) + a collapsible, scroll-wrapped Tuning section in DebugOverlay (per-knob /+ rows; each nudge fires the authoritative RPC AND the optimistic local apply).
  • Consumers repointed: DashSystem (predicted) + EnemyAISystem (server-only Charger/Grunt) read the singleton via TryGetSingleton ? : Defaults() — so release builds fall back to Defaults() == today's consts, behaviour unchanged. The 5 dash consts and 4 Charger consts were deleted (now in Defaults()); the 7 Charger read-sites kept their names (locals shadow the old const names) so only the const declarations changed.

The mandatory pre-code design review (operator-forced, per the standing rule)

Operator chose "run the design review first" over "build it now" (honoring validate-netcode-design-before-coding / DR-029_Path_A_Fork_Locks's ritual). A 3-lens adversarial Workflow (netcode/determinism · reuse/scope/Burst · edge-cases, each finding adversarially refuted vs ground-truth code) → 15 findings, 1 confirmed, 14 refuted.

  • CONFIRMED — MAJOR (fixed): dash-speed divide-by-zero → PERMANENT NaN. DashSystem computes dashSpeed = DashDistance / (IFrameWindowTicks/60) at a read site distinct from the window math. The spec only guarded the window, not the divisor — and the optimistic SetLocal path bypasses Apply's clamp, so nudging the i-frame-window knob to 0 → 0 denominator → MoveVelocity NaN → the kinematic body's LocalTransform.Position goes NaN and stays NaN forever (not self-correcting; corrupts the replicated transform — bricks the player). Fix: clamp at the read site (uint iFrameTicks = (uint)math.max(1f, t.IFrameWindowTicks)) used for BOTH the window and the divisor, AND clamp in Apply + SetLocal (defense in depth). Pinned by TuningConfigTests.Apply_Clamps_Tick_Knobs_To_At_Least_One + the golden-defaults test.
  • Hardening folded in (several refuted findings converged): Apply/SetLocal clamp all tick knobs ≥1 and value knobs ≥0; Charger read-sites guard with (uint)math.max(1,…); do NOT delete Tuning.AttackWindupTicks (a reviewer caught TelegraphTests.cs:73 references it → Defaults().GruntWindupTicks references it instead); a golden test pins Defaults() to the historical consts.
  • Architecture validated (no change): unconditional types / no re-bake confirmed; the broadcast is genuinely load-bearing for MPPM thin clients (not redundant); the knob-change transient mis-predict is acceptable for a dev tool; within one client's rollback re-sim the singleton is per-tick-constant (mutated ≤once/tick) so per-tick determinism holds.

Validation (all 3 gates)

  • Compile: clean — 0 errors / 0 warnings (Bursted DashSystem/EnemyAISystem reading the singleton compiled fine; no enum-on-Burst hazard, byte knob map).
  • EditMode: 267/267 green (was 259; +8 TuningConfigTests: golden Defaults()==consts incl. GruntWindupTicks==Tuning.AttackWindupTicks, Apply index-map, tick/value clamps, out-of-range no-op, ×1000 wire round-trip, ToReport/FromReport round-trip, and a world test proving DashSystem reads a non-default singleton — IFrameUntilTick = now+20 with a tuned window, not Defaults' 12). The pre-existing DashSystemTests (asserting 112/121/145/200/20 with no singleton) double as the behaviour-preserving guard on the Defaults() fallback path.
  • Live Play (real netcode session, server+client): both worlds create+seed TuningConfig to Defaults() (iframe 12 / dist 4 / chWindup 30 / lungeSpd 16 / grunt 18); a SetTuning RPC (iframe 12→18, chargerWindup 30→40) applied on the server AND converged on the client via the broadcast, untargeted knobs unchanged; zero console errors/exceptions (only benign Server-Tick-Batching warnings from in-editor host load) — no Burst stale-binary / undeclared-component throw.

Notes / deviations (deliberate)

  • Broadcast is every 15 ticks unconditionally (full state, not a delta) — simplest robust convergence for late joiners / thin clients; ~16 bytes/0.25 s on an editor-only loopback is negligible.
  • Charger knobs are server-only consumed (clients never simulate Chargers) but are broadcast anyway for uniformity + overlay display.
  • A mid-dash knob change desyncs the in-flight dash's baked window-length from its live recomputed speed for ONE dash (cosmetic, self-corrects next dash) — acceptable per the review.

Open items (operator)

  • The MC-1 fun-gate is still the open gate — now unblocked for fast live tuning: open DEV ▲ → "- Tuning (MC-0) -", nudge dash/Charger knobs while playing, read the live counters in "- Telemetry (MC-0) -". MC-1 is NOT "done" until the feel pass + bench (timed vs spam ≥70% fewer hits) + friend read pass; MC-1 is the project kill-switch.
  • After MC-1 passes → MC-4 (melee cleave) is the next committed code milestone (Path_to_Fun).
  • Assets/_Recovery/0.unity untracked artifact still pending review/delete (carried from 2026-06-09_MC1_Implementation).

Path_to_Fun · DR-029_Path_A_Fork_Locks · DR-028_Combat_Primary_Verb_Depth_First · 2026-06-09_MC1_Implementation · 2026-06-09_MC1_Build_Spec · validate-netcode-design-before-coding