using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Numerics; using AcDream.Content; using AcDream.Content.CharGen; using AcDream.Core.CharGen; using DatReaderWriter; using DatReaderWriter.DBObjs; using DatReaderWriter.Enums; using DatReaderWriter.Lib.IO; using DatReaderWriter.Types; using DatCharGen = DatReaderWriter.DBObjs.CharGen; using DatObjDesc = DatReaderWriter.Types.ObjDesc; namespace AcDream.Content.Tests.CharGen; /// /// Tests for 's pure projection /// () against a hand-built /// fixture — proves the field mapping without /// depending on the installed DAT. The installed-DAT read path itself is /// covered by . /// public sealed class ChargenTableReaderTests { private static PStringBase Str(string value) { var s = new PStringBase(); s.Value = value; return s; } private static QualifiedDataId Qdi(uint id) where T : DBObj, new() { var qdi = new QualifiedDataId(); qdi.DataId = id; return qdi; } private static DatObjDesc MakeObjDesc(uint paletteId, byte partIndex, uint oldTex, uint newTex) { var od = new DatObjDesc(); od.PaletteId = new PackedQualifiedDataId(); od.PaletteId.DataId = paletteId; var sub = new SubPalette(); sub.SubId = new PackedQualifiedDataId(); sub.SubId.DataId = 0x04000099u; sub.Offset = 8; sub.NumColors = 24; od.SubPalettes.Add(sub); var tex = new TextureMapChange(); tex.PartIndex = partIndex; tex.OldTexture = new PackedQualifiedDataId(); tex.OldTexture.DataId = oldTex; tex.NewTexture = new PackedQualifiedDataId(); tex.NewTexture.DataId = newTex; od.TextureChanges.Add(tex); var part = new AnimationPartChange(); part.PartIndex = 2; part.PartId = new PackedQualifiedDataId(); part.PartId.DataId = 0x0100ABCDu; od.AnimPartChanges.Add(part); return od; } private static DatCharGen BuildFixture() { var table = new DatCharGen(); var area = new StartingArea(); area.Name = Str("Holtburg"); var position = new Position(); position.CellId = 0xA9B40000u; position.Frame = new Frame { Origin = new Vector3(1f, 2f, 3f), Orientation = new Quaternion(0f, 0f, 0f, 1f), }; area.Locations.Add(position); table.StartingAreas.Add(area); var heritage = new HeritageGroupCG(); heritage.Name = Str("Aluvian"); heritage.IconId = Qdi(0x06000001u); heritage.SetupId = Qdi(0x02000010u); heritage.EnvironmentSetupId = Qdi(0x02000020u); heritage.AttributeCredits = 180u; heritage.SkillCredits = 50u; heritage.PrimaryStartAreas.Add(0); heritage.SecondaryStartAreas.Add(0); var skill = new SkillCG(); skill.Id = DatReaderWriter.Enums.SkillId.Axe; skill.NormalCost = 4; skill.PrimaryCost = 12; heritage.Skills.Add(skill); var template = new TemplateCG(); template.Name = Str("Soldier"); template.IconId = Qdi(0x06000002u); template.Title = 42u; template.Strength = 40; template.Endurance = 40; template.Coordination = 40; template.Quickness = 20; template.Focus = 20; template.Self = 20; template.NormalSkills.Add(DatReaderWriter.Enums.SkillId.Axe); template.PrimarySkills.Add(DatReaderWriter.Enums.SkillId.MeleeDefense); heritage.Templates.Add(template); var sex = new SexCG(); sex.Name = Str("Male"); sex.Scale = 1000000u; sex.SetupId = Qdi(0x02000030u); sex.SoundTable = Qdi(0x22000001u); sex.IconId = Qdi(0x06000003u); sex.BasePalette = Qdi(0x04000001u); sex.SkinPalSet = Qdi(0x04001001u); sex.PhysicsTable = Qdi(0x0D000001u); sex.MotionTable = Qdi(0x09000001u); sex.CombatTable = Qdi(0x0F000001u); sex.BaseObjDesc = MakeObjDesc(0x04000002u, 0, 0x05000001u, 0x05000002u); sex.HairColors.Add(0x0Au); sex.EyeColors.Add(0x0Bu); sex.ClothingColors.Add(0x0Cu); var hairStyle = new HairStyleCG(); hairStyle.IconId = Qdi(0x06000004u); hairStyle.Bald = false; hairStyle.AlternateSetup = 0x02000099u; hairStyle.ObjDesc = MakeObjDesc(0x04000003u, 1, 0x05000003u, 0x05000004u); sex.HairStyles.Add(hairStyle); var eyeStrip = new EyeStripCG(); eyeStrip.IconId = Qdi(0x06000005u); eyeStrip.BaldIconId = 0x06000006u; eyeStrip.ObjDesc = MakeObjDesc(0x04000004u, 2, 0x05000005u, 0x05000006u); eyeStrip.BaldObjDesc = MakeObjDesc(0x04000005u, 3, 0x05000007u, 0x05000008u); sex.EyeStrips.Add(eyeStrip); var noseStrip = new FaceStripCG(); noseStrip.IconId = Qdi(0x06000007u); noseStrip.ObjDesc = MakeObjDesc(0x04000006u, 4, 0x05000009u, 0x0500000Au); sex.NoseStrips.Add(noseStrip); var mouthStrip = new FaceStripCG(); mouthStrip.IconId = Qdi(0x06000008u); mouthStrip.ObjDesc = MakeObjDesc(0x04000007u, 5, 0x0500000Bu, 0x0500000Cu); sex.MouthStrips.Add(mouthStrip); var headgear = new GearCG(); headgear.Name = Str("Leather Cap"); headgear.ClothingTable = Qdi(0x31000001u); headgear.WeenieDefault = 300001u; sex.Headgears.Add(headgear); var shirt = new GearCG(); shirt.Name = Str("Tunic"); shirt.ClothingTable = Qdi(0x31000002u); shirt.WeenieDefault = 300002u; sex.Shirts.Add(shirt); var pants = new GearCG(); pants.Name = Str("Breeches"); pants.ClothingTable = Qdi(0x31000003u); pants.WeenieDefault = 300003u; sex.Pants.Add(pants); var footwear = new GearCG(); footwear.Name = Str("Boots"); footwear.ClothingTable = Qdi(0x31000004u); footwear.WeenieDefault = 300004u; sex.Footwear.Add(footwear); heritage.Genders.Add(0, sex); table.HeritageGroups.Add(1u, heritage); return table; } [Fact] public void Project_MapsStarterAreasWithPositionsAndCellId() { ChargenOptions options = ChargenTableReader.Project(BuildFixture()); ChargenStarterArea area = Assert.Single(options.StarterAreas); Assert.Equal(0, area.Index); Assert.Equal("Holtburg", area.Name); ChargenPosition position = Assert.Single(area.Locations); Assert.Equal(0xA9B40000u, position.CellId); Assert.Equal(new Vector3(1f, 2f, 3f), position.Origin); Assert.Equal(new Quaternion(0f, 0f, 0f, 1f), position.Orientation); } [Fact] public void Project_MapsHeritageScalarFieldsAndStartAreaIndices() { ChargenOptions options = ChargenTableReader.Project(BuildFixture()); Assert.True(options.TryGetHeritage(1u, out ChargenHeritageOptions? heritage)); Assert.Equal("Aluvian", heritage!.Name); Assert.Equal(0x06000001u, heritage.IconId); Assert.Equal(0x02000010u, heritage.SetupId); Assert.Equal(0x02000020u, heritage.EnvironmentSetupId); Assert.Equal(180u, heritage.AttributeCredits); Assert.Equal(50u, heritage.SkillCredits); Assert.Equal([0], heritage.PrimaryStartAreaIndices); Assert.Equal([0], heritage.SecondaryStartAreaIndices); Assert.False(heritage.IsOlthoi); } [Fact] public void Project_MapsSkillCostsKeyedByRawSkillId() { ChargenOptions options = ChargenTableReader.Project(BuildFixture()); Assert.True(options.TryGetHeritage(1u, out ChargenHeritageOptions? heritage)); uint axeId = (uint)DatReaderWriter.Enums.SkillId.Axe; Assert.True(heritage!.SkillCostsBySkillId.TryGetValue(axeId, out ChargenSkillCost cost)); Assert.Equal(axeId, cost.SkillId); Assert.Equal(4, cost.NormalCost); Assert.Equal(12, cost.PrimaryCost); } [Fact] public void Project_MapsTemplateAttributesAndSkillLists() { ChargenOptions options = ChargenTableReader.Project(BuildFixture()); Assert.True(options.TryGetHeritage(1u, out ChargenHeritageOptions? heritage)); ChargenTemplate template = Assert.Single(heritage!.Templates); Assert.Equal("Soldier", template.Name); Assert.Equal(42u, template.TitleStringId); Assert.Equal(new ChargenAttributeValues(40, 40, 40, 20, 20, 20), template.Attributes); Assert.Equal(180, template.Attributes.Total); Assert.Equal([(uint)DatReaderWriter.Enums.SkillId.Axe], template.NormalSkills); Assert.Equal([(uint)DatReaderWriter.Enums.SkillId.MeleeDefense], template.PrimarySkills); } [Fact] public void Project_MapsGenderScalarsAndOptionLists() { ChargenOptions options = ChargenTableReader.Project(BuildFixture()); Assert.True(options.TryGetHeritage(1u, out ChargenHeritageOptions? heritage)); Assert.True(heritage!.GendersByKey.TryGetValue(0, out ChargenGenderOptions? gender)); Assert.Equal("Male", gender!.Name); Assert.Equal(1000000u, gender.Scale); Assert.Equal(0x02000030u, gender.SetupId); Assert.Equal(0x04000001u, gender.BasePaletteId); Assert.Equal(0x04001001u, gender.SkinPalSetId); Assert.Equal([0x0Au], gender.HairColors); Assert.Equal([0x0Bu], gender.EyeColors); Assert.Equal([0x0Cu], gender.ClothingColors); Assert.True(gender.HasAnyAppearanceOptions); ChargenHairStyle hair = Assert.Single(gender.HairStyles); Assert.Equal(0x06000004u, hair.IconId); Assert.False(hair.Bald); Assert.Equal(0x02000099u, hair.AlternateSetup); ChargenEyeStrip eye = Assert.Single(gender.EyeStrips); Assert.Equal(0x06000005u, eye.IconId); Assert.Equal(0x06000006u, eye.BaldIconId); ChargenFaceStrip nose = Assert.Single(gender.NoseStrips); Assert.Equal(0x06000007u, nose.IconId); ChargenFaceStrip mouth = Assert.Single(gender.MouthStrips); Assert.Equal(0x06000008u, mouth.IconId); ChargenGearOption headgear = Assert.Single(gender.Headgears); Assert.Equal("Leather Cap", headgear.Name); Assert.Equal(0x31000001u, headgear.ClothingTableId); Assert.Equal(300001u, headgear.WeenieDefaultId); Assert.Single(gender.Shirts); Assert.Single(gender.Pants); Assert.Single(gender.Footwear); } [Fact] public void Project_MapsObjDescPaletteSubPaletteTextureAndAnimPartChanges() { ChargenOptions options = ChargenTableReader.Project(BuildFixture()); Assert.True(options.TryGetHeritage(1u, out ChargenHeritageOptions? heritage)); heritage!.GendersByKey.TryGetValue(0, out ChargenGenderOptions? gender); ChargenObjDesc baseDesc = gender!.BaseObjDesc; Assert.Equal(0x04000002u, baseDesc.PaletteId); ChargenSubPalette sub = Assert.Single(baseDesc.SubPalettes); Assert.Equal(0x04000099u, sub.SubPaletteId); Assert.Equal((byte)8, sub.Offset); Assert.Equal((byte)24, sub.NumColors); ChargenTextureChange tex = Assert.Single(baseDesc.TextureChanges); Assert.Equal((byte)0, tex.PartIndex); Assert.Equal(0x05000001u, tex.OldTextureId); Assert.Equal(0x05000002u, tex.NewTextureId); ChargenAnimPartChange part = Assert.Single(baseDesc.AnimPartChanges); Assert.Equal((byte)2, part.PartIndex); Assert.Equal(0x0100ABCDu, part.PartId); // The eye strip carries a SEPARATE bald ObjDesc from its normal one. ChargenEyeStrip eye = Assert.Single(gender.EyeStrips); Assert.Equal(0x04000004u, eye.ObjDesc.PaletteId); Assert.Equal(0x04000005u, eye.BaldObjDesc.PaletteId); Assert.NotEqual(eye.ObjDesc.PaletteId, eye.BaldObjDesc.PaletteId); } [Fact] public void Load_ReturnsEmptyOptions_WhenTableIsMissingFromDatSource() { var empty = new EmptyDatReaderWriter(); ChargenOptions options = ChargenTableReader.Load(empty); Assert.Empty(options.StarterAreas); Assert.Empty(options.HeritagesById); } /// Minimal stub whose Get /// always misses — proves 's /// missing-table tolerance without a live DAT. Mirrors the shape of /// DatResolutionPrecedenceTests.ResolutionSource. private sealed class EmptyDatReaderWriter : IDatReaderWriter { private readonly StubDatabase _db = new(); public string SourceDirectory => string.Empty; public IDatDatabase Portal => _db; public IDatDatabase Cell => _db; public ReadOnlyDictionary CellRegions { get; } = new(new Dictionary()); public IDatDatabase HighRes => _db; public IDatDatabase Language => _db; public IDatDatabase Local => _db; public ReadOnlyDictionary RegionFileMap { get; } = new(new Dictionary()); public int PortalIteration => 0; public int CellIteration => 0; public int HighResIteration => 0; public int LanguageIteration => 0; public bool TryGetFileBytes(uint regionId, uint fileId, ref byte[] bytes, out int bytesRead) { bytesRead = 0; return false; } public IEnumerable GetAllIdsOfType() where T : IDBObj => Array.Empty(); public IEnumerable ResolveId(uint id) => Array.Empty(); public bool TrySave(T obj, int iteration = 0) where T : IDBObj => throw new NotSupportedException(); public bool TrySave(uint regionId, T obj, int iteration = 0) where T : IDBObj => throw new NotSupportedException(); [return: MaybeNull] public T Get(uint fileId) where T : IDBObj => default; public bool TryGet(uint fileId, [MaybeNullWhen(false)] out T value) where T : IDBObj { value = default; return false; } public void Dispose() { } private sealed class StubDatabase : IDatDatabase { public DatDatabase Db => null!; public int Iteration => 0; public IEnumerable GetAllIdsOfType() where T : IDBObj => Array.Empty(); public bool TryGet(uint fileId, [MaybeNullWhen(false)] out T value) where T : IDBObj { value = default; return false; } public bool TryGetFileBytes(uint fileId, [MaybeNullWhen(false)] out byte[] value) { value = default; return false; } public bool TryGetFileBytes(uint fileId, ref byte[] bytes, out int bytesRead) { bytesRead = 0; return false; } public bool TrySave(T obj, int iteration = 0) where T : IDBObj => false; public void Dispose() { } } } }