7.8 KiB
date, type, tags, permalink
| date | type | tags | permalink | ||||||
|---|---|---|---|---|---|---|---|---|---|
| 2026-06-10 | session |
|
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;GruntWindupTicksreferencesTuning.AttackWindupTicks(not a duplicate literal —Tuning.AttackWindupTicksstays alive forTelegraphTests).Apply/Get/ClampKnobswitch on a byte index (no enum on a Bursted/RPC path).TuningBroadcastSystem(Server, editor-only): ensures+seeds the server singleton toDefaults()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 clientTuningConfigsingleton from it each tick (so the PREDICTEDDashSystemreads live values). The overlay mutates the readout optimistically viaSetLocal(instant feel for the tuner; eventually consistent with the server within ~1 RTT).DebugOp.SetTuning = 12+ the server handler (TuningConfig.Applyon the server singleton) +DebugCommandSendSystem.SetTuning(knob, value)(×1000 fixed-point over the intArgB) + a collapsible, scroll-wrapped Tuning section inDebugOverlay(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 viaTryGetSingleton ? : Defaults()— so release builds fall back toDefaults()== today's consts, behaviour unchanged. The 5 dash consts and 4 Charger consts were deleted (now inDefaults()); 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.
DashSystemcomputesdashSpeed = DashDistance / (IFrameWindowTicks/60)at a read site distinct from the window math. The spec only guarded the window, not the divisor — and the optimisticSetLocalpath bypassesApply's clamp, so nudging the i-frame-window knob to 0 → 0 denominator →MoveVelocityNaN → the kinematic body'sLocalTransform.Positiongoes 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 inApply+SetLocal(defense in depth). Pinned byTuningConfigTests.Apply_Clamps_Tick_Knobs_To_At_Least_One+ the golden-defaults test. - Hardening folded in (several refuted findings converged):
Apply/SetLocalclamp all tick knobs ≥1 and value knobs ≥0; Charger read-sites guard with(uint)math.max(1,…); do NOT deleteTuning.AttackWindupTicks(a reviewer caughtTelegraphTests.cs:73references it →Defaults().GruntWindupTicksreferences it instead); a golden test pinsDefaults()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/EnemyAISystemreading the singleton compiled fine; no enum-on-Burst hazard, byte knob map). - EditMode: 267/267 green (was 259; +8
TuningConfigTests: goldenDefaults()==consts incl.GruntWindupTicks==Tuning.AttackWindupTicks,Applyindex-map, tick/value clamps, out-of-range no-op, ×1000 wire round-trip,ToReport/FromReportround-trip, and a world test provingDashSystemreads a non-default singleton — IFrameUntilTick = now+20 with a tuned window, not Defaults' 12). The pre-existingDashSystemTests(asserting 112/121/145/200/20 with no singleton) double as the behaviour-preserving guard on theDefaults()fallback path. - Live Play (real netcode session, server+client): both worlds create+seed
TuningConfigtoDefaults()(iframe 12 / dist 4 / chWindup 30 / lungeSpd 16 / grunt 18); aSetTuningRPC (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.unityuntracked artifact still pending review/delete (carried from 2026-06-09_MC1_Implementation).
Links
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