Skip to content

v0.10.0

Choose a tag to compare

@github-actions github-actions released this 15 Jul 18:27
· 9 commits to main since this release
17ac072

Added

  • atlas diff baseline.trx candidate.trx: first-class differential comparison of two TRX
    runs (issue #88, 0.10.0 roadmap, from the StratumParity field pattern: the same suite runs
    against vanilla and against a fork, and the outcome comparison was hand-rolled scripts
    until now). Comparison is keyed by test name exactly as the TRX reports it (theory rows
    carry their arguments in the name, so every row diffs on its own; duplicate names, one per
    rerun attempt, merge worst-outcome-first) and buckets every change into new failures
    (failed in candidate; passed, skipped or absent in baseline), fixed (failed to passed),
    vanished (present to absent), new tests, still failing, and notable duration shifts
    between two passing runs (conservative on purpose: at least 2x AND at least 500 ms apart,
    both directions reported, informational only). The console report is a summary line plus
    compact per-category listings (empty categories print nothing); --json replaces it with
    a stable machine shape versioned like the worker protocol (v: 1 first, additive
    evolution, category keys always present). Exit codes gate differential CI directly: 0 no
    regressions, 1 at least one regression (a new failure or a vanished test, nothing else), 2
    usage or unreadable input. Works on the TRX Atlas writes (atlas run --parallel --trx)
    and tolerates any spec-conforming TRX including plain dotnet test --logger trx; no
    server, assembly or VINTAGE_STORY involved. Full contract in
    docs/specs/2026-07-14-diff-command.md.

  • IWorldSession.EntitySimulationTicks, a monotonic counter of the embedded server's
    real entity-simulation ticks, so entity-tick-frequency probes can assert exact counts
    instead of ratios (issue #79, from the StratumParity field report: a counting
    EntityBehavior on spawned straw dummies observed non-constant ratios between
    World.Ticks(n) and actual entity ticks, about half on some runs and 100 percent on
    others, forcing the suite onto ratio assertions). The investigation (decompiles of
    1.20.12/1.21.7/1.22.3 plus the Stratum patches, and instrumented live runs on both
    flavors, written up in docs/specs/2026-07-14-tick-contract.md) found the variance was
    never pump pacing: one Atlas tick is one fire of a 1ms game-tick listener, at most one
    per ServerMain.Process() pass, and at the engine's default 33.33ms pacing, passes,
    Atlas ticks and entity-simulation ticks (a separate 20ms-stride server system) all run
    1:1 on both vanilla and Stratum. The half-counts were Stratum's distance-band entity
    throttle keyed on the engine's randomized new-player spawn (spawnRadius, 50 blocks on
    default playstyles): a probe anchored to world spawn lands at a random 0-55 blocks from
    the anchor player and straddles the fork's 32-block near/mid band boundary run by run
    (measured: 25.5 blocks -> 150/150 ticks, 50.8 blocks -> 75/150). The new counter reads
    the engine's own record of the entity-simulation system's last tick (the public
    millisecondsSinceStart stamp of the ServerSystemEntitySimulation entry in the
    internal ServerMain.Systems array, symbols verified identical on 1.20.12, 1.21.7 and
    1.22.3 and untouched by Stratum), sampled by the pump once per pass, which observes
    every fire exactly once (systems tick at most once per pass and every fire strictly
    advances the stamp). Reads run on the game thread in the same turn as probe reads, so
    Assert.Equal(counterDelta, probeTicks) is exact for an unthrottled entity on every
    supported engine, and it holds on Stratum too once the probe is anchored to
    player.Entity.Pos instead of world spawn (both proven in E2E on vanilla and the
    fork). On an engine whose tick machinery drifted the counter degrades at boot behind a
    one-time warning and only reading the property throws, with the drifted symbols named.
    The spec also pins, for the first time, what await World.Ticks(n) guarantees and does
    not; Ticks(n) semantics are deliberately unchanged.

  • Engine-assembly auto-staging at launch (issue #49, option 2): a prebuilt test assembly now
    runs against whichever install VINTAGE_STORY points at, without a rebuild. The 0.8.0
    preflight fail-fasted on a diverged test-output VintagestoryAPI.dll, and cross-VERSION
    mixes never even reached it: the game thread's own JIT loads VintagestoryLib against the
    stale copy and kills the whole testhost with a raw TypeLoadException (measured: a
    1.22.3-built output pointed at 1.21.7 dies on ServerMain.PlaySoundAt with no Atlas frame
    executed). In-process redirection is measurably impossible, not just late:
    AssemblyLoadContext.Default.LoadFromAssemblyPath(installCopy) defers to the default
    binder, which prefers the app-path copy for a colliding simple name (measured on .NET 10:
    it returns the test-output assembly). So Atlas now rewrites the test-output copy itself,
    dll AND pdb as a unit, through same-directory temp files and atomic renames, from module
    initializers that run before anything can bind it: Atlas.XUnit's fires at xUnit DISCOVERY
    (instantiating the [AtlasScenario] discoverer, measured to precede the first engine-type
    JIT), Atlas's own covers direct ServerHost flows, and both stage the assembly's own
    directory too, which is the scenario directory under atlas run (where the app base is
    the CLI's own bin, so the CLI needed no change). The boot preflight moved from the game
    thread to StartAsync, on a thread that can still surface an error before that JIT, and
    now verifies instead of failing: staging already happened, was a no-op (identical copies,
    the documented rebuild flow, byte-for-byte unchanged behavior), or the boot throws an
    AtlasSetupException naming both file identities and the remedies for the genuinely
    impossible cases: a diverged copy already bound because engine types were JITted before
    any Atlas code ran (the output is still re-staged so a plain re-run recovers), an
    unwritable test output, an install shipping no VintagestoryAPI.pdb. Field motivation
    from the issue #49 thread: StratumParity's differential CI (vanilla and the Stratum fork
    on every push) rebuilt the test assembly once per install under the single-VINTAGE_STORY
    constraint, doubling its CI time; auto-staging removes the per-install rebuild. The staging
    set covers the second game-provided file too, direction-aware: the output's
    Newtonsoft.Json.dll (the BUILD-time install's game build) is left alone when it is the
    same-or-newer build than the target's (the measured-green mix: the newer build is a
    superset for the older engine), and an OLDER build, which the newer engine cannot run (it
    binds members its own build added: JToken.WriteTo(JsonWriter) on 1.22.3 killed every boot
    in measurement, 90/105 scenarios plus all samples), is refused up front with both file
    versions and the remedies named, plus the same on-disk re-stage, because the VSTest host
    binds that assembly at process start, before any trigger could ever swap it in-process. So
    the supported prebuilt pattern is: build against the newest engine you target, run
    everywhere older; the full engine E2E suite built once on 1.22.3 runs 105/105 on 1.21.7 AND
    1.20.12 without a rebuild, and the per-push prebuilt-cross-install CI lane proves the
    samples crossing 1.22.3 -> 1.21.7 -> 1.21.7 (idempotence) -> 1.22.3 from one build, with
    byte-identity asserts on the staged copy. The pure decision core (EngineStaging) and the
    thin IO/reflection shell (EngineStager) replace VsInstall.VerifyApiCopyMatchesInstall;
    ApiCopySync keeps the shared identity/compare primitives.

Fixed

  • EngineCompat now resolves EnumClientState.Playing from the loaded engine by name instead
    of comparing joined clients against the compiled-in value: 1.22 inserted Admitted into the
    enum, shifting Playing from 3 (1.20.x/1.21.x) to 4, and the C# compiler bakes enum values
    into the referencing assembly's IL exactly like the GameVersion consts the shim already
    reads from metadata at run time. A prebuilt Atlas therefore misread the join lifecycle on the
    other engine line: on the first issue #49 cross-install engine run (1.22.3-built suite on
    1.21.7), all 33 join-dependent scenarios timed out in WaitForPlaying, comparing that
    engine's Queued against Playing, while every join-free scenario passed (72/105). The value
    is boot-validated like every shim member (a fork that renames the state fails fast with the
    symbol and game version named), and the compat source rule gains its corollary: engine enum
    members whose positions shift across supported versions are compile-time constants too, and
    must go through EngineCompat. Same-version runs are byte-for-byte unaffected.

  • WorldSession.SpawnEntity now reaches Entity.Pos/ServerPos through EngineCompat
    instead of direct member access: 1.22 turned both FIELDS into properties
    (ServerPos => Pos, one shared instance), so the same source compiles on every supported
    version but the emitted IL binds to exactly one shape, and a prebuilt binary dies with
    MissingMethodException: get_ServerPos() on the other engine line (the four failures left
    on the issue #49 cross-install engine run after the enum fix took it from 33 down to 4).
    EngineCompat.ServerPosOf/PosOf resolve the member shape once per process (property
    preferred, public-field fallback, boot-validated fail-fast with the missing member and game
    version named). SidedPos, a property on every supported version, stays the documented
    surface for SPAWNED entities (the one engine-test assert reading Entity.Pos moved to it);
    the new accessors exist for the pre-registration window where SidedPos is unusable (it
    dereferences entity.World, unset until SpawnEntity, on pre-1.22 engines).