feat(content): Campaign CC CC1 — chargen table reader and typed options model
Adds the CC1 data layer for Campaign CC (retail character creation): a reader for portal.dat's CharGen table (0x0E000002) plus a presentation-free, Chorizite-free typed options model, and the pure attribute/skill credit math the later CC3 Runtime owner needs. Retail oracle (docs/research/named-retail/acclient_2013_pseudo_c.txt): - ACCharGenData::Serialize @ 0x005C36D0 (table shape: StartingAreas + HeritageGroups) - HeritageGroup_CG::Serialize @ 0x005C2100 - Sex_CG::Serialize @ 0x005C1600 - Template_CG::Serialize @ 0x005C0450 - CharGenState::SetHeritageGroup @ 0x005C67A0 and the six attribute-slider setters (~0x005C46CE..0x005C494E): remainingAtrbCredits = totalAtrbCredits - (str+end+coord+quick+focus+self) — a heritage's AttributeCredits is the budget the six RAW attribute values must fit, not points above the floor. - CharGenState::Reset @ 0x005C68A0: atrbMin=10, atrbMax=100. - gmCharGenMainUI::DoFinish @ 0x004E9170: Finish refuses only when remainingAtrbCredits > 0 (attributes only — skill credits are never gated to zero, confirmed by reading the function body). - CharGenState::UpdateRemainingSkillCredits @ 0x005C37C0: exactly one of NormalCost/PrimaryCost is charged per Trained/Specialized skill. - gmCGAppearancePage::Update @ 0x0047E8F0: the mHeritageGroup==0xc/0xd (Olthoi/OlthoiAcid) camera-offset branch CC6 will need. Cross-checked against ACE's ACE.DatLoader.FileTypes.CharGen and ACE.DatLoader.Entity.HeritageGroupCG/SexCG/TemplateCG/SkillCG loaders (same field order, different byte format) and ACE.Entity.Enum.HeritageGroup / SkillAdvancementClass for the two small stable enums the model exposes. src/AcDream.Core/CharGen/: ChargenOptions (root: StarterAreas + HeritagesById), ChargenHeritageOptions, ChargenGenderOptions (BaseObjDesc + every appearance-option list: hair styles/colors, eye colors, eye/nose/ mouth strips, headgear/shirt/pants/footwear, clothing colors), ChargenTemplate, ChargenObjDesc (palette/subpalette/texture/anim-part-swap shape, mirrors PaletteOverride's presentation-free pattern), and the pure math: ChargenAttributeMath (RemainingCredits/IsFullySpent/range checks) and ChargenSkillCreditMath (retail's Trained-xor-Specialized cost sum) plus ChargenSkillAdvancementSet, a structurally-fixed 55-slot type (reserved slot 0 + SkillId 1..54) so CC2's future wire builder cannot send anything but exactly 55 entries. src/AcDream.Content/CharGen/ChargenTableReader.cs projects the Chorizite DBObj graph into the Core model (MagicCatalog.Load's shape) — no Chorizite type crosses into ChargenOptions. Tests: hand-built-fixture unit tests for the pure math (Core.Tests) and the Content projector (Content.Tests), plus six installed-DAT gate tests (ContentConformanceDats pattern) against the real portal.dat: 13 heritage groups (11 standard + 2 Olthoi), the four named heritages with retail display names incl. "Gharu'ndim", every heritage has a gender with non-empty appearance option lists, every template's attributes stay in 10..100 and never exceed its heritage's budget (discovered live: NOT every template fully spends it — each human heritage's "Adventurer" template sits at the floor as retail's real-DAT-backed "Custom" starting point), start-area indices resolve into the shared list, and skill costs key to valid 1..54 wire ids. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
c3a8c231b8
commit
0445004164
19 changed files with 1656 additions and 0 deletions
215
src/AcDream.Content/CharGen/ChargenTableReader.cs
Normal file
215
src/AcDream.Content/CharGen/ChargenTableReader.cs
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
using AcDream.Core.CharGen;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Types;
|
||||
using CoreChargenObjDesc = AcDream.Core.CharGen.ChargenObjDesc;
|
||||
using DatCharGen = DatReaderWriter.DBObjs.CharGen;
|
||||
using DatObjDesc = DatReaderWriter.Types.ObjDesc;
|
||||
|
||||
namespace AcDream.Content.CharGen;
|
||||
|
||||
/// <summary>
|
||||
/// Projects portal.dat's CharGen table (id <see cref="ChargenTableDid"/>,
|
||||
/// retail <c>ACCharGenData::Serialize @ 0x005C36D0</c>) into acdream's
|
||||
/// presentation-free <see cref="AcDream.Core.CharGen.ChargenOptions"/> tree.
|
||||
/// Mirrors <c>MagicCatalog.Load</c>'s shape: one static entry point over
|
||||
/// <see cref="IDatReaderWriter"/>, no Chorizite types cross into the
|
||||
/// returned model. Cross-checked against ACE's
|
||||
/// <c>ACE.DatLoader.FileTypes.CharGen</c> +
|
||||
/// <c>ACE.DatLoader.Entity.HeritageGroupCG/SexCG/TemplateCG</c> loaders,
|
||||
/// which unpack the identical field order from the same DAT bytes.
|
||||
/// </summary>
|
||||
public static class ChargenTableReader
|
||||
{
|
||||
/// <summary>Retail's CharGen DAT file id (ACE:
|
||||
/// <c>ACE.DatLoader.FileTypes.CharGen.FILE_ID</c>).</summary>
|
||||
public const uint ChargenTableDid = 0x0E000002u;
|
||||
|
||||
/// <summary>
|
||||
/// Loads and projects the installed CharGen table. Returns
|
||||
/// <see cref="ChargenOptions.Empty"/> if the table is missing from the
|
||||
/// supplied dat source (mirrors <c>MagicCatalog</c>'s tolerance for a
|
||||
/// missing optional table — callers that require the table present
|
||||
/// should check <c>HeritagesById.Count</c> themselves).
|
||||
/// </summary>
|
||||
public static ChargenOptions Load(IDatReaderWriter dats)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(dats);
|
||||
|
||||
DatCharGen? table = dats.Get<DatCharGen>(ChargenTableDid);
|
||||
return table is null ? ChargenOptions.Empty : Project(table);
|
||||
}
|
||||
|
||||
/// <summary>Pure projection from an already-loaded DAT record — split out
|
||||
/// from <see cref="Load"/> so tests can exercise it against
|
||||
/// hand-built <see cref="DatCharGen"/> fixtures without a live DAT.</summary>
|
||||
public static ChargenOptions Project(DatCharGen table)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(table);
|
||||
|
||||
var starterAreas = new List<ChargenStarterArea>(table.StartingAreas.Count);
|
||||
for (int i = 0; i < table.StartingAreas.Count; i++)
|
||||
starterAreas.Add(ProjectStarterArea(i, table.StartingAreas[i]));
|
||||
|
||||
var heritagesById = new Dictionary<uint, ChargenHeritageOptions>(table.HeritageGroups.Count);
|
||||
foreach (KeyValuePair<uint, HeritageGroupCG> pair in table.HeritageGroups)
|
||||
heritagesById[pair.Key] = ProjectHeritage(pair.Key, pair.Value);
|
||||
|
||||
return new ChargenOptions(starterAreas, heritagesById);
|
||||
}
|
||||
|
||||
private static ChargenStarterArea ProjectStarterArea(int index, StartingArea area)
|
||||
{
|
||||
var locations = new List<ChargenPosition>(area.Locations.Count);
|
||||
foreach (Position position in area.Locations)
|
||||
{
|
||||
locations.Add(new ChargenPosition(
|
||||
position.CellId,
|
||||
position.Frame.Origin,
|
||||
position.Frame.Orientation));
|
||||
}
|
||||
return new ChargenStarterArea(index, area.Name.Value, locations);
|
||||
}
|
||||
|
||||
private static ChargenHeritageOptions ProjectHeritage(uint heritageId, HeritageGroupCG cg)
|
||||
{
|
||||
var skillCosts = new Dictionary<uint, ChargenSkillCost>(cg.Skills.Count);
|
||||
foreach (SkillCG skill in cg.Skills)
|
||||
{
|
||||
uint skillId = (uint)skill.Id;
|
||||
skillCosts[skillId] = new ChargenSkillCost(skillId, skill.NormalCost, skill.PrimaryCost);
|
||||
}
|
||||
|
||||
var templates = new List<ChargenTemplate>(cg.Templates.Count);
|
||||
foreach (TemplateCG template in cg.Templates)
|
||||
templates.Add(ProjectTemplate(template));
|
||||
|
||||
var gendersByKey = new Dictionary<int, ChargenGenderOptions>(cg.Genders.Count);
|
||||
foreach (KeyValuePair<int, SexCG> pair in cg.Genders)
|
||||
gendersByKey[pair.Key] = ProjectGender(pair.Key, pair.Value);
|
||||
|
||||
return new ChargenHeritageOptions(
|
||||
heritageId,
|
||||
cg.Name.Value,
|
||||
cg.IconId.DataId,
|
||||
cg.SetupId.DataId,
|
||||
cg.EnvironmentSetupId.DataId,
|
||||
cg.AttributeCredits,
|
||||
cg.SkillCredits,
|
||||
new List<int>(cg.PrimaryStartAreas),
|
||||
new List<int>(cg.SecondaryStartAreas),
|
||||
skillCosts,
|
||||
templates,
|
||||
gendersByKey);
|
||||
}
|
||||
|
||||
private static ChargenTemplate ProjectTemplate(TemplateCG template)
|
||||
{
|
||||
var normalSkills = new List<uint>(template.NormalSkills.Count);
|
||||
foreach (var skillId in template.NormalSkills)
|
||||
normalSkills.Add((uint)skillId);
|
||||
|
||||
var primarySkills = new List<uint>(template.PrimarySkills.Count);
|
||||
foreach (var skillId in template.PrimarySkills)
|
||||
primarySkills.Add((uint)skillId);
|
||||
|
||||
return new ChargenTemplate(
|
||||
template.Name.Value,
|
||||
template.IconId.DataId,
|
||||
template.Title,
|
||||
new ChargenAttributeValues(
|
||||
template.Strength,
|
||||
template.Endurance,
|
||||
template.Coordination,
|
||||
template.Quickness,
|
||||
template.Focus,
|
||||
template.Self),
|
||||
normalSkills,
|
||||
primarySkills);
|
||||
}
|
||||
|
||||
private static ChargenGenderOptions ProjectGender(int genderKey, SexCG sex)
|
||||
{
|
||||
var hairStyles = new List<ChargenHairStyle>(sex.HairStyles.Count);
|
||||
foreach (HairStyleCG hair in sex.HairStyles)
|
||||
{
|
||||
hairStyles.Add(new ChargenHairStyle(
|
||||
hair.IconId.DataId,
|
||||
hair.Bald,
|
||||
hair.AlternateSetup,
|
||||
ProjectObjDesc(hair.ObjDesc)));
|
||||
}
|
||||
|
||||
var eyeStrips = new List<ChargenEyeStrip>(sex.EyeStrips.Count);
|
||||
foreach (EyeStripCG eye in sex.EyeStrips)
|
||||
{
|
||||
eyeStrips.Add(new ChargenEyeStrip(
|
||||
eye.IconId.DataId,
|
||||
eye.BaldIconId,
|
||||
ProjectObjDesc(eye.ObjDesc),
|
||||
ProjectObjDesc(eye.BaldObjDesc)));
|
||||
}
|
||||
|
||||
var noseStrips = new List<ChargenFaceStrip>(sex.NoseStrips.Count);
|
||||
foreach (FaceStripCG strip in sex.NoseStrips)
|
||||
noseStrips.Add(new ChargenFaceStrip(strip.IconId.DataId, ProjectObjDesc(strip.ObjDesc)));
|
||||
|
||||
var mouthStrips = new List<ChargenFaceStrip>(sex.MouthStrips.Count);
|
||||
foreach (FaceStripCG strip in sex.MouthStrips)
|
||||
mouthStrips.Add(new ChargenFaceStrip(strip.IconId.DataId, ProjectObjDesc(strip.ObjDesc)));
|
||||
|
||||
return new ChargenGenderOptions(
|
||||
genderKey,
|
||||
sex.Name.Value,
|
||||
sex.Scale,
|
||||
sex.SetupId.DataId,
|
||||
sex.SoundTable.DataId,
|
||||
sex.IconId.DataId,
|
||||
sex.BasePalette.DataId,
|
||||
sex.SkinPalSet.DataId,
|
||||
sex.PhysicsTable.DataId,
|
||||
sex.MotionTable.DataId,
|
||||
sex.CombatTable.DataId,
|
||||
ProjectObjDesc(sex.BaseObjDesc),
|
||||
new List<uint>(sex.HairColors),
|
||||
hairStyles,
|
||||
new List<uint>(sex.EyeColors),
|
||||
eyeStrips,
|
||||
noseStrips,
|
||||
mouthStrips,
|
||||
ProjectGearList(sex.Headgears),
|
||||
ProjectGearList(sex.Shirts),
|
||||
ProjectGearList(sex.Pants),
|
||||
ProjectGearList(sex.Footwear),
|
||||
new List<uint>(sex.ClothingColors));
|
||||
}
|
||||
|
||||
private static List<ChargenGearOption> ProjectGearList(List<GearCG> gearList)
|
||||
{
|
||||
var result = new List<ChargenGearOption>(gearList.Count);
|
||||
foreach (GearCG gear in gearList)
|
||||
result.Add(new ChargenGearOption(gear.Name.Value, gear.ClothingTable.DataId, gear.WeenieDefault));
|
||||
return result;
|
||||
}
|
||||
|
||||
private static CoreChargenObjDesc ProjectObjDesc(DatObjDesc objDesc)
|
||||
{
|
||||
var subPalettes = new List<ChargenSubPalette>(objDesc.SubPalettes.Count);
|
||||
foreach (SubPalette sub in objDesc.SubPalettes)
|
||||
subPalettes.Add(new ChargenSubPalette(sub.SubId.DataId, sub.Offset, sub.NumColors));
|
||||
|
||||
var textureChanges = new List<ChargenTextureChange>(objDesc.TextureChanges.Count);
|
||||
foreach (TextureMapChange change in objDesc.TextureChanges)
|
||||
{
|
||||
textureChanges.Add(new ChargenTextureChange(
|
||||
change.PartIndex,
|
||||
change.OldTexture.DataId,
|
||||
change.NewTexture.DataId));
|
||||
}
|
||||
|
||||
var animPartChanges = new List<ChargenAnimPartChange>(objDesc.AnimPartChanges.Count);
|
||||
foreach (AnimationPartChange change in objDesc.AnimPartChanges)
|
||||
animPartChanges.Add(new ChargenAnimPartChange(change.PartIndex, change.PartId.DataId));
|
||||
|
||||
return new CoreChargenObjDesc(objDesc.PaletteId.DataId, subPalettes, textureChanges, animPartChanges);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue