Lands the working A8 indoor-rendering and streaming fixes accumulated this session. User has verified these visually to some degree (e.g. lifestone / translucent meshes confirmed fine under the FrontFace flip; bridge / wall / collision regressions confirmed fixed after travel); not every path has been exhaustively gated. The cellar-flap defect remains OPEN and will be solved the retail-faithful way via a dedicated brainstorm (see handoff docs). Rendering core (reviewed, high confidence): - EnvCellRenderer SSBO stride fix: upload packed Matrix4x4[] (64B) instead of the 80B CPU InstanceData struct the shader never expected — fixes the transform/texture "explosion" for any draw with >1 instance (cells that dedupe to a shared cellGeomId). Real root cause. - WB-style global FrontFace(CW) + per-batch CullMode carried through the MDI layout (GroupKey + BuildIndirectArrays + DrawIndirectRange split into same-cull runs with absolute uDrawIDOffset per run). - EntitySet partitioning (IndoorPass / OutdoorScenery / LiveDynamic) + WorldEntity.BuildingShellAnchorCellId so building shells scope to their dat-derived building cell instead of rendering everywhere. - RenderOutsideInAcdream (look into buildings from outside) + CollectVisiblePortalBuildings frustum cull of portal bounds. - Sky-when-inside-building + per-cell audit probe + GL-state probe. Streaming / perf (test-covered; not independently code-reviewed this session): - Near/far priority queues so near work wins over far; PromoteToNear carries full landblock + mesh data; LandblockEntriesWithoutAnimatedIndex avoids rebuilding the animated-lookup dict in the hot draw path. Fixes the bridge-not-appearing / missing-walls / broken-collision-after-travel regressions and improves post-transition FPS. Tooling + docs: - tools/A8CellAudit: offline dat cell/portal/building dumper (portals + buildings modes) — reproduces the cellar-flap investigation with no launch. - docs/research cellar-flap root-cause + option-2 handoff (the didInsideStencil double-duty finding + the WB-recursive design decision + brainstorm prompt), entity-taxonomy, replan, issue-78 visibility investigation. Diagnostics retained on purpose: ACDREAM_A8_DIAG_* gates, portal_stencil.vert provisional pos.w clamp, and the probe families are kept (env-var gated, zero cost when off) because the pending option-2 cellar-flap brainstorm needs them. Strip in the option-2 ship commit. Indoor branch stays behind ACDREAM_A8_INDOOR_BRANCH=1 (default off = pre-A8 visual). Build green; App tests + Core (streaming/dispatcher/loader) tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
247 lines
10 KiB
C#
247 lines
10 KiB
C#
using System.Collections.Generic;
|
|
using AcDream.App;
|
|
|
|
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
|
|
{
|
|
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.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.DumpMoveTruth);
|
|
Assert.False(opts.NoAudio);
|
|
Assert.False(opts.EnableSkyPesDebug);
|
|
Assert.Equal(-1, opts.HidePartIndex);
|
|
// Default-on: RetailCloseDegrades is true unless explicitly disabled.
|
|
Assert.True(opts.RetailCloseDegrades);
|
|
Assert.False(opts.DumpSceneryZ);
|
|
Assert.False(opts.A8DiagDisableInsideStep4Terrain);
|
|
Assert.False(opts.A8DiagDisableInsideStep4Outdoor);
|
|
Assert.False(opts.A8DiagDisableInsideStep3EnvCellOpaque);
|
|
Assert.False(opts.A8DiagDisableInsideStep3IndoorPass);
|
|
Assert.False(opts.A8DiagDisableInsideStep2Punch);
|
|
Assert.False(opts.A8DiagDisablePortalDepthClamp);
|
|
Assert.Null(opts.LegacyStreamRadius);
|
|
Assert.False(opts.HasLiveCredentials);
|
|
}
|
|
|
|
[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 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 DiagnosticFlags_RespectExactValueOne()
|
|
{
|
|
var allOn = RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_DEVTOOLS"] = "1",
|
|
["ACDREAM_DUMP_MOVE_TRUTH"] = "1",
|
|
["ACDREAM_NO_AUDIO"] = "1",
|
|
["ACDREAM_ENABLE_SKY_PES"] = "1",
|
|
["ACDREAM_DUMP_SCENERY_Z"] = "1",
|
|
["ACDREAM_A8_DIAG_DISABLE_INSIDE_STEP4_TERRAIN"] = "1",
|
|
["ACDREAM_A8_DIAG_DISABLE_INSIDE_STEP4_OUTDOOR"] = "1",
|
|
["ACDREAM_A8_DIAG_DISABLE_INSIDE_STEP3_ENVCELL_OPAQUE"] = "1",
|
|
["ACDREAM_A8_DIAG_DISABLE_INSIDE_STEP3_INDOORPASS"] = "1",
|
|
["ACDREAM_A8_DIAG_DISABLE_INSIDE_STEP2_PUNCH"] = "1",
|
|
["ACDREAM_A8_DIAG_DISABLE_PORTAL_DEPTH_CLAMP"] = "1",
|
|
}));
|
|
Assert.True(allOn.DevTools);
|
|
Assert.True(allOn.DumpMoveTruth);
|
|
Assert.True(allOn.NoAudio);
|
|
Assert.True(allOn.EnableSkyPesDebug);
|
|
Assert.True(allOn.DumpSceneryZ);
|
|
Assert.True(allOn.A8DiagDisableInsideStep4Terrain);
|
|
Assert.True(allOn.A8DiagDisableInsideStep4Outdoor);
|
|
Assert.True(allOn.A8DiagDisableInsideStep3EnvCellOpaque);
|
|
Assert.True(allOn.A8DiagDisableInsideStep3IndoorPass);
|
|
Assert.True(allOn.A8DiagDisableInsideStep2Punch);
|
|
Assert.True(allOn.A8DiagDisablePortalDepthClamp);
|
|
|
|
// Any non-"1" value leaves them off, matching the
|
|
// string.Equals(env, "1", StringComparison.Ordinal) check.
|
|
var anyOther = RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_DEVTOOLS"] = "true",
|
|
["ACDREAM_DUMP_MOVE_TRUTH"] = "yes",
|
|
["ACDREAM_NO_AUDIO"] = "2",
|
|
["ACDREAM_ENABLE_SKY_PES"] = "on",
|
|
["ACDREAM_DUMP_SCENERY_Z"] = " 1",
|
|
["ACDREAM_A8_DIAG_DISABLE_INSIDE_STEP4_TERRAIN"] = "true",
|
|
["ACDREAM_A8_DIAG_DISABLE_INSIDE_STEP4_OUTDOOR"] = "yes",
|
|
["ACDREAM_A8_DIAG_DISABLE_INSIDE_STEP3_ENVCELL_OPAQUE"] = "2",
|
|
["ACDREAM_A8_DIAG_DISABLE_INSIDE_STEP3_INDOORPASS"] = " 1",
|
|
["ACDREAM_A8_DIAG_DISABLE_INSIDE_STEP2_PUNCH"] = "true",
|
|
["ACDREAM_A8_DIAG_DISABLE_PORTAL_DEPTH_CLAMP"] = "yes",
|
|
}));
|
|
Assert.False(anyOther.DevTools);
|
|
Assert.False(anyOther.DumpMoveTruth);
|
|
Assert.False(anyOther.NoAudio);
|
|
Assert.False(anyOther.EnableSkyPesDebug);
|
|
Assert.False(anyOther.DumpSceneryZ);
|
|
Assert.False(anyOther.A8DiagDisableInsideStep4Terrain);
|
|
Assert.False(anyOther.A8DiagDisableInsideStep4Outdoor);
|
|
Assert.False(anyOther.A8DiagDisableInsideStep3EnvCellOpaque);
|
|
Assert.False(anyOther.A8DiagDisableInsideStep3IndoorPass);
|
|
Assert.False(anyOther.A8DiagDisableInsideStep2Punch);
|
|
Assert.False(anyOther.A8DiagDisablePortalDepthClamp);
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_RejectsNullDatDirOrEnv()
|
|
{
|
|
Assert.Throws<ArgumentNullException>(() => RuntimeOptions.Parse(null!, EmptyEnv()));
|
|
Assert.Throws<ArgumentNullException>(() => RuntimeOptions.Parse(AnyDatDir, null!));
|
|
}
|
|
}
|