using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using AcDream.Core.Lighting;
using DatReaderWriter;
using DatReaderWriter.Options;
using DatLandBlockInfo = DatReaderWriter.DBObjs.LandBlockInfo;
using DatSetup = DatReaderWriter.DBObjs.Setup;
using Xunit;
using Xunit.Abstractions;
namespace AcDream.Core.Tests.Conformance;
///
/// A7 Fix D round 2 (2026-06-19) — resolve the OPEN torch-REACH question without
/// guessing or a live launch: dump the RAW dat LightInfo.Falloff for every
/// static light in the Holtburg landblocks, via the EXACT production load path
/// (). The dat is the SAME file retail reads, so
/// these falloffs ARE what retail reads (modulo any load-time transform, settled
/// separately in the decomp). Output-only — no assertions; read the log.
///
public sealed class HoltburgTorchFalloffProbeTests
{
private readonly ITestOutputHelper _out;
public HoltburgTorchFalloffProbeTests(ITestOutputHelper output) => _out = output;
[Fact]
public void Dump_Holtburg_StaticLight_Falloffs()
{
var datDir = ConformanceDats.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; }
using var dats = new DatCollection(datDir, DatAccessType.Read);
// The meeting hall sits in the Holtburg town landblocks. Sweep a small
// neighbourhood so we catch every entrance torch the streaming window
// would load around the player at the hall.
uint[] landblocks =
{
0xA9B3u, 0xA9B4u, 0xA9B2u, 0xA9B5u, 0xAAB3u, 0xAAB4u, 0xA8B3u, 0xA8B4u,
};
// Tally every distinct raw Falloff seen (the headline number).
var falloffTally = new SortedDictionary();
int totalLights = 0;
foreach (uint lb in landblocks)
{
uint infoId = (lb << 16) | 0xFFFEu;
var info = dats.Get(infoId);
if (info is null) { _out.WriteLine($"=== LB 0x{lb:X4}: LandBlockInfo NULL ==="); continue; }
int buildings = info.Buildings?.Count ?? 0;
int objects = info.Objects?.Count ?? 0;
_out.WriteLine($"=== LB 0x{lb:X4}: Buildings={buildings} Objects={objects} ===");
// Record building-shell origins so we can rank torches by proximity.
var shells = new List<(uint model, Vector3 pos)>();
if (info.Buildings is not null)
{
foreach (var b in info.Buildings)
{
var o = b.Frame.Origin;
shells.Add((b.ModelId, new Vector3(o.X, o.Y, o.Z)));
_out.WriteLine($" BUILDING shell model=0x{b.ModelId:X8} pos=({o.X:F1},{o.Y:F1},{o.Z:F1}) portals={b.Portals?.Count ?? 0}");
}
}
if (info.Objects is null) continue;
foreach (var stab in info.Objects)
{
// Only Setup-sourced stabs (0x02xxxxxx) carry a Lights dictionary —
// identical gate to GameWindow.cs:6399.
if ((stab.Id & 0xFF000000u) != 0x02000000u) continue;
var setup = dats.Get(stab.Id);
if (setup?.Lights is null || setup.Lights.Count == 0) continue;
var loaded = LightInfoLoader.Load(
setup,
ownerId: 0,
entityPosition: new Vector3(stab.Frame.Origin.X, stab.Frame.Origin.Y, stab.Frame.Origin.Z),
entityRotation: new Quaternion(
stab.Frame.Orientation.X, stab.Frame.Orientation.Y,
stab.Frame.Orientation.Z, stab.Frame.Orientation.W));
foreach (var (kvp, ls) in setup.Lights.Zip(loaded, (k, l) => (k, l)))
{
float rawFalloff = kvp.Value.Falloff;
totalLights++;
falloffTally.TryGetValue(rawFalloff, out int c);
falloffTally[rawFalloff] = c + 1;
// Nearest building shell, for "is this an entrance torch on the hall?".
float nearest = float.MaxValue;
uint nearestModel = 0;
foreach (var (model, spos) in shells)
{
float dd = Vector3.Distance(ls.WorldPosition, spos);
if (dd < nearest) { nearest = dd; nearestModel = model; }
}
_out.WriteLine(
$" LIGHT setup=0x{stab.Id:X8} kind={ls.Kind} " +
$"pos=({ls.WorldPosition.X:F1},{ls.WorldPosition.Y:F1},{ls.WorldPosition.Z:F1}) " +
$"color=({ls.ColorLinear.X:F3},{ls.ColorLinear.Y:F3},{ls.ColorLinear.Z:F3}) " +
$"intensity={ls.Intensity:F1} rawFalloff={rawFalloff:F3} Range={ls.Range:F3} " +
$"cone={ls.ConeAngle:F3} nearestShell=0x{nearestModel:X8}@{(nearest == float.MaxValue ? -1f : nearest):F1}m");
}
}
}
_out.WriteLine($"=== FALLOFF HISTOGRAM (raw dat values across {totalLights} static lights) ===");
foreach (var kv in falloffTally)
_out.WriteLine($" rawFalloff={kv.Key:F3} -> Range(x1.3)={kv.Key * 1.3f:F3}m count={kv.Value}");
}
}