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}"); } [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); } }