fix(render): hide editor-only placement markers in dungeons — port retail's degrade-to-nothing (#136)
The "red cone" (+ green floor petals) in the 0x0007 Town Network dungeon is a dat
EnvCell static object (Setup 0x02000C39 / GfxObj 0x010028CA) using pure red/green
MARKER textures (0x08000109 / 0x0800010A). It is an EDITOR-ONLY placement marker:
its DIDDegrade table 0x11000118 is {slot0 Id=mesh MaxDist=0, slot1 Id=0 MaxDist=FLT_MAX},
i.e. visible ONLY at distance 0 (the WorldBuilder editor origin) and degraded to
GfxObj id 0 (nothing) at any real distance. retail's distance-based degrade
(CPhysicsPart::UpdateViewerDistance 0x0050E030 -> Draw 0x0050D7A0) therefore never
draws it in the live client.
acdream's render pipeline is extracted from WorldBuilder, which (being an editor)
renders every cell static's base mesh directly and has NO degrade handling at all
(zero DIDDegrade references in references/WorldBuilder) — so acdream inherited the
"show the marker" behavior and drew it forever. It only became visible now because
the #135 login-into-dungeon fix drops the player at the exact saved spawn next to it.
Fix: GfxObjDegradeResolver.IsRuntimeHiddenMarker() detects the editor-marker pattern
(HasDIDDegrade + Degrades[0].MaxDist==0 + a degrade entry with Id==0). The EnvCell
static-object hydration (GameWindow ~5793) skips such GfxObjs — whole-stab for bare
GfxObj stabs, per-part for Setup stabs (an all-marker Setup then drops via
meshRefs.Count==0). This is the faithful equivalent of retail's runtime degrade for
static geometry (always viewed at distance > 0); real LOD objects (slot0.MaxDist>0)
and degrade-to-real-mesh objects are untouched.
Diagnosis was extensive (geometry-not-VFX via particle-off; texture-not-lighting via
flat-ambient frame dumps; per-surface runtime decode pinned the red/green marker
surfaces; a draw-time probe pinned the dat-static entity id; a dat dump of the Setup +
degrade table confirmed the editor-marker pattern). Verified live via a frame dump:
the red cone + green petals are gone, all real dungeon decorations still render.
4 new GfxObjDegradeResolver unit tests cover the marker / normal-LOD / no-table /
degrades-to-real-mesh cases.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b4ed8e7908
commit
6f81e2c91d
3 changed files with 151 additions and 0 deletions
|
|
@ -179,4 +179,89 @@ public class GfxObjDegradeResolverTests
|
|||
Assert.Equal(baseId, resolvedId);
|
||||
Assert.Null(resolvedGfx);
|
||||
}
|
||||
|
||||
// ── #136: editor-only placement marker detection ──────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// The #136 dungeon "cone": its degrade table's slot 0 is visible ONLY at distance 0
|
||||
/// (MaxDist=0) and the table degrades to GfxObj id 0 (= nothing) at real distance.
|
||||
/// Retail's distance degrade never draws it in the live client; we must skip it.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IsRuntimeHiddenMarker_EditorMarkerDegradingToNothing_True()
|
||||
{
|
||||
const uint markerGfx = 0x010028CAu;
|
||||
const uint degradeId = 0x11000118u;
|
||||
var gfx = new GfxObj { Flags = GfxObjFlags.HasDIDDegrade, DIDDegrade = degradeId };
|
||||
var info = new GfxObjDegradeInfo
|
||||
{
|
||||
Degrades =
|
||||
{
|
||||
new GfxObjInfo { Id = markerGfx, MaxDist = 0f },
|
||||
new GfxObjInfo { Id = 0u, MaxDist = float.MaxValue },
|
||||
},
|
||||
};
|
||||
var gfxObjs = new Dictionary<uint, GfxObj> { [markerGfx] = gfx };
|
||||
var infos = new Dictionary<uint, GfxObjDegradeInfo> { [degradeId] = info };
|
||||
|
||||
Assert.True(GfxObjDegradeResolver.IsRuntimeHiddenMarker(
|
||||
id => gfxObjs.GetValueOrDefault(id), id => infos.GetValueOrDefault(id), markerGfx));
|
||||
}
|
||||
|
||||
/// <summary>A real LOD object — slot 0 visible out to a real distance (MaxDist>0) —
|
||||
/// is NOT a marker, even though it degrades further.</summary>
|
||||
[Fact]
|
||||
public void IsRuntimeHiddenMarker_NormalLodObject_False()
|
||||
{
|
||||
const uint baseId = 0x01000055u;
|
||||
const uint degradeId = 0x110006D0u;
|
||||
var gfx = new GfxObj { Flags = GfxObjFlags.HasDIDDegrade, DIDDegrade = degradeId };
|
||||
var info = new GfxObjDegradeInfo
|
||||
{
|
||||
Degrades =
|
||||
{
|
||||
new GfxObjInfo { Id = 0x01001795u, MaxDist = 25f },
|
||||
new GfxObjInfo { Id = 0u, MaxDist = float.MaxValue },
|
||||
},
|
||||
};
|
||||
var gfxObjs = new Dictionary<uint, GfxObj> { [baseId] = gfx };
|
||||
var infos = new Dictionary<uint, GfxObjDegradeInfo> { [degradeId] = info };
|
||||
|
||||
Assert.False(GfxObjDegradeResolver.IsRuntimeHiddenMarker(
|
||||
id => gfxObjs.GetValueOrDefault(id), id => infos.GetValueOrDefault(id), baseId));
|
||||
}
|
||||
|
||||
/// <summary>No degrade table at all → not a marker.</summary>
|
||||
[Fact]
|
||||
public void IsRuntimeHiddenMarker_NoDegradeTable_False()
|
||||
{
|
||||
const uint baseId = 0x01001212u;
|
||||
var gfx = new GfxObj { Flags = 0, DIDDegrade = 0 };
|
||||
var gfxObjs = new Dictionary<uint, GfxObj> { [baseId] = gfx };
|
||||
Assert.False(GfxObjDegradeResolver.IsRuntimeHiddenMarker(
|
||||
id => gfxObjs.GetValueOrDefault(id), _ => null, baseId));
|
||||
}
|
||||
|
||||
/// <summary>slot 0 is editor-only (MaxDist=0) but degrades to a REAL mesh (no id-0
|
||||
/// entry) — a genuine close-only LOD, not an invisible marker. Do NOT skip.</summary>
|
||||
[Fact]
|
||||
public void IsRuntimeHiddenMarker_EditorSlotButDegradesToRealMesh_False()
|
||||
{
|
||||
const uint baseId = 0x01002000u;
|
||||
const uint degradeId = 0x11002000u;
|
||||
var gfx = new GfxObj { Flags = GfxObjFlags.HasDIDDegrade, DIDDegrade = degradeId };
|
||||
var info = new GfxObjDegradeInfo
|
||||
{
|
||||
Degrades =
|
||||
{
|
||||
new GfxObjInfo { Id = baseId, MaxDist = 0f },
|
||||
new GfxObjInfo { Id = 0x01002001u, MaxDist = float.MaxValue },
|
||||
},
|
||||
};
|
||||
var gfxObjs = new Dictionary<uint, GfxObj> { [baseId] = gfx };
|
||||
var infos = new Dictionary<uint, GfxObjDegradeInfo> { [degradeId] = info };
|
||||
|
||||
Assert.False(GfxObjDegradeResolver.IsRuntimeHiddenMarker(
|
||||
id => gfxObjs.GetValueOrDefault(id), id => infos.GetValueOrDefault(id), baseId));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue