Campaign OVERHAUL S3 chunk 1 (docs/research/2026-09-01-overhaul/s3-walk-ownership-map.md
§11): the transcript-kinds/fixtures/print-only-emitter half of the walk work,
built AFTER chunk 3 landed LC/SC (the per-land-cell interleave).
B1 — the emitter (print-only, never gates admission/depth/order):
- ACDREAM_DUMP_WALK_TRANSCRIPT=1 is read once into RuntimeOptions.DumpWalkTranscript
(rule 4) and handed to RenderingDiagnostics.DumpWalkTranscriptEnabled (rule 5,
a settable static, not a second env read) once at GameWindow construction — the
deep walk call sites have no reachable RuntimeOptions reference.
- WalkTranscriptDump (new) prints the OH line kinds — F/P/LS/LC/SC/BLD/DI/DC/EC/OC —
to Console at the exact points retail's cdb breakpoints sit
(tools/walk-oracle/oh/oh-capture-walk.cdb.template), gated internally so every
method bails out before any string work when the flag is off.
- Every call site lives in WalkFrameDriver.cs, at the point the driver already
processes that turn: Collect (F/P, after BeginFrame), Emit's DI/LS/DC/BLD cases,
OnLandCellTurn/OnLandscapeCellTurn (LC/SC, at LOD resolution via the new
WalkTranscriptDump.LodCellId helper, before the 8x8-bucket expansion), and the
EmitFloodTurns/EmitCellContentsTurn loops (EC/OC — both UNCONDITIONAL per flood
visit, matching the OH captures' always-equal EC/OC counts; retail's own
DrawEnvCell stamp dedupe sits past the breakpoint, inside the function).
- DC's "pv=" field encodes interior(0)/outdoor(1) as an 8-hex-digit 0/1 so it
satisfies the same pv=[0-9a-f]{8} regex real captures use; derived from
_currentDcStage at the DC event (CellStatic = interior pview, else outdoor).
- The frame-root pose (origin, quaternion) is a reasonable orthonormal basis built
from the walk's own CyPlane.Normal forward vector and WalkLandscape's own
ViewerWorldOriginX/Y block origin — self-consistent for the round-trip parser,
not a byte-exact reproduction of retail's Frame (B4's diff never compares P).
B2 — WalkOracleTrace learns EC/OC event kinds (LC/SC already existed from chunk 3).
New WalkOraclePartsTrace.cs holds two small read-only parsers for the parts log
(PD/DM) and the alpha-depth log (AM/FL/PM/PC) — records only, no validator, no
canonical JSONL, no new tool.
B3 — fixtures: the five OH walk captures join WalkOracleTraceTests.AllFixtures
(now (root, name) pairs — FW0's own root plus the OH capture directory) for
parse + complete-frame pins. The four kit poses join WalkTraceConformanceTests'
still-fixture coverage as NEW rows (the OH cathedral-arrival root is f4180108,
not FW0's f4180106): terrace-edge/cathedral-arrival extend the existing theory
(now (root, fixture) parameterized); holtburg-doorway-still and foundry-deep get
dedicated tests mirroring their FW0 siblings' own structure. Finding: the OH
foundry-deep capture's own retail transcript draws 12 real town buildings through
its exit chain (unlike the FW0 capture, which apparently reached none at that
pose) — the FW0 test's stub 1x1 landscape undershoots it (first divergence:
nothing after "LS" vs retail's real BLD content); fixed by building the full
landscape/building world via WalkLandscapeDatBuilder.Build, matching the shared
theory's own approach, not by skipping or weakening the row.
WalkTraceReplayContext.Signature(WalkOracleFrame) now filters to the DI/DC/BLD/LS
kinds (LC/SC/EC/OC never had a WalkEvent analogue in RetailFrameWalk's own
four-kind vocabulary) instead of mapping them to a "?" placeholder, so the still-
fixture comparison stays apples-to-apples on both sides.
B4 — WalkTranscriptSignatureDiff (test-side only, no runner/tool): diffs two
transcripts (raw lines or parsed frames) at the full DI/DC/BLD/LS/LC/SC/EC/OC
level, reporting the first divergent event and position per frame. Proven over a
synthetic self-vs-self-minus-one-event pair (SignatureDiff_ReportsTheExactRemovedEvent).
Tests: T1 (flag off) is split into a unit-level zero-allocation/zero-output check
on WalkTranscriptDump itself (the walk's pre-existing allocation, e.g.
RetailFrameWalk.EmitDrawCells's per-call array, is untouched by this chunk and not
independently zero-alloc) and an integration-level Collect() check; both assert via
WalkOracleTrace.Parse returning zero frames rather than raw string equality, which
is robust to unrelated Console.WriteLine noise from other test classes running in
parallel (a real, observed hazard — WalkFrameDriverTests joins
CameraDiagnosticsCollection for the same reason CornerFloodReplayTests/
Issue181WallPressEquilibriumTests already do, issue #251). T2 proves the
emitter/parser round trip on a synthetic interior frame. T3's InstalledDat rows all
pass. T4: LaunchOptionsDocumentationTests green with the ACDREAM_DUMP_WALK_TRANSCRIPT
row (both directions).
Gates: hermetic lane 6,814/0 (was 6,795 baseline + new tests), three consecutive
clean runs; InstalledDat lane 245/3 known-failures (the two pre-existing #383
layout tests + TowerAscent) unchanged from baseline.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
652 lines
24 KiB
C#
652 lines
24 KiB
C#
using System.Collections.Generic;
|
|
using AcDream.App;
|
|
using AcDream.App.Rendering.Residency;
|
|
using AcDream.App.Streaming;
|
|
|
|
namespace AcDream.App.Tests;
|
|
|
|
/// <summary>
|
|
/// Unit tests for <see cref="RuntimeOptions"/> startup-time parsing.
|
|
/// Behavior-preservation only: every assertion locks in the exact
|
|
/// boolean / numeric / nullability semantics from the env reads that
|
|
/// previously lived in <c>GameWindow.cs</c>.
|
|
/// </summary>
|
|
public sealed class RuntimeOptionsTests
|
|
{
|
|
[Fact]
|
|
public void ResidencyBudgetsPreserveCurrentProductionDefaults()
|
|
{
|
|
RuntimeOptions options = RuntimeOptions.Parse(
|
|
"D:\\dat",
|
|
_ => null);
|
|
|
|
Assert.Equal(ResidencyBudgetOptions.Default, options.ResidencyBudgets);
|
|
Assert.Equal(
|
|
StreamingWorkBudgetOptions.Default,
|
|
options.StreamingWorkBudgets);
|
|
}
|
|
|
|
[Fact]
|
|
public void StreamingWorkBudgetOverridesAreOneTypedProfile()
|
|
{
|
|
var values = new Dictionary<string, string?>
|
|
{
|
|
["ACDREAM_STREAM_WORK_MS"] = "1.75",
|
|
["ACDREAM_STREAM_WORK_COMPLETIONS"] = "31",
|
|
["ACDREAM_STREAM_WORK_CPU_MIB"] = "6",
|
|
["ACDREAM_STREAM_WORK_ENTITY_OPS"] = "144",
|
|
["ACDREAM_STREAM_WORK_GPU_MIB"] = "5",
|
|
["ACDREAM_STREAM_WORK_GL_RETIRE_OPS"] = "23",
|
|
["ACDREAM_STREAM_WORK_DEST_RESERVE_PERCENT"] = "60",
|
|
};
|
|
|
|
RuntimeOptions options = RuntimeOptions.Parse(
|
|
"D:\\dat",
|
|
name => values.GetValueOrDefault(name));
|
|
|
|
Assert.Equal(1.75, options.StreamingWorkBudgets.MaxUpdateMilliseconds);
|
|
Assert.Equal(31, options.StreamingWorkBudgets.MaxCompletionAdmissions);
|
|
Assert.Equal(
|
|
6 * StreamingWorkBudgetOptions.MiB,
|
|
options.StreamingWorkBudgets.MaxAdoptedCpuBytes);
|
|
Assert.Equal(144, options.StreamingWorkBudgets.MaxEntityOperations);
|
|
Assert.Equal(
|
|
5 * StreamingWorkBudgetOptions.MiB,
|
|
options.StreamingWorkBudgets.MaxGpuUploadBytes);
|
|
Assert.Equal(23, options.StreamingWorkBudgets.MaxGlRetireOperations);
|
|
Assert.Equal(0.60f, options.StreamingWorkBudgets.DestinationReserveFraction);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("0")]
|
|
[InlineData("-1")]
|
|
[InlineData("NaN")]
|
|
[InlineData("Infinity")]
|
|
[InlineData("bad")]
|
|
public void InvalidStreamingWorkValuesFallBackIndependently(string value)
|
|
{
|
|
RuntimeOptions options = RuntimeOptions.Parse(
|
|
"D:\\dat",
|
|
name => name switch
|
|
{
|
|
"ACDREAM_STREAM_WORK_MS" => value,
|
|
"ACDREAM_STREAM_WORK_DEST_RESERVE_PERCENT" => value,
|
|
_ => null,
|
|
});
|
|
|
|
Assert.Equal(
|
|
StreamingWorkBudgetOptions.Default.MaxUpdateMilliseconds,
|
|
options.StreamingWorkBudgets.MaxUpdateMilliseconds);
|
|
Assert.Equal(
|
|
StreamingWorkBudgetOptions.Default.DestinationReserveFraction,
|
|
options.StreamingWorkBudgets.DestinationReserveFraction);
|
|
}
|
|
|
|
[Fact]
|
|
public void ResidencyBudgetOverridesAreTypedMebibytesAndCounts()
|
|
{
|
|
var values = new Dictionary<string, string?>
|
|
{
|
|
["ACDREAM_RESIDENCY_MESH_GPU_MIB"] = "768",
|
|
["ACDREAM_RESIDENCY_MESH_UNOWNED_ENTRIES"] = "72",
|
|
["ACDREAM_RESIDENCY_ANIMATION_MIB"] = "48",
|
|
["ACDREAM_RESIDENCY_ANIMATION_ENTRIES"] = "300",
|
|
["ACDREAM_RESIDENCY_AUDIO_MIB"] = "24",
|
|
};
|
|
|
|
RuntimeOptions options = RuntimeOptions.Parse(
|
|
"D:\\dat",
|
|
name => values.GetValueOrDefault(name));
|
|
|
|
Assert.Equal(
|
|
768 * ResidencyBudgetOptions.MiB,
|
|
options.ResidencyBudgets.ObjectMeshGpuBytes);
|
|
Assert.Equal(
|
|
72,
|
|
options.ResidencyBudgets.ObjectMeshUnownedEntries);
|
|
Assert.Equal(
|
|
48 * ResidencyBudgetOptions.MiB,
|
|
options.ResidencyBudgets.AnimationBytes);
|
|
Assert.Equal(300, options.ResidencyBudgets.AnimationEntries);
|
|
Assert.Equal(
|
|
24 * ResidencyBudgetOptions.MiB,
|
|
options.ResidencyBudgets.AudioBytes);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("0")]
|
|
[InlineData("-1")]
|
|
[InlineData("bad")]
|
|
[InlineData("8796093022208")]
|
|
public void InvalidResidencyBudgetFallsBackToCurrentDefault(string value)
|
|
{
|
|
RuntimeOptions options = RuntimeOptions.Parse(
|
|
"D:\\dat",
|
|
name => name == "ACDREAM_RESIDENCY_MESH_GPU_MIB"
|
|
? value
|
|
: null);
|
|
|
|
Assert.Equal(
|
|
ResidencyBudgetOptions.Default.ObjectMeshGpuBytes,
|
|
options.ResidencyBudgets.ObjectMeshGpuBytes);
|
|
}
|
|
|
|
private const string AnyDatDir = "C:/Users/test/dats";
|
|
|
|
private static Func<string, string?> Env(Dictionary<string, string?> values)
|
|
=> name => values.TryGetValue(name, out var v) ? v : null;
|
|
|
|
private static Func<string, string?> EmptyEnv() => _ => null;
|
|
|
|
[Fact]
|
|
public void Defaults_AllSafeOff_WhenEnvironmentIsEmpty()
|
|
{
|
|
var opts = RuntimeOptions.Parse(AnyDatDir, EmptyEnv());
|
|
|
|
Assert.Equal(AnyDatDir, opts.DatDir);
|
|
Assert.Equal(
|
|
Path.Combine(AnyDatDir, "acdream.pak"),
|
|
opts.PreparedAssetPath);
|
|
Assert.False(opts.LiveMode);
|
|
Assert.Equal("127.0.0.1", opts.LiveHost);
|
|
Assert.Equal(9000, opts.LivePort);
|
|
Assert.Null(opts.LiveUser);
|
|
Assert.Null(opts.LivePass);
|
|
Assert.False(opts.DevTools);
|
|
Assert.False(opts.UncappedRendering);
|
|
Assert.False(opts.DumpMoveTruth);
|
|
Assert.False(opts.DumpWalkTranscript);
|
|
Assert.False(opts.NoAudio);
|
|
Assert.Equal(-1, opts.HidePartIndex);
|
|
// Default-on: RetailCloseDegrades is true unless explicitly disabled.
|
|
Assert.True(opts.RetailCloseDegrades);
|
|
Assert.False(opts.DumpSceneryZ);
|
|
Assert.Null(opts.LegacyStreamRadius);
|
|
Assert.False(opts.UiProbeDump);
|
|
Assert.Null(opts.UiProbeScript);
|
|
Assert.Null(opts.AutomationArtifactDirectory);
|
|
Assert.False(opts.ExactAutomationFramebuffer);
|
|
Assert.False(opts.UiProbeEnabled);
|
|
Assert.False(opts.HasLiveCredentials);
|
|
Assert.Empty(opts.PluginTags);
|
|
}
|
|
|
|
[Fact]
|
|
public void PluginPeerTagsAreParsedOnceBoundedAndCaseInsensitive()
|
|
{
|
|
string oversized = new('x', 129);
|
|
RuntimeOptions options = RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
Env(new()
|
|
{
|
|
["ACDREAM_PLUGIN_TAGS"] =
|
|
$" healer,Leader,HEALER,,{oversized}, scout ",
|
|
}));
|
|
|
|
Assert.Equal(["healer", "Leader", "scout"], options.PluginTags);
|
|
}
|
|
|
|
[Fact]
|
|
public void PreparedAssetPath_DefaultsBesideDats_AndAllowsOneOverride()
|
|
{
|
|
Assert.Equal(
|
|
Path.Combine(AnyDatDir, "acdream.pak"),
|
|
RuntimeOptions.Parse(AnyDatDir, EmptyEnv()).PreparedAssetPath);
|
|
|
|
var overridden = RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
Env(new() { ["ACDREAM_PAK_PATH"] = "D:/prepared/acdream.pak" }));
|
|
Assert.Equal("D:/prepared/acdream.pak", overridden.PreparedAssetPath);
|
|
}
|
|
|
|
[Fact]
|
|
public void LiveMode_Set_ExactlyByValue1()
|
|
{
|
|
Assert.True(RuntimeOptions.Parse(AnyDatDir, Env(new() { ["ACDREAM_LIVE"] = "1" })).LiveMode);
|
|
Assert.False(RuntimeOptions.Parse(AnyDatDir, Env(new() { ["ACDREAM_LIVE"] = "0" })).LiveMode);
|
|
Assert.False(RuntimeOptions.Parse(AnyDatDir, Env(new() { ["ACDREAM_LIVE"] = "true" })).LiveMode);
|
|
Assert.False(RuntimeOptions.Parse(AnyDatDir, Env(new() { ["ACDREAM_LIVE"] = "" })).LiveMode);
|
|
}
|
|
|
|
[Fact]
|
|
public void LiveHostAndPort_FallBackToDefaults_WhenUnsetOrInvalid()
|
|
{
|
|
var withDefaults = RuntimeOptions.Parse(AnyDatDir, EmptyEnv());
|
|
Assert.Equal("127.0.0.1", withDefaults.LiveHost);
|
|
Assert.Equal(9000, withDefaults.LivePort);
|
|
|
|
var withOverrides = RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_TEST_HOST"] = "play.example.com",
|
|
["ACDREAM_TEST_PORT"] = "9123",
|
|
}));
|
|
Assert.Equal("play.example.com", withOverrides.LiveHost);
|
|
Assert.Equal(9123, withOverrides.LivePort);
|
|
|
|
// Non-numeric port falls back to default; we don't throw at parse time.
|
|
var withBadPort = RuntimeOptions.Parse(AnyDatDir, Env(new() { ["ACDREAM_TEST_PORT"] = "abc" }));
|
|
Assert.Equal(9000, withBadPort.LivePort);
|
|
}
|
|
|
|
[Fact]
|
|
public void LiveUserPass_NullWhenEmptyOrUnset()
|
|
{
|
|
var emptyValues = RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_TEST_USER"] = "",
|
|
["ACDREAM_TEST_PASS"] = "",
|
|
}));
|
|
Assert.Null(emptyValues.LiveUser);
|
|
Assert.Null(emptyValues.LivePass);
|
|
|
|
var realValues = RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_TEST_USER"] = "testaccount",
|
|
["ACDREAM_TEST_PASS"] = "testpassword",
|
|
}));
|
|
Assert.Equal("testaccount", realValues.LiveUser);
|
|
Assert.Equal("testpassword", realValues.LivePass);
|
|
}
|
|
|
|
[Fact]
|
|
public void RecordPrintMembersRedactsTheLivePassword()
|
|
{
|
|
RuntimeOptions options = RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
Env(new()
|
|
{
|
|
["ACDREAM_LIVE"] = "1",
|
|
["ACDREAM_TEST_USER"] = "testaccount",
|
|
["ACDREAM_TEST_PASS"] = "top-secret-value",
|
|
}));
|
|
|
|
string printed = options.ToString();
|
|
|
|
Assert.DoesNotContain("top-secret-value", printed, StringComparison.Ordinal);
|
|
Assert.Contains("LivePass = <redacted>", printed, StringComparison.Ordinal);
|
|
Assert.Contains("LiveHost = 127.0.0.1", printed, StringComparison.Ordinal);
|
|
Assert.Contains("HasLiveCredentials = True", printed, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void HasLiveCredentials_RequiresLiveModeAndBothUserAndPass()
|
|
{
|
|
// Live mode off → no credentials regardless of user/pass.
|
|
var noLive = RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_TEST_USER"] = "u",
|
|
["ACDREAM_TEST_PASS"] = "p",
|
|
}));
|
|
Assert.False(noLive.HasLiveCredentials);
|
|
|
|
// Live mode on but missing user → no credentials.
|
|
var missingUser = RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_LIVE"] = "1",
|
|
["ACDREAM_TEST_PASS"] = "p",
|
|
}));
|
|
Assert.False(missingUser.HasLiveCredentials);
|
|
|
|
// Live mode on but missing pass → no credentials.
|
|
var missingPass = RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_LIVE"] = "1",
|
|
["ACDREAM_TEST_USER"] = "u",
|
|
}));
|
|
Assert.False(missingPass.HasLiveCredentials);
|
|
|
|
// All three present → credentials available.
|
|
var ok = RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_LIVE"] = "1",
|
|
["ACDREAM_TEST_USER"] = "u",
|
|
["ACDREAM_TEST_PASS"] = "p",
|
|
}));
|
|
Assert.True(ok.HasLiveCredentials);
|
|
}
|
|
|
|
[Fact]
|
|
public void HidePartIndex_MinusOneWhenUnset_ParsesIntegers()
|
|
{
|
|
Assert.Equal(-1, RuntimeOptions.Parse(AnyDatDir, EmptyEnv()).HidePartIndex);
|
|
Assert.Equal(7, RuntimeOptions.Parse(AnyDatDir, Env(new() { ["ACDREAM_HIDE_PART"] = "7" })).HidePartIndex);
|
|
// Invalid → fall back to -1 (preserves the int.TryParse failure semantics).
|
|
Assert.Equal(-1, RuntimeOptions.Parse(AnyDatDir, Env(new() { ["ACDREAM_HIDE_PART"] = "abc" })).HidePartIndex);
|
|
}
|
|
|
|
[Fact]
|
|
public void RetailCloseDegrades_DefaultOn_ExceptWhenValueIsExactlyZero()
|
|
{
|
|
// Unset → on.
|
|
Assert.True(RuntimeOptions.Parse(AnyDatDir, EmptyEnv()).RetailCloseDegrades);
|
|
|
|
// Exactly "0" → off. Matches the pre-refactor semantics:
|
|
// !string.Equals(env, "0", StringComparison.Ordinal)
|
|
Assert.False(RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_RETAIL_CLOSE_DEGRADES"] = "0",
|
|
})).RetailCloseDegrades);
|
|
|
|
// Any other value → on (including "1", "false", "True"). The original
|
|
// code only checked for the literal "0"; preserve that.
|
|
Assert.True(RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_RETAIL_CLOSE_DEGRADES"] = "1",
|
|
})).RetailCloseDegrades);
|
|
Assert.True(RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_RETAIL_CLOSE_DEGRADES"] = "false",
|
|
})).RetailCloseDegrades);
|
|
}
|
|
|
|
[Fact]
|
|
public void LegacyStreamRadius_NullWhenUnsetOrInvalid_ParsesNonNegativeIntegers()
|
|
{
|
|
Assert.Null(RuntimeOptions.Parse(AnyDatDir, EmptyEnv()).LegacyStreamRadius);
|
|
Assert.Null(RuntimeOptions.Parse(AnyDatDir, Env(new() { ["ACDREAM_STREAM_RADIUS"] = "abc" })).LegacyStreamRadius);
|
|
// Negative values are filtered out by the pre-refactor `sr >= 0` guard.
|
|
Assert.Null(RuntimeOptions.Parse(AnyDatDir, Env(new() { ["ACDREAM_STREAM_RADIUS"] = "-3" })).LegacyStreamRadius);
|
|
|
|
Assert.Equal(0, RuntimeOptions.Parse(AnyDatDir, Env(new() { ["ACDREAM_STREAM_RADIUS"] = "0" })).LegacyStreamRadius);
|
|
Assert.Equal(5, RuntimeOptions.Parse(AnyDatDir, Env(new() { ["ACDREAM_STREAM_RADIUS"] = "5" })).LegacyStreamRadius);
|
|
Assert.Equal(12, RuntimeOptions.Parse(AnyDatDir, Env(new() { ["ACDREAM_STREAM_RADIUS"] = "12" })).LegacyStreamRadius);
|
|
}
|
|
|
|
[Fact]
|
|
public void DayGroupOverride_IsReadOnceIntoTypedOptions()
|
|
{
|
|
Assert.Null(
|
|
RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
EmptyEnv()).ForcedDayGroupIndex);
|
|
Assert.Null(
|
|
RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
Env(new() { ["ACDREAM_DAY_GROUP"] = "-1" }))
|
|
.ForcedDayGroupIndex);
|
|
Assert.Equal(
|
|
7,
|
|
RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
Env(new() { ["ACDREAM_DAY_GROUP"] = "7" }))
|
|
.ForcedDayGroupIndex);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Campaign V slice V7. Pins the Dereth day fraction -- the sun, the
|
|
/// keyframe, and every lit surface. Range-checked rather than merely parsed:
|
|
/// a day fraction outside [0, 1) is not a clamp candidate, it is a typo, and
|
|
/// silently pinning the world at 12.5 would be worse than ignoring it.
|
|
/// </summary>
|
|
[Fact]
|
|
public void WorldTimeOverride_IsReadOnceIntoTypedOptions()
|
|
{
|
|
Assert.Null(
|
|
RuntimeOptions.Parse(AnyDatDir, EmptyEnv()).PinnedWorldDayFraction);
|
|
Assert.Equal(
|
|
0.5f,
|
|
RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
Env(new() { ["ACDREAM_WORLD_TIME"] = "0.5" }))
|
|
.PinnedWorldDayFraction);
|
|
Assert.Equal(
|
|
0f,
|
|
RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
Env(new() { ["ACDREAM_WORLD_TIME"] = "0" }))
|
|
.PinnedWorldDayFraction);
|
|
foreach (string rejected in new[] { "1", "1.5", "-0.1", "midnight", "" })
|
|
{
|
|
Assert.Null(
|
|
RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
Env(new() { ["ACDREAM_WORLD_TIME"] = rejected }))
|
|
.PinnedWorldDayFraction);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void ExactAutomationFramebuffer_IsExplicitAndExactOneOnly()
|
|
{
|
|
Assert.True(RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
Env(new() { ["ACDREAM_AUTOMATION_EXACT_FRAMEBUFFER"] = "1" }))
|
|
.ExactAutomationFramebuffer);
|
|
|
|
foreach (string value in new[] { "", "0", "true", "yes" })
|
|
{
|
|
Assert.False(RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
Env(new() { ["ACDREAM_AUTOMATION_EXACT_FRAMEBUFFER"] = value }))
|
|
.ExactAutomationFramebuffer);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void OrbitDistanceOverride_AcceptsOnlyPositiveFiniteMeters()
|
|
{
|
|
Assert.Null(
|
|
RuntimeOptions.Parse(AnyDatDir, EmptyEnv())
|
|
.InitialOrbitDistanceMeters);
|
|
Assert.Equal(
|
|
120f,
|
|
RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
Env(new() { ["ACDREAM_ORBIT_DISTANCE_METERS"] = "120" }))
|
|
.InitialOrbitDistanceMeters);
|
|
|
|
foreach (string rejected in new[] { "0", "-1", "NaN", "Infinity", "near" })
|
|
{
|
|
Assert.Null(
|
|
RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
Env(new() { ["ACDREAM_ORBIT_DISTANCE_METERS"] = rejected }))
|
|
.InitialOrbitDistanceMeters);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void OrbitAngleOverrides_AcceptFiniteYawAndBoundedPitchDegrees()
|
|
{
|
|
RuntimeOptions defaults = RuntimeOptions.Parse(AnyDatDir, EmptyEnv());
|
|
Assert.Null(defaults.InitialOrbitYawDegrees);
|
|
Assert.Null(defaults.InitialOrbitPitchDegrees);
|
|
|
|
RuntimeOptions parsed = RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
Env(new()
|
|
{
|
|
["ACDREAM_ORBIT_YAW_DEGREES"] = "-135.5",
|
|
["ACDREAM_ORBIT_PITCH_DEGREES"] = "7.25",
|
|
}));
|
|
Assert.Equal(-135.5f, parsed.InitialOrbitYawDegrees);
|
|
Assert.Equal(7.25f, parsed.InitialOrbitPitchDegrees);
|
|
|
|
foreach (string rejected in new[] { "NaN", "Infinity", "angle" })
|
|
{
|
|
Assert.Null(
|
|
RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
Env(new() { ["ACDREAM_ORBIT_YAW_DEGREES"] = rejected }))
|
|
.InitialOrbitYawDegrees);
|
|
}
|
|
foreach (string rejected in new[] { "-90", "90", "NaN", "pitch" })
|
|
{
|
|
Assert.Null(
|
|
RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
Env(new() { ["ACDREAM_ORBIT_PITCH_DEGREES"] = rejected }))
|
|
.InitialOrbitPitchDegrees);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Campaign V slice V7. The sky has two clocks and this pins the one
|
|
/// ACDREAM_DAY_GROUP cannot reach — the cloud sheet's UV scroll, which
|
|
/// accumulates against real elapsed time. Unset must stay unset: every
|
|
/// ordinary run keeps the wall clock, and only the backend differential gate
|
|
/// asks for a pin.
|
|
/// </summary>
|
|
[Fact]
|
|
public void SkyPhaseOverride_IsReadOnceIntoTypedOptions()
|
|
{
|
|
Assert.Null(
|
|
RuntimeOptions.Parse(AnyDatDir, EmptyEnv()).SkyAnimationPhaseSeconds);
|
|
Assert.Null(
|
|
RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
Env(new() { ["ACDREAM_SKY_PHASE_SECONDS"] = "not-a-number" }))
|
|
.SkyAnimationPhaseSeconds);
|
|
// Zero is a real request, not "unset": it is the cloud sheet's authored
|
|
// origin and the value the gate script pins by default.
|
|
Assert.Equal(
|
|
0f,
|
|
RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
Env(new() { ["ACDREAM_SKY_PHASE_SECONDS"] = "0" }))
|
|
.SkyAnimationPhaseSeconds);
|
|
Assert.Equal(
|
|
12.5f,
|
|
RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
Env(new() { ["ACDREAM_SKY_PHASE_SECONDS"] = "12.5" }))
|
|
.SkyAnimationPhaseSeconds);
|
|
}
|
|
|
|
[Fact]
|
|
public void DiagnosticFlags_RespectExactValueOne()
|
|
{
|
|
var allOn = RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_DEVTOOLS"] = "1",
|
|
["ACDREAM_UNCAPPED_RENDER"] = "1",
|
|
["ACDREAM_DUMP_MOVE_TRUTH"] = "1",
|
|
["ACDREAM_DUMP_SKY"] = "1",
|
|
["ACDREAM_DUMP_WALK_TRANSCRIPT"] = "1",
|
|
["ACDREAM_NO_AUDIO"] = "1",
|
|
["ACDREAM_DUMP_SCENERY_Z"] = "1",
|
|
}));
|
|
Assert.True(allOn.DevTools);
|
|
Assert.True(allOn.UncappedRendering);
|
|
Assert.True(allOn.DumpMoveTruth);
|
|
Assert.True(allOn.DumpSky);
|
|
Assert.True(allOn.DumpWalkTranscript);
|
|
Assert.True(allOn.NoAudio);
|
|
Assert.True(allOn.DumpSceneryZ);
|
|
|
|
// Any non-"1" value leaves them off, matching the
|
|
// string.Equals(env, "1", StringComparison.Ordinal) check.
|
|
var anyOther = RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_DUMP_MOVE_TRUTH"] = "true",
|
|
["ACDREAM_DEVTOOLS"] = "true",
|
|
["ACDREAM_UNCAPPED_RENDER"] = "true",
|
|
["ACDREAM_NO_AUDIO"] = "2",
|
|
["ACDREAM_DUMP_SCENERY_Z"] = " 1",
|
|
["ACDREAM_DUMP_WALK_TRANSCRIPT"] = "0",
|
|
}));
|
|
Assert.False(anyOther.DevTools);
|
|
Assert.False(anyOther.UncappedRendering);
|
|
Assert.False(anyOther.DumpMoveTruth);
|
|
Assert.False(anyOther.NoAudio);
|
|
Assert.False(anyOther.DumpSceneryZ);
|
|
Assert.False(anyOther.DumpWalkTranscript);
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_RejectsNullDatDirOrEnv()
|
|
{
|
|
Assert.Throws<ArgumentNullException>(() => RuntimeOptions.Parse(null!, EmptyEnv()));
|
|
Assert.Throws<ArgumentNullException>(() => RuntimeOptions.Parse(AnyDatDir, null!));
|
|
}
|
|
|
|
[Fact]
|
|
public void VulkanDeviceOverride_IsNullWhenUnsetOrEmpty()
|
|
{
|
|
Assert.Null(RuntimeOptions.Parse(AnyDatDir, EmptyEnv()).VulkanDeviceOverride);
|
|
Assert.Null(
|
|
RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
Env(new() { ["ACDREAM_VULKAN_DEVICE"] = "" })).VulkanDeviceOverride);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The override is carried verbatim — index or name substring — because the
|
|
/// capability report records exactly what the operator asked for, matched or
|
|
/// not.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData("1")]
|
|
[InlineData("Radeon")]
|
|
public void VulkanDeviceOverride_IsCarriedVerbatim(string value)
|
|
{
|
|
Assert.Equal(
|
|
value,
|
|
RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
Env(new() { ["ACDREAM_VULKAN_DEVICE"] = value })).VulkanDeviceOverride);
|
|
}
|
|
|
|
[Fact]
|
|
public void VulkanForcedUnsupportedFeature_DrivesTheExitFourGateKnob()
|
|
{
|
|
Assert.Null(
|
|
RuntimeOptions.Parse(AnyDatDir, EmptyEnv()).VulkanForcedUnsupportedFeature);
|
|
Assert.Equal(
|
|
"timelineSemaphore",
|
|
RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
Env(new() { ["ACDREAM_VULKAN_FORCE_UNSUPPORTED"] = "timelineSemaphore" }))
|
|
.VulkanForcedUnsupportedFeature);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Campaign V slice V9. The frame budget is what lets the probe harness run
|
|
/// unattended in CI. Zero is the interactive default, so every invocation
|
|
/// that predates the slice keeps presenting until its window closes.
|
|
/// </summary>
|
|
[Fact]
|
|
public void VulkanCapabilityProbeFrames_DefaultsToUnbounded()
|
|
{
|
|
Assert.Equal(
|
|
0,
|
|
RuntimeOptions.Parse(AnyDatDir, EmptyEnv()).VulkanCapabilityProbeFrames);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("1", 1)]
|
|
[InlineData("30", 30)]
|
|
[InlineData("0", 0)]
|
|
public void VulkanCapabilityProbeFrames_ParsesANonNegativeBudget(
|
|
string value,
|
|
int expected)
|
|
{
|
|
Assert.Equal(
|
|
expected,
|
|
RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
Env(new() { ["ACDREAM_VULKAN_PROBE_FRAMES"] = value }))
|
|
.VulkanCapabilityProbeFrames);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A malformed budget falls back to the interactive default rather than to
|
|
/// some invented number: a CI step that meant to bound the run and mistyped
|
|
/// it should hang and be noticed, not silently capture at a frame count
|
|
/// nobody asked for.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData("")]
|
|
[InlineData("-1")]
|
|
[InlineData("many")]
|
|
[InlineData("30.5")]
|
|
public void VulkanCapabilityProbeFrames_RejectsMalformedValues(string value)
|
|
{
|
|
Assert.Equal(
|
|
0,
|
|
RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
Env(new() { ["ACDREAM_VULKAN_PROBE_FRAMES"] = value }))
|
|
.VulkanCapabilityProbeFrames);
|
|
}
|
|
}
|