Same-session follow-up: user confirmed the light-carrier hydration fix worked, then reported missing candle flames + fountain water particles. Tested whether it's the same root cause (a mesh-empty Setup dropped by EntityHydrationRules before its Setup.DefaultScript — the ambient particle script GpuWorldState.cs:221 fires — is ever read). Refuted: the fountain (0x02000AA3) has a surviving mesh part and a real DefaultScript (0x33000B21), never dropped by the gate. The guessed "candle" objects (0x02001967, ring of 16 around the fountain) have real mesh and no DefaultScript at all — not candles. Separate root cause; filed as its own issue rather than chased further this session. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
186 lines
9.3 KiB
C#
186 lines
9.3 KiB
C#
using System;
|
|
using System.IO;
|
|
using DatReaderWriter;
|
|
using DatReaderWriter.DBObjs;
|
|
using DatReaderWriter.Options;
|
|
using Xunit;
|
|
using Xunit.Abstractions;
|
|
using Env = System.Environment;
|
|
|
|
namespace AcDream.Core.Tests.Rendering;
|
|
|
|
/// <summary>
|
|
/// #79/#93/#176/#177 A7.L1 (2026-07-09) — dat-truth check for the Town Network
|
|
/// fountain room (0x00070144). Live probe evidence (launch-a7-lightscope.log)
|
|
/// showed 0x00070144 NEVER appears in the [indoor-light] byCell histogram across
|
|
/// every visit this session, regardless of the visible-cell scoping fix — every
|
|
/// pooled light belongs to NEIGHBORING cells (0x132/0x133/0x143/0x145/0x155/0x157),
|
|
/// never to 0x144 itself. Both registration sites (GameWindow.cs:3771 live weenie
|
|
/// spawns, GameWindow.cs:7919 dat EnvCell statics) already tag CellId correctly, so
|
|
/// this is either (a) the room genuinely has no dat-authored Setup.Lights of its
|
|
/// own (a registration/spawn-side non-issue — the darkness is caused by something
|
|
/// else entirely), or (b) it DOES have fixtures that never made it into
|
|
/// EnvCell.StaticObjects / the live weenie spawn set (a real hydration gap).
|
|
/// Dumps StaticObjects + each Setup-sourced static's Setup.Lights.Count for the
|
|
/// fountain room and its immediate neighbors observed this session, so the answer
|
|
/// is read from the dat directly instead of inferred from aggregate counts.
|
|
/// </summary>
|
|
public class Issue93TownNetworkFountainRoomLightInspectionTests
|
|
{
|
|
private readonly ITestOutputHelper _out;
|
|
public Issue93TownNetworkFountainRoomLightInspectionTests(ITestOutputHelper output) => _out = output;
|
|
|
|
private static string? ResolveDatDir()
|
|
{
|
|
var datDir = Env.GetEnvironmentVariable("ACDREAM_DAT_DIR")
|
|
?? Path.Combine(Env.GetFolderPath(Env.SpecialFolder.UserProfile),
|
|
"Documents", "Asheron's Call");
|
|
return Directory.Exists(datDir) ? datDir : null;
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0x00070144u)] // the fountain room — the user's original repro spot
|
|
[InlineData(0x00070156u)] // player's teleport-in cell this session
|
|
[InlineData(0x00070164u)]
|
|
[InlineData(0x00070132u)]
|
|
[InlineData(0x00070133u)]
|
|
[InlineData(0x00070143u)]
|
|
[InlineData(0x00070145u)]
|
|
[InlineData(0x00070146u)]
|
|
[InlineData(0x00070155u)]
|
|
[InlineData(0x00070157u)]
|
|
public void StaticObjects_SetupLightsCount_Dump(uint cellId)
|
|
{
|
|
var datDir = ResolveDatDir();
|
|
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; }
|
|
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
|
|
|
var envCell = dats.Get<EnvCell>(cellId);
|
|
if (envCell is null)
|
|
{
|
|
_out.WriteLine($"=== 0x{cellId:X8} NOT FOUND in dat ===");
|
|
return;
|
|
}
|
|
|
|
_out.WriteLine($"=== 0x{cellId:X8} Env=0x{envCell.EnvironmentId:X4} struct={envCell.CellStructure} " +
|
|
$"pos=({envCell.Position.Origin.X:F2},{envCell.Position.Origin.Y:F2},{envCell.Position.Origin.Z:F2}) " +
|
|
$"StaticObjects={envCell.StaticObjects.Count} ===");
|
|
|
|
int totalLights = 0;
|
|
foreach (var so in envCell.StaticObjects)
|
|
{
|
|
if ((so.Id & 0xFF000000u) == 0x02000000u)
|
|
{
|
|
var setup = dats.Get<Setup>(so.Id);
|
|
int lightCount = setup?.Lights.Count ?? -1;
|
|
totalLights += Math.Max(lightCount, 0);
|
|
_out.WriteLine($" SETUP id=0x{so.Id:X8} at ({so.Frame.Origin.X:F2},{so.Frame.Origin.Y:F2},{so.Frame.Origin.Z:F2}) Lights={lightCount}");
|
|
}
|
|
else
|
|
{
|
|
_out.WriteLine($" id=0x{so.Id:X8} at ({so.Frame.Origin.X:F2},{so.Frame.Origin.Y:F2},{so.Frame.Origin.Z:F2}) (not a Setup — no Lights dict)");
|
|
}
|
|
}
|
|
_out.WriteLine($" TOTAL dat-authored Lights in cell 0x{cellId:X8} StaticObjects: {totalLights}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Setup 0x02000365 at (69.88,-69.92,5.01) — 5 m above the fountain, the room's
|
|
/// ONLY dat-authored light (Lights=1) — never appeared in the live [indoor-light]
|
|
/// byCell histogram. GameWindow.cs:7324 drops the ENTIRE hydrated entity (light
|
|
/// included) when its flattened <c>meshRefs.Count == 0</c> — mirror that exact
|
|
/// gate here (IsRuntimeHiddenMarker skip, then GfxObj-resolves check) to see
|
|
/// whether this specific fixture is a mesh-less/marker-only "light attach point"
|
|
/// that the mesh-gate silently drops before the lights loop (GameWindow.cs:7892)
|
|
/// ever sees it.
|
|
/// </summary>
|
|
[Fact]
|
|
public void CeilingFixtureSetup_MeshFlattenSurvivorCount_Dump()
|
|
{
|
|
var datDir = ResolveDatDir();
|
|
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; }
|
|
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
|
|
|
const uint setupId = 0x02000365u;
|
|
var setup = dats.Get<Setup>(setupId);
|
|
Assert.NotNull(setup);
|
|
|
|
_out.WriteLine($"=== Setup 0x{setupId:X8}: Parts={setup!.Parts.Count} PlacementFrames={setup.PlacementFrames.Count} Lights={setup.Lights.Count} ===");
|
|
foreach (var kvp in setup.Lights)
|
|
_out.WriteLine($" light[{kvp.Key}] Color=({kvp.Value.Color?.Red},{kvp.Value.Color?.Green},{kvp.Value.Color?.Blue}) Intensity={kvp.Value.Intensity} Falloff={kvp.Value.Falloff} ConeAngle={kvp.Value.ConeAngle}");
|
|
|
|
var flat = AcDream.Core.Meshing.SetupMesh.Flatten(setup);
|
|
_out.WriteLine($" SetupMesh.Flatten -> {flat.Count} MeshRefs");
|
|
|
|
int survivors = 0, markerSkipped = 0, gfxNull = 0;
|
|
foreach (var mr in flat)
|
|
{
|
|
if (AcDream.Core.Meshing.GfxObjDegradeResolver.IsRuntimeHiddenMarker(dats, mr.GfxObjId))
|
|
{
|
|
markerSkipped++;
|
|
_out.WriteLine($" part gfx=0x{mr.GfxObjId:X8} -> MARKER (skipped)");
|
|
continue;
|
|
}
|
|
var gfx = dats.Get<GfxObj>(mr.GfxObjId);
|
|
if (gfx is null)
|
|
{
|
|
gfxNull++;
|
|
_out.WriteLine($" part gfx=0x{mr.GfxObjId:X8} -> GFXOBJ-NULL (dropped)");
|
|
continue;
|
|
}
|
|
survivors++;
|
|
_out.WriteLine($" part gfx=0x{mr.GfxObjId:X8} -> survives (Polygons={gfx.Polygons.Count})");
|
|
}
|
|
_out.WriteLine($" meshRefs survivor count = {survivors} (markerSkipped={markerSkipped} gfxNull={gfxNull}) " +
|
|
$"=> GameWindow.cs:7324 would {(survivors == 0 ? "DROP" : "KEEP")} this entity");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Follow-up (same session, 2026-07-09): user confirmed lighting improved but
|
|
/// reported missing candle flames + fountain water particles. Hypothesis tested:
|
|
/// same mesh-empty hydration-gate class as the light fix, just for
|
|
/// Setup.DefaultScript instead of Setup.Lights (GpuWorldState.cs:221 fires
|
|
/// ambient particle scripts from DefaultScript.DataId per hydrated entity — the
|
|
/// SAME lb.Entities list EntityHydrationRules.ShouldKeepEntity gates).
|
|
/// <b>REFUTED — do not re-chase this specific mechanism for #79/#93's particle
|
|
/// residual.</b> The fountain (0x02000AA3) has a SURVIVING mesh part (1/1) AND a
|
|
/// real DefaultScript (0x33000B21, almost certainly the water spray) — never
|
|
/// dropped by the gate, mesh or no. The "16 ring objects" guess (0x02001967) was
|
|
/// WRONG: real mesh, DefaultScript==0 — not candles, no script at all. Filed as
|
|
/// its own issue (missing ambient particles) — the remaining suspects are
|
|
/// downstream of hydration entirely: does EntityScriptActivator/
|
|
/// PhysicsScriptRunner actually fire DefaultScript=0x33000B21 for dat-hydrated
|
|
/// (ServerGuid==0) entities at runtime, does that script resolve to a real
|
|
/// PhysicsScriptTable entry, and does the resulting emitter reach the particle
|
|
/// renderer. NOT investigated further this session — separate root cause,
|
|
/// separate fix.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData(0x02000AA3u)] // the fountain itself (cell 0x00070144 center)
|
|
[InlineData(0x02001967u)] // the 16 ring objects around the fountain (candles?)
|
|
[InlineData(0x020018C5u)] // the other cell-center static
|
|
public void FountainAndCandleSetup_DefaultScriptAndMeshSurvivorCount_Dump(uint setupId)
|
|
{
|
|
var datDir = ResolveDatDir();
|
|
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; }
|
|
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
|
|
|
var setup = dats.Get<Setup>(setupId);
|
|
Assert.NotNull(setup);
|
|
|
|
_out.WriteLine($"=== Setup 0x{setupId:X8}: Parts={setup!.Parts.Count} DefaultScript.DataId=0x{setup.DefaultScript.DataId:X8} Lights={setup.Lights.Count} ===");
|
|
|
|
var flat = AcDream.Core.Meshing.SetupMesh.Flatten(setup);
|
|
int survivors = 0;
|
|
foreach (var mr in flat)
|
|
{
|
|
if (AcDream.Core.Meshing.GfxObjDegradeResolver.IsRuntimeHiddenMarker(dats, mr.GfxObjId)) continue;
|
|
if (dats.Get<GfxObj>(mr.GfxObjId) is null) continue;
|
|
survivors++;
|
|
}
|
|
bool wouldBeKeptByCurrentFix = AcDream.Core.Meshing.EntityHydrationRules.ShouldKeepEntity(survivors, setup.Lights.Count);
|
|
_out.WriteLine($" flattened={flat.Count} meshSurvivors={survivors} " +
|
|
$"hasDefaultScript={setup.DefaultScript.DataId != 0} " +
|
|
$"=> current ShouldKeepEntity(mesh,lights) = {wouldBeKeptByCurrentFix}");
|
|
}
|
|
}
|