Captures everything learned from a long worktree iteration on the foreground-rain bug (ISSUES.md #1 / #26) plus a new star-rendering bug observed in the same area. The code work from that worktree (WeatherDispatcher, EmitterDescLoader.LoadFromDat, WeatherCellRenderer, GameWindow integration) was reverted because it didn't visibly fix the rain bug — but the research findings + diagnostic tools are durable and should not have to be rediscovered. What's added: - docs/research/2026-04-26-sky-investigation-handoff.md Comprehensive seed prompt for the next session. Covers: * Bug A: foreground rain (#26) — what's open, what's confirmed, what's been tried * Bug B: stars rendering as square in corner (NEW, user-observed) * 40-agent decomp scan findings — retail rain is NOT camera- particles, NOT server-driven, NOT screen-space; the mesh IS a hollow octagonal tube; only 5 weather GfxObjs in Dereth * Things ruled out by trial (envelope, scaling, unlit, depth- always alone, Setup loading) * Things to try next (depth+zfar combined, full render-state audit, frame ordering, star UV bug as easier first target) * Acceptance criteria for "done" - docs/research/2026-04-26-chorizite-pr-draft.md Upstream PR draft for Chorizite/DatReaderWriter. Five generated DBObj source files reference nonexistent enum values and are silently excluded from the NuGet build: ParticleEmitterInfo, Clothing, PaletteSet, DataIdMapper, DualDataIdMapper. Fix: delete the duplicates. Independent of the rain work — benefits the AC modding ecosystem broadly. - docs/research/2026-04-26-datreaderwriter-reference.md Developer reference for our DatReaderWriter usage. Version, types we consume, known broken types, thread-safety caveats, upgrade procedure, NuGet-vs-vendored decision matrix. - tools/PesChainAudit/ Recursive PES walker — given a 0x33xxxxxx script id, walks all CallPES references and dumps every hook + every referenced ParticleEmitter's parameters. Used to prove no weather PES emits rain particles. - tools/TextureDump/ Dumps texture pixel statistics (alpha histogram, brightness, max) and saves as PNG for visual inspection. - tools/WeatherEnumerator/ Enumerates every DayGroup in a Region, lists weather SkyObjects (Properties & 0x04), dumps GfxObj bounding boxes. - tools/WeatherSetupProbe/ Loads a Setup id, dumps each part's GfxObj + frame + scale + surface. Used to prove weather Setups are 5cm dummy carriers. Worktree feature/sky-fixes is being deleted in a follow-up step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
120 lines
6 KiB
C#
120 lines
6 KiB
C#
// WeatherSetupProbe — Issue #26: dump weather Setups (0x02000BA6, 0x02000588, 0x02000589)
|
|
// to determine if any contain a small near-camera billboard / particle layer.
|
|
using System;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Numerics;
|
|
using DatReaderWriter;
|
|
using DatReaderWriter.DBObjs;
|
|
using DatReaderWriter.Enums;
|
|
using DatReaderWriter.Options;
|
|
using DatReaderWriter.Types;
|
|
using SysEnv = System.Environment;
|
|
|
|
string datDir = SysEnv.GetEnvironmentVariable("ACDREAM_DAT_DIR")
|
|
?? Path.Combine(SysEnv.GetFolderPath(SysEnv.SpecialFolder.UserProfile),
|
|
"Documents", "Asheron's Call");
|
|
Console.WriteLine($"datDir = {datDir}");
|
|
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
|
|
|
uint[] setupIds = { 0x02000BA6u, 0x02000588u, 0x02000589u };
|
|
foreach (uint sid in setupIds) ProbeSetup(dats, sid);
|
|
return 0;
|
|
|
|
static void ProbeSetup(DatCollection dats, uint sid)
|
|
{
|
|
Console.WriteLine();
|
|
Console.WriteLine($"================ Setup 0x{sid:X8} ================");
|
|
if (!dats.TryGet<Setup>(sid, out var s) || s is null)
|
|
{
|
|
Console.WriteLine(" (NOT FOUND)");
|
|
return;
|
|
}
|
|
Console.WriteLine($" Flags={s.Flags} NumParts={s.NumParts}");
|
|
Console.WriteLine($" Setup Height={s.Height:F3} Radius={s.Radius:F3}");
|
|
Console.WriteLine($" SortingSphere center=({s.SortingSphere.Origin.X:F2},{s.SortingSphere.Origin.Y:F2},{s.SortingSphere.Origin.Z:F2}) r={s.SortingSphere.Radius:F3}");
|
|
Console.WriteLine($" SelectionSphere center=({s.SelectionSphere.Origin.X:F2},{s.SelectionSphere.Origin.Y:F2},{s.SelectionSphere.Origin.Z:F2}) r={s.SelectionSphere.Radius:F3}");
|
|
Console.WriteLine($" HoldingLocations={s.HoldingLocations.Count} ConnectionPoints={s.ConnectionPoints.Count} PlacementFrames={s.PlacementFrames.Count} Lights={s.Lights.Count}");
|
|
Console.WriteLine($" DefaultAnimation=0x{(uint)s.DefaultAnimation:X8} DefaultScript=0x{(uint)s.DefaultScript:X8} DefaultMotionTable=0x{(uint)s.DefaultMotionTable:X8}");
|
|
|
|
// PlacementFrames: typically Placement.Default has the per-part frames.
|
|
AnimationFrame? defaultFrame = null;
|
|
foreach (var kv in s.PlacementFrames)
|
|
{
|
|
Console.WriteLine($" Placement[{kv.Key}]: Frames={kv.Value.Frames.Count} Hooks={kv.Value.Hooks.Count}");
|
|
if (defaultFrame is null) defaultFrame = kv.Value;
|
|
if (kv.Key == Placement.Default) defaultFrame = kv.Value;
|
|
}
|
|
|
|
for (int pi = 0; pi < s.Parts.Count; pi++)
|
|
{
|
|
uint partGid = (uint)s.Parts[pi];
|
|
uint parent = (s.Flags.HasFlag(SetupFlags.HasParent) && pi < s.ParentIndex.Count) ? s.ParentIndex[pi] : 0xFFFFFFFFu;
|
|
Vector3 scale = (s.Flags.HasFlag(SetupFlags.HasDefaultScale) && pi < s.DefaultScale.Count) ? s.DefaultScale[pi] : new Vector3(1, 1, 1);
|
|
Frame? frame = (defaultFrame is not null && pi < defaultFrame.Frames.Count) ? defaultFrame.Frames[pi] : null;
|
|
string frameStr = frame is null ? "(no-frame)"
|
|
: $"pos=({frame.Origin.X:F2},{frame.Origin.Y:F2},{frame.Origin.Z:F2}) ori=({frame.Orientation.W:F3},{frame.Orientation.X:F3},{frame.Orientation.Y:F3},{frame.Orientation.Z:F3})";
|
|
string parentStr = parent == 0xFFFFFFFFu ? "ROOT" : parent.ToString();
|
|
Console.WriteLine($" Part[{pi}] GfxObj=0x{partGid:X8} parent={parentStr} scale=({scale.X:F2},{scale.Y:F2},{scale.Z:F2}) {frameStr}");
|
|
DumpGfxObj(dats, partGid, " ");
|
|
}
|
|
}
|
|
|
|
static void DumpGfxObj(DatCollection dats, uint gid, string indent)
|
|
{
|
|
if (!dats.TryGet<GfxObj>(gid, out var g) || g is null)
|
|
{
|
|
Console.WriteLine($"{indent}GfxObj 0x{gid:X8} NOT FOUND");
|
|
return;
|
|
}
|
|
int triCount = g.Polygons.Count;
|
|
int physTri = g.PhysicsPolygons.Count;
|
|
int vertCount = g.VertexArray?.Vertices?.Count ?? 0;
|
|
Vector3 mn = new(float.PositiveInfinity), mx = new(float.NegativeInfinity);
|
|
if (g.VertexArray?.Vertices is not null)
|
|
{
|
|
foreach (var v in g.VertexArray.Vertices.Values)
|
|
{
|
|
mn = Vector3.Min(mn, v.Origin);
|
|
mx = Vector3.Max(mx, v.Origin);
|
|
}
|
|
}
|
|
Vector3 size = (vertCount > 0) ? (mx - mn) : Vector3.Zero;
|
|
float radius = (vertCount > 0) ? size.Length() * 0.5f : 0f;
|
|
string sizeTag = radius < 5 ? " <<TINY"
|
|
: radius < 20 ? " <<SMALL"
|
|
: radius < 200 ? "" : " <<HUGE";
|
|
Console.WriteLine($"{indent}GfxObj 0x{gid:X8} Flags={g.Flags} Surfaces={g.Surfaces.Count} RenderTris={triCount} PhysTris={physTri} Verts={vertCount}");
|
|
if (vertCount > 0)
|
|
{
|
|
Console.WriteLine($"{indent} bbox min=({mn.X:F2},{mn.Y:F2},{mn.Z:F2}) max=({mx.X:F2},{mx.Y:F2},{mx.Z:F2}) size=({size.X:F2},{size.Y:F2},{size.Z:F2}) r~{radius:F2}{sizeTag}");
|
|
Console.WriteLine($"{indent} SortCenter=({g.SortCenter.X:F2},{g.SortCenter.Y:F2},{g.SortCenter.Z:F2})");
|
|
// billboard heuristic: 4 verts and 1-2 polygons
|
|
if (vertCount <= 8 && triCount <= 4)
|
|
Console.WriteLine($"{indent} >>> BILLBOARD CANDIDATE (verts<=8, tris<=4)");
|
|
}
|
|
for (int si = 0; si < g.Surfaces.Count; si++)
|
|
{
|
|
uint surfId = (uint)g.Surfaces[si];
|
|
if (!dats.TryGet<Surface>(surfId, out var surf) || surf is null)
|
|
{
|
|
Console.WriteLine($"{indent} Surf[{si}]=0x{surfId:X8} (not found)");
|
|
continue;
|
|
}
|
|
string tex = "solid";
|
|
if (surf.Type.HasFlag(SurfaceType.Base1Image) || surf.Type.HasFlag(SurfaceType.Base1ClipMap))
|
|
{
|
|
uint stid = (uint)surf.OrigTextureId;
|
|
if (stid != 0 && dats.TryGet<SurfaceTexture>(stid, out var st) && st is not null && st.Textures.Count > 0)
|
|
{
|
|
uint rsid = (uint)st.Textures[0];
|
|
if (dats.TryGet<RenderSurface>(rsid, out var rs) && rs is not null)
|
|
tex = $"{rs.Width}x{rs.Height} {rs.Format} STex=0x{stid:X8}";
|
|
else
|
|
tex = $"STex=0x{stid:X8} (rs miss)";
|
|
}
|
|
else tex = $"STex=0x{stid:X8}";
|
|
}
|
|
Console.WriteLine($"{indent} Surf[{si}]=0x{surfId:X8} Type={surf.Type} Translucency={surf.Translucency:F3} Lum={surf.Luminosity:F3} Diffuse={surf.Diffuse:F3} {tex}");
|
|
}
|
|
}
|