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; /// /// Installed-DAT gate for — /// mirrors 's env-gated skip pattern /// (no unit-testable pure surface exists here the way /// has one, because THIS builder's whole job /// is resolving Setup/GfxObj/Surface/Animation dat data that /// receives pre-resolved). /// 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}"); } /// /// F16 (Campaign CC gate round 1 closeout): the Appearance and Summary /// pages must stamp DIFFERENT /// values on their preview entities — both feed the SAME shared /// TextureCache owner-tracking key /// (ChargenPreviewEntityBuilder.SummaryPreviewRenderId's own doc /// has the full collision trace). Pins BOTH halves: the constants /// themselves are distinct, AND the explicit renderId parameter /// actually reaches the built entity (not silently ignored). /// [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); } /// /// CC6b: TryBuildAnimated resolves a real idle Animation (retail's /// m_didAnimation) against the installed EoR dat, with a usable /// frame range and a non-empty drawable-part list a /// ChargenPreviewAnimator can drive. /// [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); } /// /// Decomp-verified quirk (gmCG3DView's ctor / ::Update, /// pseudo-C ~0x004ee7e9/0x004ee7ff and ~0x004ee892/0x004ee8a8): Olthoi /// and OlthoiAcid use the SAME enum key (0x10000011 / 0x10000013) for /// BOTH the live idle DID (m_didAnimation) and the rest DID /// (m_didAnimationRest) — 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 /// RetailHeldPose.ResolvePoseDid call /// ChargenPreviewEntityBuilder's ResolveIdleAnimEnum AND /// ResolveRestPoseEnum 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. /// [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. } /// /// Extends /// to the idle side: TryBuildAnimated resolves a real idle /// Animation for Olthoi too (not just the rest pose the older /// TryBuild-only test covers), so an Olthoi /// ChargenPreviewAnimator actually plays instead of silently /// falling back to the rest-only pose. /// [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); } /// /// 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 EnvironmentSetupId was already parsed /// () but never consumed before this fix. /// [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}"); } /// F16 (Campaign CC gate round 1 closeout): the backdrop's own /// explicit renderId parameter reaches the built entity, the /// same shape as /// pins for the main preview entity. [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); } /// Retail's own gate at 0x004eed29 (if (eax_32 != INVALID_DID.id)) /// skips creating a backdrop object entirely when the heritage authors no /// environment Setup — id 0/unset must return null, not an empty entity. [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); } /// /// Live-DAT measurement (not assumed): every one of the 13 retail /// heritages' EnvironmentSetupId 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. /// [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(); foreach (KeyValuePair 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."); } }