Tuning Knobs

This commit is contained in:
2026-06-10 15:22:30 -07:00
parent da522efe7a
commit 08f16b689f
20 changed files with 11045 additions and 18 deletions
+3 -1
View File
@@ -79,7 +79,9 @@ Green EditMode + server==client stay **necessary, not sufficient** — they were
### The thesis
Depth = a **dialogue**. Enemies ask distinct, readable questions (a committed lunge to dodge, a bolt to reposition from, a swarm to AoE); the player answers with tools that have skill and **commitment cost**. The keystone is **enemy commitment + a punishable whiff** paired with the **dash** — the dash is the *answer*, a committed lunge is the *question*; neither is fun alone (so they ship together). The repo is well-shaped for this: the predicted CharacterController, the `RespawnInvuln`/`KnockbackState`/`AttackWindup` windowed-tick idiom, the derive-don't-replicate `Dead` gate, the `StatModifier` fold, and a near-complete `CombatFeedbackSystem` juice scaffold are all already proven under prediction.
### MC-0 — Instrument the box (dev-overlay readback) `~0.5 d` · risk LOW
### MC-0 — Instrument the box (dev-overlay readback) `~0.5 d` · risk LOW · ✅ DONE (2026-06-10)
> **Status:** telemetry counters **and** the `TuningConfig` live-tuning singleton — 10 dash/Charger knobs nudgeable from the dev overlay mid-Play, no recompile — both landed. See [[2026-06-10_MC0_TuningConfig_LiveTuning]]. The MC-1 fun-gate's playtest→nudge→replay loop is now unblocked.
**Goal:** make every later fun-gate *measurable* before spending a friend's time. The M8 dev-tools triad (`DebugCommandRequest` + `DebugOverlay` + `DebugCommandReceiveSystem`) today only **sends** commands and never reads live values back — that gap is why the gates are unfalsifiable.
- **Scope:** add a server-only `DevTelemetry` `IComponentData` (a flat struct of `uint` counters + a few `float` accumulators, **not** `[GhostField]` by default) updated at the stamp sites the later milestones already touch. Surface it to the local overlay via a handful of owner-send `[GhostField] uint`s on the predicted player (read each frame in `PresentationSystemGroup`) **or** a periodic `DebugTelemetryReport` RPC server→client (avoids any ghost-hash change). Add a read-only IMGUI readout block to `DebugOverlay` showing the live counters + derived ratios (negated-hits/dash, whiff-convert %, per-player DPS, hit-stop frames, downed/revive timers).
- **Build notes:** the dev-RPC wire type stays **unconditional** (no `#if` on the struct — the RpcCollection hash must match release/dev peers); `#if UNITY_EDITOR`-gate only the send/receive systems. Add a `SetTuning(op, valueX1000)` `DebugOp` so the operator nudges live singletons (below) from the overlay without leaving Play. Pure editor-only, server-authoritative plumbing — fully Claude-headless.
@@ -0,0 +1,50 @@
---
date: 2026-06-10
type: session
tags:
- session
- combat
- mc-0
- tuning
- netcode
- dev-tools
permalink: 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|MC-1]] 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]]).
## 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]]