The remaining code-bearing findings from the round review, F4-F16 minus the doc-only items (batched separately): - F4: three client-wide UiButton corpus sweeps (LabelBox path — exactly the 4 Town buttons, confined to chargen; conflicting custom-selection- pair + standard Normal/Highlight media — zero found, no gate tightening needed; per-state label-color map — 209 matches beyond chargen, confirming AP-222's mechanism has always been broadly active since it shipped generically in DatWidgetFactory). - F5/F6: LayoutImporter's Batch C un-consumed-children carve-out now honors a child's own AuthoredInvisible flag (a narrow honor scoped to exactly that carve-out, not the general #408 client-wide one) — the chat transcript's new-text indicator (0x1000048C) was building as a visible phantom element retail never shows; verified both directions against the gold-frame pieces, which do not author Invisible. - F7: BoundedProcessOutputCapture.AppendLine combines the line text and its trailing newline into one buffer and one file open/write/close instead of two. - F9: corrected a stale comment in RuntimeSettingsTargets — #407 split DisplayModeCatalog's Resolutions/WindowedResolutions in two, so the fullscreen validator's own narrower list is now DELIBERATELY different from the Config dropdown's fuller offering, not the "must match" bug the comment described. - F10: documented (not changed) why the LabelBox path's default 3px inset and the face-relative +4px gap in DatWidgetFactory.BuildButton are deliberately different numbers — neither carries a retail citation, and moving either to match the other would be an unfounded guess on a button that currently works correctly. - F11: Heritage/Profession/Summary/Town description pages now compose DatRichText.Compose's result ONCE inside their already revision-gated Refresh, caching the built line list instead of re-wrapping on every draw call. - F14: documented (not changed) why PrivateEntityViewportRenderer's _animatedIds set carrying a reserved-but-never-drawn backdrop id is harmless — BuildDrawEntities already excludes a null/empty backdrop from the actual draw list, so the id is never looked up. - F16: the Summary preview now uses its own render-id pair (SummaryPreviewRenderId/SummaryPreviewBackdropRenderId, 0xDA11D035/ 0xDA11D036) instead of sharing the Appearance page's (0xDA11D032/0xDA11D034) — confirmed by tracing FixedEntityTextureOwnerLease through TextureCache to CompositeTextureArrayCache's shared owner tracker that both pages' previews share ONE process-wide TextureCache, so sharing render ids was a real cross-page texture-release collision (either page's own re-dress or disposal could release the OTHER page's still-active textures), not a theoretical one. F3's own register bookkeeping (AP-229 addendum) and F12's register/AD header-count corrections land in the docs-only commit alongside F15. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
474 lines
22 KiB
C#
474 lines
22 KiB
C#
using System.Linq;
|
|
using System.Numerics;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.Content;
|
|
using AcDream.Content.CharGen;
|
|
using AcDream.Content.Vfx;
|
|
using AcDream.Core.CharGen;
|
|
using DatReaderWriter;
|
|
using DatReaderWriter.Options;
|
|
using Xunit;
|
|
using Xunit.Abstractions;
|
|
|
|
namespace AcDream.App.Tests.Rendering;
|
|
|
|
/// <summary>
|
|
/// Installed-DAT gate for <see cref="ChargenPreviewEntityBuilder"/> —
|
|
/// mirrors <see cref="CornerFloodReplayTests"/>'s env-gated skip pattern
|
|
/// (no unit-testable pure surface exists here the way
|
|
/// <see cref="DollEntityBuilder"/> has one, because THIS builder's whole job
|
|
/// is resolving Setup/GfxObj/Surface/Animation dat data that
|
|
/// <see cref="DollEntityBuilder"/> receives pre-resolved).
|
|
/// </summary>
|
|
public sealed class ChargenPreviewEntityBuilderTests
|
|
{
|
|
private readonly ITestOutputHelper _out;
|
|
public ChargenPreviewEntityBuilderTests(ITestOutputHelper output) => _out = output;
|
|
|
|
[Fact]
|
|
public void TryBuild_AluvianMaleDefaultSelection_ProducesANonEmptyStaticPoseEntity()
|
|
{
|
|
string? datDir = CornerFloodReplayTests.ResolveDatDir();
|
|
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; }
|
|
|
|
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
|
using var adapter = new DatCollectionAdapter(dats);
|
|
|
|
ChargenOptions options = ChargenTableReader.Load(adapter);
|
|
Assert.True(options.TryGetHeritage(1u, out ChargenHeritageOptions? aluvian)); // Aluvian.
|
|
Assert.True(aluvian!.GendersByKey.TryGetValue(1, out ChargenGenderOptions? male));
|
|
|
|
var catalog = new ChargenAppearanceCatalog(adapter);
|
|
ChargenAppearanceSelection selection = ChargenAppearanceSelection.Default with
|
|
{
|
|
HairStyle = male!.HairStyles.Count > 0 ? 0u : ChargenAppearanceSelection.Unset,
|
|
SkinShade = 0.5,
|
|
};
|
|
|
|
bool composed = ChargenAppearanceFactory.TryCompose(
|
|
options, 1u, 1, selection, catalog, catalog, out ChargenAppearanceResult appearance);
|
|
Assert.True(composed);
|
|
Assert.Empty(appearance.MissingPalSetIds);
|
|
Assert.Empty(appearance.MissingClothingTableIds);
|
|
|
|
var animations = new RetailAnimationLoader(adapter);
|
|
var entity = ChargenPreviewEntityBuilder.TryBuild(
|
|
adapter, animations, appearance, heritageId: 1u, Quaternion.Identity, new object());
|
|
|
|
Assert.NotNull(entity);
|
|
Assert.NotEmpty(entity!.MeshRefs);
|
|
Assert.Equal(appearance.SetupId, entity.SourceGfxObjOrSetupId);
|
|
Assert.Equal(ChargenPreviewEntityBuilder.PreviewServerGuid, entity.ServerGuid);
|
|
Assert.Equal(ChargenPreviewEntityBuilder.PreviewRenderId, entity.Id);
|
|
Assert.NotNull(entity.PaletteOverride);
|
|
Assert.Equal(appearance.BasePaletteId, entity.PaletteOverride!.BasePaletteId);
|
|
|
|
_out.WriteLine($"setup=0x{appearance.SetupId:X8} meshRefs={entity.MeshRefs.Count} subPalettes={entity.PaletteOverride.SubPalettes.Count}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// F16 (Campaign CC gate round 1 closeout): the Appearance and Summary
|
|
/// pages must stamp DIFFERENT <see cref="AcDream.Core.World.WorldEntity.Id"/>
|
|
/// values on their preview entities — both feed the SAME shared
|
|
/// <c>TextureCache</c> owner-tracking key
|
|
/// (<c>ChargenPreviewEntityBuilder.SummaryPreviewRenderId</c>'s own doc
|
|
/// has the full collision trace). Pins BOTH halves: the constants
|
|
/// themselves are distinct, AND the explicit <c>renderId</c> parameter
|
|
/// actually reaches the built entity (not silently ignored).
|
|
/// </summary>
|
|
[Fact]
|
|
public void TryBuild_ExplicitRenderId_StampsThatIdOnTheEntity_DistinctFromTheAppearanceDefault()
|
|
{
|
|
Assert.NotEqual(
|
|
ChargenPreviewEntityBuilder.PreviewRenderId,
|
|
ChargenPreviewEntityBuilder.SummaryPreviewRenderId);
|
|
Assert.NotEqual(
|
|
ChargenPreviewEntityBuilder.PreviewBackdropRenderId,
|
|
ChargenPreviewEntityBuilder.SummaryPreviewBackdropRenderId);
|
|
|
|
string? datDir = CornerFloodReplayTests.ResolveDatDir();
|
|
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; }
|
|
|
|
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
|
using var adapter = new DatCollectionAdapter(dats);
|
|
|
|
ChargenOptions options = ChargenTableReader.Load(adapter);
|
|
Assert.True(options.TryGetHeritage(1u, out ChargenHeritageOptions? aluvian)); // Aluvian.
|
|
Assert.True(aluvian!.GendersByKey.TryGetValue(1, out ChargenGenderOptions? male));
|
|
|
|
var catalog = new ChargenAppearanceCatalog(adapter);
|
|
ChargenAppearanceSelection selection = ChargenAppearanceSelection.Default with
|
|
{
|
|
HairStyle = male!.HairStyles.Count > 0 ? 0u : ChargenAppearanceSelection.Unset,
|
|
SkinShade = 0.5,
|
|
};
|
|
|
|
bool composed = ChargenAppearanceFactory.TryCompose(
|
|
options, 1u, 1, selection, catalog, catalog, out ChargenAppearanceResult appearance);
|
|
Assert.True(composed);
|
|
|
|
var animations = new RetailAnimationLoader(adapter);
|
|
var entity = ChargenPreviewEntityBuilder.TryBuild(
|
|
adapter, animations, appearance, heritageId: 1u, Quaternion.Identity, new object(),
|
|
renderId: ChargenPreviewEntityBuilder.SummaryPreviewRenderId);
|
|
|
|
Assert.NotNull(entity);
|
|
Assert.Equal(ChargenPreviewEntityBuilder.SummaryPreviewRenderId, entity!.Id);
|
|
Assert.NotEqual(ChargenPreviewEntityBuilder.PreviewRenderId, entity.Id);
|
|
}
|
|
|
|
[Fact]
|
|
public void TryBuild_UnknownSetupId_ReturnsNull()
|
|
{
|
|
string? datDir = CornerFloodReplayTests.ResolveDatDir();
|
|
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; }
|
|
|
|
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
|
using var adapter = new DatCollectionAdapter(dats);
|
|
var animations = new RetailAnimationLoader(adapter);
|
|
|
|
var bogusAppearance = new ChargenAppearanceResult(
|
|
SetupId: 0x0200_FFFFu, // Not a real installed Setup id.
|
|
BasePaletteId: 0u,
|
|
ObjDesc: ChargenObjDesc.Empty,
|
|
MissingPalSetIds: [],
|
|
MissingClothingTableIds: [],
|
|
ClothingTablesMissingBaseEffectForSetup: []);
|
|
|
|
var entity = ChargenPreviewEntityBuilder.TryBuild(
|
|
adapter, animations, bogusAppearance, heritageId: 1u, Quaternion.Identity, new object());
|
|
|
|
Assert.Null(entity);
|
|
}
|
|
|
|
[Fact]
|
|
public void TryBuild_OlthoiHeritage_ResolvesADifferentRestPoseDidThanStandardHeritages()
|
|
{
|
|
string? datDir = CornerFloodReplayTests.ResolveDatDir();
|
|
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; }
|
|
|
|
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
|
using var adapter = new DatCollectionAdapter(dats);
|
|
|
|
ChargenOptions options = ChargenTableReader.Load(adapter);
|
|
Assert.True(options.TryGetHeritage(12u, out ChargenHeritageOptions? olthoi)); // Olthoi.
|
|
Assert.True(olthoi!.GendersByKey.TryGetValue(1, out ChargenGenderOptions? male)
|
|
|| olthoi.GendersByKey.TryGetValue(2, out male));
|
|
Assert.NotNull(male);
|
|
int genderKey = olthoi.GendersByKey.First(kv => ReferenceEquals(kv.Value, male)).Key;
|
|
|
|
var catalog = new ChargenAppearanceCatalog(adapter);
|
|
var animations = new RetailAnimationLoader(adapter);
|
|
|
|
bool composed = ChargenAppearanceFactory.TryCompose(
|
|
options, 12u, genderKey, ChargenAppearanceSelection.Default with { SkinShade = 0.5 },
|
|
catalog, catalog, out ChargenAppearanceResult appearance);
|
|
Assert.True(composed);
|
|
|
|
var entity = ChargenPreviewEntityBuilder.TryBuild(
|
|
adapter, animations, appearance, heritageId: 12u, Quaternion.Identity, new object());
|
|
|
|
// Just proves the Olthoi branch doesn't throw / silently fall through to
|
|
// "no mesh" — the exact pose DID differs internally (0x10000011 vs
|
|
// 0x10000005) but both should still resolve a drawable mesh from Olthoi's
|
|
// own Setup.
|
|
Assert.NotNull(entity);
|
|
Assert.NotEmpty(entity!.MeshRefs);
|
|
}
|
|
|
|
/// <summary>
|
|
/// CC6b: <c>TryBuildAnimated</c> resolves a real idle Animation (retail's
|
|
/// <c>m_didAnimation</c>) against the installed EoR dat, with a usable
|
|
/// frame range and a non-empty drawable-part list a
|
|
/// <c>ChargenPreviewAnimator</c> can drive.
|
|
/// </summary>
|
|
[Fact]
|
|
public void TryBuildAnimated_AluvianMaleDefaultSelection_ResolvesARealIdleCycle()
|
|
{
|
|
string? datDir = CornerFloodReplayTests.ResolveDatDir();
|
|
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; }
|
|
|
|
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
|
using var adapter = new DatCollectionAdapter(dats);
|
|
|
|
ChargenOptions options = ChargenTableReader.Load(adapter);
|
|
Assert.True(options.TryGetHeritage(1u, out ChargenHeritageOptions? aluvian));
|
|
Assert.True(aluvian!.GendersByKey.TryGetValue(1, out ChargenGenderOptions? male));
|
|
|
|
var catalog = new ChargenAppearanceCatalog(adapter);
|
|
ChargenAppearanceSelection selection = ChargenAppearanceSelection.Default with
|
|
{
|
|
HairStyle = male!.HairStyles.Count > 0 ? 0u : ChargenAppearanceSelection.Unset,
|
|
SkinShade = 0.5,
|
|
};
|
|
|
|
bool composed = ChargenAppearanceFactory.TryCompose(
|
|
options, 1u, 1, selection, catalog, catalog, out ChargenAppearanceResult appearance);
|
|
Assert.True(composed);
|
|
|
|
var animations = new RetailAnimationLoader(adapter);
|
|
ChargenPreviewAnimatedBuild? build = ChargenPreviewEntityBuilder.TryBuildAnimated(
|
|
adapter, animations, appearance, heritageId: 1u, Quaternion.Identity, new object());
|
|
|
|
Assert.NotNull(build);
|
|
Assert.NotEmpty(build!.DrawableParts);
|
|
Assert.NotEmpty(build.RestMeshRefs);
|
|
Assert.NotNull(build.IdleAnimation);
|
|
Assert.True(build.IdleHighFrame >= build.IdleLowFrame);
|
|
Assert.True(build.IdleAnimation!.PartFrames.Count > build.IdleHighFrame);
|
|
|
|
// Live end-to-end: an Animator built from this resolves a non-empty,
|
|
// playable preview — retail's true default (idle playing), not the
|
|
// frozen rest pose TryBuild alone still returns.
|
|
var animator = new ChargenPreviewAnimator(build);
|
|
Assert.False(animator.IsZoomedIn);
|
|
Assert.NotEmpty(animator.Entity.MeshRefs);
|
|
|
|
animator.Tick(1f / 30f); // one frame's worth — must not throw or empty the mesh.
|
|
Assert.NotEmpty(animator.Entity.MeshRefs);
|
|
}
|
|
|
|
[Fact]
|
|
public void TryBuildAnimated_UnknownSetupId_ReturnsNull()
|
|
{
|
|
string? datDir = CornerFloodReplayTests.ResolveDatDir();
|
|
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; }
|
|
|
|
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
|
using var adapter = new DatCollectionAdapter(dats);
|
|
var animations = new RetailAnimationLoader(adapter);
|
|
|
|
var bogusAppearance = new ChargenAppearanceResult(
|
|
SetupId: 0x0200_FFFFu,
|
|
BasePaletteId: 0u,
|
|
ObjDesc: ChargenObjDesc.Empty,
|
|
MissingPalSetIds: [],
|
|
MissingClothingTableIds: [],
|
|
ClothingTablesMissingBaseEffectForSetup: []);
|
|
|
|
ChargenPreviewAnimatedBuild? build = ChargenPreviewEntityBuilder.TryBuildAnimated(
|
|
adapter, animations, bogusAppearance, heritageId: 1u, Quaternion.Identity, new object());
|
|
|
|
Assert.Null(build);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Decomp-verified quirk (<c>gmCG3DView</c>'s ctor / <c>::Update</c>,
|
|
/// pseudo-C ~0x004ee7e9/0x004ee7ff and ~0x004ee892/0x004ee8a8): Olthoi
|
|
/// and OlthoiAcid use the SAME enum key (0x10000011 / 0x10000013) for
|
|
/// BOTH the live idle DID (<c>m_didAnimation</c>) and the rest DID
|
|
/// (<c>m_didAnimationRest</c>) — every standard heritage uses two
|
|
/// DIFFERENT keys (0x10000006 idle vs 0x10000005 rest). This proves the
|
|
/// SHARED enum key resolves to a real installed Animation DID (the same
|
|
/// <c>RetailHeldPose.ResolvePoseDid</c> call
|
|
/// <c>ChargenPreviewEntityBuilder</c>'s <c>ResolveIdleAnimEnum</c> AND
|
|
/// <c>ResolveRestPoseEnum</c> both return for these two heritages) — the
|
|
/// enum-key identity itself is source-verified (both private methods
|
|
/// literally return the SAME numeric constant for Olthoi/OlthoiAcid, see
|
|
/// their own doc comments), so a single resolution here is enough to
|
|
/// confirm the shared key is not a dead/unresolvable id.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData(0x10000011u)] // Olthoi's shared idle/rest enum key.
|
|
[InlineData(0x10000013u)] // OlthoiAcid's shared idle/rest enum key.
|
|
public void OlthoiFamily_SharedIdleRestEnumKey_ResolvesToARealInstalledDid(uint sharedEnumKey)
|
|
{
|
|
string? datDir = CornerFloodReplayTests.ResolveDatDir();
|
|
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; }
|
|
|
|
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
|
using var adapter = new DatCollectionAdapter(dats);
|
|
|
|
uint did = RetailHeldPose.ResolvePoseDid(adapter, sharedEnumKey);
|
|
|
|
Assert.NotEqual(0u, did);
|
|
Assert.Equal(0x03u, did >> 24); // resolves to a real Animation DID.
|
|
}
|
|
|
|
/// <summary>
|
|
/// Extends <see cref="TryBuild_OlthoiHeritage_ResolvesADifferentRestPoseDidThanStandardHeritages"/>
|
|
/// to the idle side: <c>TryBuildAnimated</c> resolves a real idle
|
|
/// Animation for Olthoi too (not just the rest pose the older
|
|
/// <c>TryBuild</c>-only test covers), so an Olthoi
|
|
/// <c>ChargenPreviewAnimator</c> actually plays instead of silently
|
|
/// falling back to the rest-only pose.
|
|
/// </summary>
|
|
[Fact]
|
|
public void TryBuildAnimated_OlthoiHeritage_ResolvesARealIdleAnimationToo()
|
|
{
|
|
string? datDir = CornerFloodReplayTests.ResolveDatDir();
|
|
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; }
|
|
|
|
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
|
using var adapter = new DatCollectionAdapter(dats);
|
|
|
|
ChargenOptions options = ChargenTableReader.Load(adapter);
|
|
Assert.True(options.TryGetHeritage(12u, out ChargenHeritageOptions? olthoi));
|
|
Assert.True(olthoi!.GendersByKey.TryGetValue(1, out ChargenGenderOptions? male)
|
|
|| olthoi.GendersByKey.TryGetValue(2, out male));
|
|
Assert.NotNull(male);
|
|
int genderKey = olthoi.GendersByKey.First(kv => ReferenceEquals(kv.Value, male)).Key;
|
|
|
|
var catalog = new ChargenAppearanceCatalog(adapter);
|
|
var animations = new RetailAnimationLoader(adapter);
|
|
bool composed = ChargenAppearanceFactory.TryCompose(
|
|
options, 12u, genderKey, ChargenAppearanceSelection.Default with { SkinShade = 0.5 },
|
|
catalog, catalog, out ChargenAppearanceResult appearance);
|
|
Assert.True(composed);
|
|
|
|
ChargenPreviewAnimatedBuild? build = ChargenPreviewEntityBuilder.TryBuildAnimated(
|
|
adapter, animations, appearance, heritageId: 12u, Quaternion.Identity, new object());
|
|
|
|
Assert.NotNull(build);
|
|
Assert.NotNull(build!.IdleAnimation);
|
|
|
|
var animator = new ChargenPreviewAnimator(build);
|
|
Assert.False(animator.IsZoomedIn);
|
|
Assert.NotEmpty(animator.Entity.MeshRefs);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Campaign CC gate round 1, Batch D (GF-7/GF-14): the ENVIRONMENT
|
|
/// backdrop entity resolves against the installed dat for a real
|
|
/// heritage. Aluvian's own <c>EnvironmentSetupId</c> was already parsed
|
|
/// (<see cref="ChargenTableReader"/>) but never consumed before this fix.
|
|
/// </summary>
|
|
[Fact]
|
|
public void TryBuildBackdrop_AluvianHeritage_ResolvesANonEmptyMesh()
|
|
{
|
|
string? datDir = CornerFloodReplayTests.ResolveDatDir();
|
|
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; }
|
|
|
|
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
|
using var adapter = new DatCollectionAdapter(dats);
|
|
|
|
ChargenOptions options = ChargenTableReader.Load(adapter);
|
|
Assert.True(options.TryGetHeritage(1u, out ChargenHeritageOptions? aluvian)); // Aluvian.
|
|
if (aluvian!.EnvironmentSetupId == 0u)
|
|
{
|
|
_out.WriteLine("SKIP: installed dat's Aluvian heritage authors no EnvironmentSetupId.");
|
|
return;
|
|
}
|
|
|
|
var entity = ChargenPreviewEntityBuilder.TryBuildBackdrop(
|
|
adapter, aluvian.EnvironmentSetupId, new object());
|
|
|
|
Assert.NotNull(entity);
|
|
Assert.NotEmpty(entity!.MeshRefs);
|
|
Assert.Equal(aluvian.EnvironmentSetupId, entity.SourceGfxObjOrSetupId);
|
|
Assert.Equal(ChargenPreviewEntityBuilder.PreviewBackdropServerGuid, entity.ServerGuid);
|
|
Assert.Equal(ChargenPreviewEntityBuilder.PreviewBackdropRenderId, entity.Id);
|
|
// Decomp-cited (gmCG3DView::Update ~0x004eed2f): CPhysicsObj::makeObject
|
|
// never receives an explicit position/orientation for the backdrop —
|
|
// it sits at the private scene's origin with identity orientation,
|
|
// same as the player object's own default placement.
|
|
Assert.Equal(Vector3.Zero, entity.Position);
|
|
Assert.Equal(Quaternion.Identity, entity.Rotation);
|
|
|
|
_out.WriteLine($"backdropSetup=0x{aluvian.EnvironmentSetupId:X8} meshRefs={entity.MeshRefs.Count}");
|
|
}
|
|
|
|
/// <summary>F16 (Campaign CC gate round 1 closeout): the backdrop's own
|
|
/// explicit <c>renderId</c> parameter reaches the built entity, the
|
|
/// same shape as <see cref="TryBuild_ExplicitRenderId_StampsThatIdOnTheEntity_DistinctFromTheAppearanceDefault"/>
|
|
/// pins for the main preview entity.</summary>
|
|
[Fact]
|
|
public void TryBuildBackdrop_ExplicitRenderId_StampsThatIdOnTheEntity()
|
|
{
|
|
string? datDir = CornerFloodReplayTests.ResolveDatDir();
|
|
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; }
|
|
|
|
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
|
using var adapter = new DatCollectionAdapter(dats);
|
|
|
|
ChargenOptions options = ChargenTableReader.Load(adapter);
|
|
Assert.True(options.TryGetHeritage(1u, out ChargenHeritageOptions? aluvian)); // Aluvian.
|
|
if (aluvian!.EnvironmentSetupId == 0u)
|
|
{
|
|
_out.WriteLine("SKIP: installed dat's Aluvian heritage authors no EnvironmentSetupId.");
|
|
return;
|
|
}
|
|
|
|
var entity = ChargenPreviewEntityBuilder.TryBuildBackdrop(
|
|
adapter, aluvian.EnvironmentSetupId, new object(),
|
|
renderId: ChargenPreviewEntityBuilder.SummaryPreviewBackdropRenderId);
|
|
|
|
Assert.NotNull(entity);
|
|
Assert.Equal(ChargenPreviewEntityBuilder.SummaryPreviewBackdropRenderId, entity!.Id);
|
|
Assert.NotEqual(ChargenPreviewEntityBuilder.PreviewBackdropRenderId, entity.Id);
|
|
}
|
|
|
|
/// <summary>Retail's own gate at 0x004eed29 (<c>if (eax_32 != INVALID_DID.id)</c>)
|
|
/// skips creating a backdrop object entirely when the heritage authors no
|
|
/// environment Setup — id 0/unset must return null, not an empty entity.</summary>
|
|
[Fact]
|
|
public void TryBuildBackdrop_UnsetEnvironmentSetupId_ReturnsNull()
|
|
{
|
|
string? datDir = CornerFloodReplayTests.ResolveDatDir();
|
|
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; }
|
|
|
|
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
|
using var adapter = new DatCollectionAdapter(dats);
|
|
|
|
var entity = ChargenPreviewEntityBuilder.TryBuildBackdrop(adapter, 0u, new object());
|
|
|
|
Assert.Null(entity);
|
|
}
|
|
|
|
[Fact]
|
|
public void TryBuildBackdrop_UnknownSetupId_ReturnsNull()
|
|
{
|
|
string? datDir = CornerFloodReplayTests.ResolveDatDir();
|
|
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; }
|
|
|
|
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
|
using var adapter = new DatCollectionAdapter(dats);
|
|
|
|
var entity = ChargenPreviewEntityBuilder.TryBuildBackdrop(
|
|
adapter, 0x0200_FFFFu, new object());
|
|
|
|
Assert.Null(entity);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Live-DAT measurement (not assumed): every one of the 13 retail
|
|
/// heritages' <c>EnvironmentSetupId</c> resolves to a real installed
|
|
/// Setup with at least one drawable part. Reports precisely which
|
|
/// heritage(s) don't, if any, instead of assuming full coverage.
|
|
/// </summary>
|
|
[Fact]
|
|
public void TryBuildBackdrop_AllThirteenHeritages_ResolveOrAreReportedByName()
|
|
{
|
|
string? datDir = CornerFloodReplayTests.ResolveDatDir();
|
|
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; }
|
|
|
|
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
|
using var adapter = new DatCollectionAdapter(dats);
|
|
|
|
ChargenOptions options = ChargenTableReader.Load(adapter);
|
|
Assert.Equal(13, options.HeritagesById.Count);
|
|
|
|
var unresolved = new List<string>();
|
|
foreach (KeyValuePair<uint, ChargenHeritageOptions> pair in options.HeritagesById)
|
|
{
|
|
ChargenHeritageOptions heritage = pair.Value;
|
|
var entity = ChargenPreviewEntityBuilder.TryBuildBackdrop(
|
|
adapter, heritage.EnvironmentSetupId, new object());
|
|
if (entity is null)
|
|
{
|
|
unresolved.Add(
|
|
$"{heritage.Name} (id={pair.Key}, environmentSetupId=0x{heritage.EnvironmentSetupId:X8})");
|
|
}
|
|
}
|
|
|
|
_out.WriteLine(unresolved.Count == 0
|
|
? "All 13 heritages resolved a drawable environment backdrop."
|
|
: "Unresolved: " + string.Join("; ", unresolved));
|
|
|
|
// Measured, not assumed: report the exact set rather than asserting
|
|
// blind 13/13 in case the installed dat is missing one.
|
|
Assert.True(
|
|
unresolved.Count <= 13,
|
|
"Sanity bound only — the WriteLine above is the real measurement.");
|
|
}
|
|
}
|