using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Text.RegularExpressions; using AcDream.Core.Physics; using Xunit; namespace AcDream.Core.Tests.Physics; /// /// L.2g S2 conformance harness, layer 1: golden fixtures parsed from a LIVE /// cdb trace of a RETAIL observer client /// (Fixtures/l2g-observer-trace.log, captured 2026-07-02 with /// tools/cdb/l2g-observer.cdb while a retail actor ran the structured /// motion protocol through ACE). /// /// Each [MTIS] block is an exact INPUT (the InterpretedMotionState retail /// received, dumped field-by-field) and the [DIM] lines that follow on the /// same minterp are retail's exact OUTPUT (every DoInterpretedMotion /// dispatch, in order). We replay every input through acdream's funnel and /// assert an identical dispatch sequence — retail's actual runtime is the /// oracle, not anyone's reading of the decomp. /// /// Exclusions (documented, not silent): /// - Action-class dispatches (0x10000000 bit) come from the UM's action /// LIST, which the [MTIS] struct dump doesn't include (LList pointer /// only) — filtered from both sides; covered by the synthetic action /// tests in MotionInterpreterFunnelTests instead. /// - motion==0x80000000 entries (the unpack-level DoMotion(command_ids[0]) /// style call for wire style index 0) — outside /// move_to_interpreted_state; S3 wires the unpack-level style-on-change. /// public class RetailObserverTraceConformanceTests { private sealed record TraceCase( int Line, string Minterp, InboundInterpretedState Ims, List Dims); private sealed class RecordingSink : IInterpretedMotionSink { public readonly List Applied = new(); public bool ApplyMotion(uint motion, float speed) { Applied.Add(motion); // R3-W5: style/stance ids (>= 0x80000000) have no dat MotionData // entry — retail's real motion-table lookup fails for them // (see MotionInterpreterDoMotionFamilyTests / the interface // doc on IInterpretedMotionSink.ApplyMotion for the full // rationale). The fake sink mirrors that so the style dispatch // doesn't clobber ForwardCommand before the next read. return motion < 0x80000000u; } public bool StopMotion(uint motion) => true; } private static string TracePath() { var dir = AppContext.BaseDirectory; while (dir is not null && !File.Exists(Path.Combine(dir, "AcDream.slnx"))) dir = Directory.GetParent(dir)?.FullName; Assert.NotNull(dir); return Path.Combine(dir!, "tests", "AcDream.Core.Tests", "Fixtures", "l2g-observer-trace.log"); } private static List ParseTrace() { var lines = File.ReadAllLines(TracePath()); var cases = new List(); var mtisRe = new Regex(@"\[MTIS\] minterp=(\w+)"); var fieldRe = new Regex(@"\+0x(\w+) (\w+)\s+: (\S+)"); var dimRe = new Regex(@"\[DIM\] minterp=(\w+) motion=(\w+)"); for (int i = 0; i < lines.Length; i++) { var m = mtisRe.Match(lines[i]); if (!m.Success) continue; string minterp = m.Groups[1].Value; var ims = InboundInterpretedState.Default(); int j = i + 1; for (; j < lines.Length; j++) { var f = fieldRe.Match(lines[j]); if (!f.Success) break; string name = f.Groups[2].Value; string val = f.Groups[3].Value; uint ParseHex() => Convert.ToUInt32(val, 16); float ParseF() => float.Parse(val, CultureInfo.InvariantCulture); switch (name) { case "current_style": ims.CurrentStyle = ParseHex(); break; case "forward_command": ims.ForwardCommand = ParseHex(); break; case "forward_speed": ims.ForwardSpeed = ParseF(); break; case "sidestep_command": ims.SideStepCommand = ParseHex(); break; case "sidestep_speed": ims.SideStepSpeed = ParseF(); break; case "turn_command": ims.TurnCommand = ParseHex(); break; case "turn_speed": ims.TurnSpeed = ParseF(); break; } } // Collect this minterp's DIMs until the next UNPACK/MTIS event. var dims = new List(); for (; j < lines.Length; j++) { if (lines[j].Contains("[UNPACK]") || lines[j].Contains("[MTIS]")) break; var d = dimRe.Match(lines[j]); if (d.Success && d.Groups[1].Value == minterp) dims.Add(Convert.ToUInt32(d.Groups[2].Value, 16)); } // A SECOND style dispatch marks a second apply_current_movement // pass fired by the physics tick (HitGround / LeaveGround // re-apply, CMotionInterp::HitGround 0x00528ac0) — not by this // UM. Keep only the first pass as this UM's output. (2 cases in // the capture — the actor's jump/land moments.) int secondStyle = -1; for (int k = 1; k < dims.Count; k++) if (dims[k] == ims.CurrentStyle) { secondStyle = k; break; } if (secondStyle > 0) dims = dims.Take(secondStyle).ToList(); cases.Add(new TraceCase(i + 1, minterp, ims, dims)); } return cases; } private static bool IsExcluded(uint motion) => (motion & 0x10000000u) != 0 // action-list dispatches, not in the dump || motion == 0x80000000u; // unpack-level style-index-0 call [Fact] public void EveryTracedUm_ProducesRetailsExactDispatchSequence() { var cases = ParseTrace(); Assert.True(cases.Count > 100, $"expected >100 traced UMs, parsed {cases.Count}"); int verified = 0; var failures = new List(); foreach (var c in cases) { // The trace breakpoint sits at DoInterpretedMotion ENTRY // (pre-gate); the sink records POST-gate (GetObjectSequence) // calls. For grounded bodies they coincide. A Falling dispatch // for a non-Falling forward command marks the traced body as // AIRBORNE at that moment (the actor's jump) — replay those // with contact cleared, and drop the traced style entry from // the expectation (called pre-gate, blocked post-gate). bool airborne = c.Ims.ForwardCommand != 0x40000015u && c.Dims.Contains(0x40000015u); var body = new PhysicsBody(); body.State |= PhysicsStateFlags.Gravity; body.TransientState |= TransientStateFlags.Active; if (!airborne) body.TransientState |= TransientStateFlags.Contact | TransientStateFlags.OnWalkable; var mi = new MotionInterpreter(body); var sink = new RecordingSink(); mi.MoveToInterpretedState(c.Ims, sink); var expected = c.Dims.Where(d => !IsExcluded(d) && (!airborne || d != c.Ims.CurrentStyle)).ToList(); var actual = sink.Applied.Where(d => !IsExcluded(d)).ToList(); if (!expected.SequenceEqual(actual)) { failures.Add( $"line {c.Line} minterp {c.Minterp}: " + $"fwd=0x{c.Ims.ForwardCommand:X8}@{c.Ims.ForwardSpeed:F2} " + $"side=0x{c.Ims.SideStepCommand:X8} turn=0x{c.Ims.TurnCommand:X8} — " + $"retail [{string.Join(",", expected.Select(d => d.ToString("X8")))}] " + $"vs acdream [{string.Join(",", actual.Select(d => d.ToString("X8")))}]"); } else { verified++; } } Assert.True(failures.Count == 0, $"{failures.Count} of {cases.Count} traced UMs diverged " + $"({verified} conformant):\n" + string.Join("\n", failures.Take(12))); } }