Skip to content

v0.9.0

Choose a tag to compare

@github-actions github-actions released this 13 Jul 17:21
· 28 commits to main since this release
ed6ef8a

Added

  • Supported game-version floor lowered from 1.22.0 to 1.21.0, with 1.20.x compatible
    best-effort (issue #15, measured in docs/specs/2026-07-12-pre-122-compat.md). The entire
    compile-level gap below 1.22 is the server exit lifecycle, now owned by one runtime shim
    (EngineCompat): it installs the exit-state holder into whichever field the loaded engine
    has (ServerMain.exitState on 1.22+, ServerMain.exit before), adapts Stop between the
    1.22 Stop(string, EnumExitMode, ...) and the pre-1.22
    Stop(string, string = null, EnumLogType = Notification) shapes, and reads
    GameVersion.NetworkVersion/ShortGameVersion from the loaded assembly's metadata instead
    of the compile-time constants (which the C# compiler bakes into Atlas's IL, so a prebuilt
    Atlas on an older engine would otherwise have every test-player join kicked by the server's
    network-version check). The shim is boot-validated fail-fast: an engine whose layout
    drifted refuses to boot with an AtlasSetupException naming the game version and the
    missing symbol, and 1.19.x or older is rejected up front citing the floor (their boot API
    changes shape beyond what reflection can bridge). On 1.22.x the shim binds the modern
    members directly: zero behavior change, proven by the unchanged E2E suites. Also fixes the
    one measured pre-1.22 runtime difference: server-side entity positions are now read and
    written through SidedPos/ServerPos (pre-1.22 keeps Pos and ServerPos as two
    separate instances and only maintains ServerPos for headless joins; 1.22 unified them, so
    the fix is a no-op there) in ITestPlayer.Position, TeleportTo's dimension check,
    SpawnEntity and the player-rollback position capture/restore. Honest matrix: 1.21.7 and
    1.20.12 verified live with the full engine E2E suite plus samples (rebuilt against each
    install's own dlls, the documented flow); 1.21.7 joins the per-push CI matrix, 1.20.12
    stays on the weekly sweep; the prebuilt-NuGet-binary-on-old-engine path is designed for but
    has no CI lane yet; 1.19.x and older stay unsupported.

  • [AtlasTheory]: the theory-style counterpart to [AtlasScenario]. Combine with
    [InlineData], [MemberData] or any other xUnit DataAttribute and each data row runs as
    its own scenario on the embedded server's game thread, with the row's values in its display
    name and rows passing/failing independently. The per-scenario settings (FreshWorld,
    RollbackWorld, RestartWorld, StrictIsolation, TimeoutMs) mirror [AtlasScenario]
    exactly and apply per row (each row is a full scenario of its own, with the same isolation
    mutual exclusions). All of xUnit's own theory behavior is inherited, not
    reimplemented: serializable rows are pre-enumerated at discovery time into one test case each
    (so they appear individually in VS Test Explorer), non-serializable data falls back to
    xUnit's standard runtime-enumerating test case, and a theory with no data fails with xUnit's
    own "No data found for ..." error. Rows run sequentially like every other scenario of a
    class, and atlas run --parallel keeps a theory's rows together with their class's worker.

Changed

  • Isolation summaries close the two observability gaps of the Manifold 3c validation
    (issue #71). The per-class summary is now emitted whenever the class ran ANY isolation mode:
    FreshWorld-only classes, previously silent, report their recycle count and measured cost
    ("2 FreshWorld recycle(s) (14.2 s total)"; the recycle is measured in the registry the same
    way restarts are). And the lazy first capture of a rollback class is its own line item
    instead of being folded into the rollback count, so N rollback scenarios no longer read as
    N-1 restores: the summary starts with e.g. "1 capture (1.2 s), 3 rollback(s) succeeded
    (0.4 s total)" and the arithmetic is self-explanatory; successful restores now carry their
    measured total too. Consumer-visible in the worker protocol: the class-summary event fires
    for FreshWorld-only classes as well and its summary string uses the new wording, but the
    event's fields and v (still 1) are unchanged, per the additive rules; the worker-protocol
    spec documents the widened emission rule and wording.

  • Test players now reach EnumClientState.Playing (issue #74). Behavior change: JoinPlayer
    completes the engine's own join sequence by sending the real ClientLoaded/PlayerReady
    packets (26/29) after the inventory wait, so the SERVER runs its own transition instead of
    test players sitting one state short of visible. Joined players are now seen by everything
    that filters on ConnectedClient.IsPlayingClient or counts Playing players (Stratum's
    distance-based throttling, GetPlayersAround/NearestPlayer, playing-count broadcasts),
    and the engine's PlayerNowPlaying (and, on 1.22+, PlayerReady) events fire exactly as
    for a real client. Observable side effects of the higher fidelity: the join is announced in
    chat, the server streams world updates to the player's inert dummy buffers, natural entity
    spawning considers test players, and test players become valid interaction targets. The
    packets were originally skipped as out of scope, not because of a technical constraint; the
    decompiled 1.20.12/1.21.7/1.22.3 handlers confirmed none exists (every post-transition
    engine path is dummy-socket-safe or IsSinglePlayerClient-guarded), so Playing is the
    default with no opt-out. A player kicked by a mod DURING the join keeps today's behavior:
    JoinPlayer returns, the player never reaches Playing, and the kick is observed via
    ITestPlayer.IsConnected. A joined player that stays registered without reaching Playing
    now fails fast with an actionable AtlasSetupException (engine drift diagnosis).

Fixed

  • World rollback no longer races the engine's chunk thread on the shared savegame database
    connection. Playing test players (see above) keep chunk streaming active between scenarios,
    and the chunk thread's single-row reads (ServerSystemSupplyChunks.TryLoadMapChunk and
    friends) take no lock at all: the connection's transactionLock only serializes transaction
    blocks against each other, so a rollback transaction from the game thread made those reads
    throw mid-flight on 1.22.x ("Execute requires the command to have a transaction object when
    the connection ... is in a pending local transaction"), which the engine escalates to a full
    server shutdown; under CPU contention (4-core CI runners) this was deterministic. Capture
    reads and the restore's database phase now run inside the engine's own suspend window
    (ServerMain.Suspend(true), the exact convention the engine's autosave uses for main-thread
    database access: it pauses every server thread and waits for each acknowledgment), with a
    guaranteed resume; a suspend that cannot be acquired in time degrades the rollback
    fail-closed to a full host recycle. Suspend is public and identical on 1.20.12, 1.21.7 and
    1.22.3, so the fix needs no new reflection and protects the pre-1.22 database layer too
    (same shared connection, different timing).

  • The game-thread pump now notices an ENGINE-initiated shutdown and fails fast. When the
    engine stops itself (its reaction to an unhandled exception in one of its server threads:
    "Caught unhandled exception in thread '...'", stop reason "Exception during Process"),
    ServerMain.Process() becomes a silent sleep loop, and the pump previously spun on it
    forever: any engine crash became a job-timeout hang instead of a red test. The pump now
    watches the engine's public stopped flag (set first thing by every Stop since at least
    1.20.12), and a stop Atlas did not request is recorded as a host crash: pending tick waiters
    are faulted with the real cause, the scenario fails promptly with a ServerCrashedException
    whose message points at the server logs (the engine keeps the stop reason and the failing
    thread's stack only there), and teardown proceeds. Atlas's own stop paths are unaffected:
    they cancel the pump before ever calling the engine's Stop.