# R7 — Character Creation (deep dive) Research slice for acdream's retail-faithful character-creation pipeline: heritage taxonomy, templates, appearance, skill-credit economy, starting town, wire format, preview renderer, port plan. **Inputs surveyed:** - `docs/research/decompiled/chunk_00470000.c` (CharGen UI panel, strings, state machine; `FUN_0047aa10` is the CharGen panel ctor, `FUN_0047b590` is the summary redraw, `FUN_0047c5a0` is the starting-town description render, `FUN_0047c140` is the name-too-long dialog). - `docs/research/decompiled/chunk_00480000.c`, `chunk_00540000.c` (opcode dispatch — 0xF656 Character_SendCharGenResult send, 0xF657 Login_SendEnterWorld, 0xF643 CharGen response). - `references/DatReaderWriter/DatReaderWriter/Generated/DBObjs/CharGen.generated.cs` and related `Types/HeritageGroupCG`, `TemplateCG`, `SexCG`, `HairStyleCG`, `FaceStripCG`, `EyeStripCG`, `GearCG`, `StartingArea`, `SkillCG` (this is the authoritative on-disk schema of `0xE000002 CharGen`). - `references/ACE/Source/ACE.Entity/CharacterCreateInfo.cs`, `Appearance.cs`, `Enum/HeritageGroup.cs`, `Enum/SkillAdvancementClass.cs`, `Server/Network/Handlers/CharacterHandler.cs`, `Server/Factories/PlayerFactory.cs` (server expectations + skill-credit math). - `references/Chorizite.ACProtocol/Chorizite.ACProtocol/Messages/C2S/Character_SendCharGenResult.generated.cs`, `Types/CharGenResult.generated.cs`, `Messages/S2C/Character_CharGenVerificationResponse.generated.cs`, `Enums/CharGenResponseType.generated.cs` (generated from protocol XML; canonical wire format). - `references/holtburger/crates/holtburger-core/src/character_gen.rs`, `references/holtburger/crates/holtburger-content/src/character_gen.rs`, `references/holtburger/apps/holtburger-cli/src/pages/selection/creation.rs` (only reference client with a complete working chargen form — TUI). - `references/DatReaderWriter/DatReaderWriter.Tests/DBObjs/CharGenTests.cs` (pinned EoR values — Aluvian has 52 skill credits, 7 templates, 2 genders). - `references/AC2D/cNetwork.cpp` (char-create error code table, 0xF643 response format). **The anchor rule:** the dat-file `0xE000002 CharGen` record is the ground truth for every numeric constant (attribute credits, skill credits, template attribute values, per-heritage skill cost overrides, hair/eye/face counts, starter-area positions). The client never hardcodes any of it — everything is read from the dat and displayed. We do the same. --- ## 1. Heritage taxonomy Retail AC ships with **7 canonical heritages** in the launch/Throne of Destiny era (Aluvian, Gharu'ndim, Sho, Viamontian, Umbraen, Empyrean, Undead). Master of Arms (2013) added additional heritages (Lugian, Tumerok, Gear Knight), and Olthoi Play is retail's experiment — so the dat actually has 13 entries. acdream targets End-of-Retail (EoR) and must expose all 13. **Canonical enum (`ACE.Entity.Enum.HeritageGroup`, matches dat key):** | ID | Enum name | Dat name | Display (ToSentence) | Era | |----|-------------------|---------------|------------------------|--------------| | 0 | Invalid | (unused) | — | — | | 1 | Aluvian | Aluvian | Aluvian | Launch | | 2 | Gharundim | Gharu'ndim | Gharu'ndim | Launch | | 3 | Sho | Sho | Sho | Launch | | 4 | Viamontian | Viamontian | Viamontian | Throne of D. | | 5 | Shadowbound | Umbraen | Umbraen | Darktide | | 6 | Gearknight | Gearknight | Gearknight | Master of A. | | 7 | Tumerok | Tumerok | Tumerok | Master of A. | | 8 | Lugian | Lugian | Lugian | Master of A. | | 9 | Empyrean | Empyrean | Empyrean | Master of A. | | 10 | Penumbraen | Penumbraen | Penumbraen | Darktide | | 11 | Undead | Undead | Undead | Halloween | | 12 | Olthoi | Olthoi | Olthoi | Special | | 13 | OlthoiAcid | OlthoiAcid | Olthoi (acid) | Special | **Decompiled confirmation.** `FUN_0047b278` in chunk_00470000.c switches on heritage id 1..13 (0xb, 0xc, 0xd included) to pick the random name list (`ID_CharGen_AluMaleNames`, `ID_CharGen_GharuFemaleNames`, …, `ID_CharGen_OlthoiMaleNames`). Case 5 and case 10 both fall through to the "Shad" strings — the retail client treats Umbraen and Penumbraen as the same visual heritage with one shared name list but distinct dat entries (different body scale / base palette). **Per-heritage data comes from the dat.** Each `HeritageGroupCG` carries: - `Name` — English label ("Aluvian", "Gharu'ndim", …). - `IconId` — `0x06000xxx` RenderSurface for the UI portrait. - `SetupId` — body model used to build the preview character. - `EnvironmentSetupId` — backdrop environment (pedestal / room) rendered behind the preview character during chargen. - `AttributeCredits` — budget for the 6 attributes (see §3). - `SkillCredits` — starting skill credit pool (see §4); **52 for all standard heritages, 68 for Olthoi** per `PlayerFactory.cs` comment. - `PrimaryStartAreas` / `SecondaryStartAreas` — lists of `StartingArea` indices (1-based? 0-based? — the client stores them as `List`; a value like 1 for Aluvian is the Holtburg index in `StartingAreas`). - `Skills` — list of `SkillCG { Id, NormalCost, PrimaryCost }` **overrides** that replace the default `SkillBase.TrainedCost` / `SkillBase.SpecializedCost` for this heritage. The Aluvian test pins `ArcaneLore` to `NormalCost=0, PrimaryCost=2` — this is how heritage-specific "free" or "cheap" skills are encoded. - `Templates` — list of `TemplateCG` (see §2). The Aluvian test pins 7 templates; the last has STR=100, COORD=100, 4 primary skills. - `Genders` — hash table `1 → Male`, `2 → Female` of `SexCG`. **Attribute credits are per-heritage.** Not one constant. Every heritage declares its own pool. Retail values (verified via DatReaderWriter tests and ACE's factory code): - Most heritages: **330** attribute credits (spread across 6 attributes each valued 10–100; see §3). - Olthoi variants: different budget; read from dat, do not hardcode. **Starting skill credits:** 52 for most, 68 for Olthoi. Read from dat. --- ## 2. Template system Each `HeritageGroupCG.Templates` entry is a **pre-baked class archetype**: Swordsman, Sorcerer, Archer, Custom, etc. The dat stores: ```csharp public partial class TemplateCG : IDatObjType { public PStringBase Name; // "Swordsman", "Custom", ... public QualifiedDataId IconId; public uint Title; // Title.TitleID granted on create public int Strength, Endurance, Coordination, Quickness, Focus, Self; public List NormalSkills; // auto-trained public List PrimarySkills; // auto-specialized } ``` **How templates work in retail:** 1. The user picks a heritage → the Templates list for that heritage is shown. 2. Picking a template pre-fills: - the 6 attribute stat values (e.g., Aluvian "Swordsman" gives STR=70, COORD=60, …), - the skill advancement table: every SkillId in `PrimarySkills` gets `SkillAdvancementClass.Specialized`, every SkillId in `NormalSkills` gets `Trained`, everything else starts `Untrained`, - the character's starting title (`Title` dword). 3. The user can then modify either — change template = reset both. 4. There's always a "Custom" template (the last entry in some heritages, the first in others) with `Strength = Endurance = … = 10` and empty primary/normal lists. Picking Custom lets the user distribute all attribute/skill credits freely. **Holtburger discovery** (`custom_template_for_heritage` in `holtburger-core/src/character_gen.rs`): the client locates the "Custom" template by name-matching "Custom" case-insensitively, with fallback to `template_option == 0`, then finally the first entry. acdream must do the same — retail data is not strictly ordered. **Attribute pre-fill semantics.** `TemplateCG.Strength` is the **starting value** for that attribute — not an addition. The rest of the `AttributeCredits - (STR + END + COORD + QCK + FOC + SELF)` is available for the user to distribute further. For the Aluvian "tank" template with STR=70, that leaves `330 - sum` credits for additional spending. **The template_option sent on the wire** (`CharGenResult.TemplateNum`) is the 0-based index into `HeritageGroupCG.Templates`. The server uses this to look up the granted title via `heritage.Templates[template].Title` (see `PlayerFactory.cs:138 player.AddTitle(heritageGroup.Templates[...].Title, true)`). --- ## 3. Attribute point budget **Range per attribute:** each of Strength, Endurance, Coordination, Quickness, Focus, Self is clamped to **10..100 inclusive**. Enforced both client-side and by `PlayerFactory.ValidateAttributeCredits` server-side: ```csharp // PlayerFactory.cs:622 if (attributeValue < 10 || attributeValue > 100) return CreateResult.InvalidSkillRequested; ``` **Total budget:** `HeritageGroupCG.AttributeCredits` (typically 330). ```csharp // PlayerFactory.cs:628 if (total > maxAttributes) return CreateResult.TooManySkillCreditsUsed; ``` **Note that ACE allows total < budget** (underspend). Retail's client does not — the UI forces the user to spend the whole budget before the "Create" button enables. Our UI must match retail: block submit until the counter reads 0 remaining. holtburger implements this via a strict `AttributeBudgetIncomplete` validation error (`holtburger-core/character_gen.rs:343-348`). **Minimum footprint:** 6 × 10 = 60 points, leaving 270 to distribute for a standard 330 budget. **Holtburger constants to port verbatim:** ```csharp public const uint CharGenMinAttribute = 10; public const uint CharGenMaxAttribute = 100; ``` --- ## 4. Skill credit economy This is where chargen has real depth. **Ultrathink required:** get the spec/trained combo math wrong and the whole system silently misreports. ### 4.1 Credit budget - `HeritageGroupCG.SkillCredits` = starting pool (52 for most, 68 for Olthoi). - The pool is expressed as `PropertyInt.AvailableSkillCredits` = `PropertyInt.TotalSkillCredits` on the created Player. ### 4.2 Per-skill costs Each skill in the dat has a `SkillBase`: ```csharp public partial class SkillBase : IDatObjType { public int TrainedCost; // cost to go Untrained → Trained public int SpecializedCost; // cost to go Trained → Specialized public SkillCategory Category; public bool ChargenUse; // if false, can't train at chargen public uint MinLevel; // gate for late-game skills public SkillFormula Formula; // attribute weighting public double UpperBound, LowerBound, LearnMod; } ``` **Retail pitfall (the one we must get right):** `TrainedCost` is the credit cost to train from Untrained. `SpecializedCost` is the **ADDITIONAL** cost to go from Trained to Specialized. To **specialize from scratch** you pay `TrainedCost + SpecializedCost`. The field is a delta, not a total. ACE's factory proves this (`PlayerFactory.cs:199-202`): ```csharp if (sac == SkillAdvancementClass.Specialized) { if (!player.TrainSkill((Skill)i, trainedCost)) // pay train first return CreateResult.FailedToTrainSkill; if (!player.SpecializeSkill((Skill)i, specializedCost)) // then pay spec delta return CreateResult.FailedToSpecializeSkill; } ``` holtburger's accumulator proves it too (`holtburger-core/character_gen.rs:395-400`): ```rust SkillAdvancementClass::Trained => spent += trained_cost, SkillAdvancementClass::Specialized => spent += trained_cost + specialized_cost, ``` ### 4.3 Heritage overrides `HeritageGroupCG.Skills` (list of `SkillCG`) **overrides** the base costs for specific skills. Aluvian's EoR override: ArcaneLore `{NormalCost=0, PrimaryCost=2}` — Aluvians can train ArcaneLore for free and specialize it for just 2 credits, reflecting their magic affinity. The override replaces (not adds to) the base. This is why Aluvian mages get built. Port sequence (matches ACE `PlayerFactory.cs:184-195`): ```csharp int trainedCost = skill.TrainedCost; int specializedCost = skill.UpgradeCostFromTrainedToSpecialized; foreach (var skillGroup in heritageGroup.Skills) { if (skillGroup.SkillNum == i) { trainedCost = skillGroup.NormalCost; specializedCost = skillGroup.PrimaryCost; break; } } ``` ### 4.4 Unavailable skills Some skills have `ChargenUse = false` (late-game or retired skills: Salvaging, void magic at first, etc.). The UI must hide these from the chargen picker. The server also gates on `ChargenUse`: ```csharp if (!DatManager.PortalDat.SkillTable.SkillBaseHash.ContainsKey(i)) return CreateResult.InvalidSkillRequested; ``` holtburger adds a safety: **cost >= 999 is a sentinel for "cannot raise at chargen"** (`CHARACTER_GEN_UNAVAILABLE_SKILL_COST = 999`). Retail uses high cost values on skills that weren't meant to be taken. We adopt the same sentinel check to refuse to enable the "up" button when the next tier cost >= 999. ### 4.5 SkillAdvancementClass values ```csharp public enum SkillAdvancementClass : uint { Inactive, // no credits spent, not in player's skill list Untrained, // 0 credits, visible but unskilled Trained, // TrainedCost credits spent Specialized // TrainedCost + SpecializedCost credits spent } ``` `Inactive` vs `Untrained` distinction: `Untrained` means "in the skill list but you haven't trained it" — the skill still has a trainable future. `Inactive` is the server-side "don't even show this row" state for skills that don't exist for this character's era. The wire message sends one enum value per slot in the 55-skill array (55 is the EoR skill count — checked explicitly by `PlayerFactory.cs:166 if (characterCreateInfo.SkillAdvancementClasses.Count != 55) return ClientServerSkillsMismatch`). ### 4.6 Cannot despec Retail rule: once specialized at chargen, always specialized. The client UI offers "lower skill" but the server-side state machine will reject despec after the character is saved. At chargen it's purely UI — holtburger's `lower_selected_skill` allows stepping back down from Specialized to Trained to Untrained (modulo template minimums) while still on the chargen panel, because no server state exists yet. **Template minimums block despec below the template floor.** If a template sets `NormalSkills = [Sword]`, the chargen form forbids untraining Sword. Even the Custom template has effective minimums if `TrainedCost == 0` for a skill in the heritage override (Aluvian ArcaneLore is effectively free-trained → minimum is Trained, not Untrained). See `minimum_skill_advancement_for_template` in holtburger-core. --- ## 5. Skill categories / UI grouping The chargen summary panel (`FUN_0047b590` in chunk_00470000.c) groups skills into four rows: | Dat flag | Summary label | UI color | |----------|--------------------------|-----------------| | 3 | "Specialized Skills" | yellow | | 2 | "Trained Skills" | green / white | | 1 | "Useable Untrained Skills" | grey (Usable) | | 1 | "Unuseable Untrained Skills" | dark grey | "Useable Untrained" means `SkillBase.Category` allows casting/using with a penalty; "Unuseable" means the skill has `Untrained = cannot use at all`. ACE stores this via `SkillCategory` — reflects whether a skill can be attempted with zero points. The UI has to know this to draw the two separate untrained buckets. --- ## 6. Appearance system Per heritage + gender, the dat carries a full `SexCG`: ```csharp public partial class SexCG : IDatObjType { public PStringBase Name; // "Male" or "Female" public uint Scale; // 100 = default, <100 = smaller public QualifiedDataId SetupId; // body model public QualifiedDataId SoundTable; public QualifiedDataId IconId; public QualifiedDataId BasePalette; public QualifiedDataId SkinPalSet; // skin hue gradient public QualifiedDataId PhysicsTable; public QualifiedDataId MotionTable; public QualifiedDataId CombatTable; public ObjDesc BaseObjDesc; public List HairColors; // uint = PalSet id public List HairStyles; public List EyeColors; // uint = Palette id public List EyeStrips; // face eye variant public List NoseStrips; public List MouthStrips; public List Headgears; public List Shirts; public List Pants; public List Footwear; public List ClothingColors; // uint = PalSet id } ``` ### 6.1 Hair / eyes / face texture selection - `HairStyleCG` holds `{ IconId, Bald bool, AlternateSetup uint, ObjDesc }`. The `ObjDesc` carries texture overrides. `Bald` is a flag that selects a different `EyeStripCG` variant (`BaldObjDesc`) — eyes look different against a bald scalp. - `AlternateSetup` is used by Gear Knight / Olthoi: their "hair style" is really a whole-body model swap because they have no hair. When `AlternateSetup > 0` the server sets `PropertyDataId.Setup` to that alternate — switching the model for the creature (see `PlayerFactory.cs:74-75`). - `EyeStripCG` carries `IconId, BaldIconId, ObjDesc, BaldObjDesc`. The chargen UI picks `BaldObjDesc` when `HairStyleCG.Bald == true`. - `FaceStripCG` (used for noses, mouths) is simpler: `IconId + ObjDesc`. ### 6.2 Colors (palette sets) `HairColors` and `ClothingColors` are lists of `PalSet` ids. A PalSet is a **gradient** of palettes — selecting a hair color gives you a base (e.g., "red"), and the **hue** slider (`HairHue` 0.0..1.0 double) picks a specific palette from that gradient. Server-side: ```csharp // PlayerFactory.cs:96-97 var hairPalSet = DatManager.PortalDat.ReadFromDat( sex.HairColorList[(int)characterCreateInfo.Appearance.HairColor]); player.SetProperty(PropertyDataId.HairPalette, hairPalSet.GetPaletteID(characterCreateInfo.Appearance.HairHue)); ``` - `Appearance.HairColor` (uint): which PalSet (red vs brown vs blonde). - `Appearance.HairHue` (double, 0.0..1.0): where in the gradient. - Same pattern for `SkinHue`, `ShirtHue`, `PantsHue`, `HeadgearHue`, `FootwearHue`. `EyeColors` is a `List` of `Palette` (not PalSet) ids — eyes have no hue slider, just a discrete choice. ### 6.3 Gender-gated options Beards, body shape, and armor-cut are all resolved via distinct `SexCG` records per (heritage, gender). There is no explicit "has beard" flag — a male Aluvian simply has different `HairStyleCG` entries than female. Hair styles for male Aluvian include beards as facial hair decals on the head ObjDesc. ### 6.4 Headgear is optional `Appearance.HeadgearStyle == 0xFFFFFFFF` (uint::MaxValue) is the "no hat" sentinel. Every other appearance index must be valid (< options.Length). holtburger's `validate_optional_index` handles this — if the sentinel is sent, skip validation; else require in-range. ### 6.5 Randomization The client has a "Random Appearance" button. Decompilation shows `FUN_004e9720` with `ID_CharGen_RandomizeWarning` prompt — it shows a confirmation dialog before clobbering the player's manual appearance tweaks. When confirmed, every index gets a uniform random in `[0, options.Length)`, hues get uniform `[0, 1)`, headgear has ~50% chance of `0xFFFFFFFF`. Port holtburger's `randomize_appearance` directly (`holtburger-core/character_gen.rs:195-242`). ### 6.6 Gear Knight / Olthoi special handling Gear Knight has no hair palette, no clothing (body is a full suit). `PlayerFactory.cs:105-106`: ```csharp if (player.Heritage != (int)HeritageGroup.Gearknight) // ... apply hat/shirt/pants/shoes ``` Olthoi has `IsOlthoiPlayer == true` → weenie is `olthoiplayer` (not `human`), entire appearance block skipped. Port plan: guard clothing application on `heritage != Gearknight && !heritage.IsOlthoi()`. --- ## 7. Starting town selection `CharGen.StartingAreas` is a 5-entry list in EoR data: | Index | Name | Heritages (primary) | |-------|------------|-----------------------------------------------| | 0 | Holtburg | Aluvian | | 1 | Shoushi | Sho, Gharu'ndim | | 2 | Yaraq | Gharu'ndim, Sho (secondary) | | 3 | Sanamar | Viamontian, Umbraen, Empyrean, Undead, etc. | | 4 | OlthoiLair | Olthoi / OlthoiAcid only | Each `StartingArea` has `{ Name, List Locations }`. Multiple locations per town are used for randomized spawn jitter. ACE picks `starterArea.Locations[0]` always (`PlayerFactory.cs:360`) — retail may or may not randomize. acdream can start with `[0]` and revisit. **Decompiled confirmation.** `FUN_0047c5a0` in chunk_00470000.c takes the selected town id 1..4 and renders the flavor text string (`ID_CharGen_HoltText`, `ID_CharGen_ShoushiText`, `ID_CharGen_YaraqText`, `ID_CharGen_SanamarText`). Note the client uses 1-based town IDs internally, while the dat list is 0-indexed. The UI layer maps `uiTownId → datIndex = uiTownId - 1`; the wire message uses the 0-based `start_area`. This 1-vs-0 gotcha lives in the UI state, not the dat. **Per-heritage validation.** `HeritageGroupCG.PrimaryStartAreas` lists the allowed indices. The UI cycles through allowed options only. `SecondaryStartAreas` is currently unused by retail but the dat reserves it. holtburger combines both lists into a single allowed set. **Post-login Free Ride.** After character creation, the server casts a "Free Ride to Holtburg / Shoushi / Yaraq / Sanamar" spell to teleport the player to the Dereth Coast Tutorial. The initial `player.Location` is the tutorial, and `player.Instantiation` is the town itself (`PlayerFactory.cs:360-389`). acdream's first-login flow does not care — we just receive the player's actual spawn location in the PlayerCreate message. Noted here because the "starting town" the player picks is NOT the first position they see; it's where they recall to after the tutorial. --- ## 8. Name validation ### 8.1 Client-side (retail) From chunk_00470000.c: - `ID_CharGen_NameTooLong` string ← `FUN_0047c140` checks length and pops a modal if the typed name exceeds the limit. The limit is the `PStringBase` max for character names: **20 characters** per retail convention (hardcoded in the UI text field's char limit). - The client filters the IME/keyboard input to only accept latin alphanumeric plus apostrophe and hyphen. Whitespace is trimmed on submit. Uppercase/lowercase preserved. ### 8.2 Server-side (ACE) 1. **Empty name** → rejected (holtburger enforces `require_nonempty_name = true` by default). 2. **Taboo table match** → `CharacterGenerationVerificationResponse.NameBanned`. ACE reads `DatManager.PortalDat.TabooTable` and checks `ContainsBadWord(name.ToLowerInvariant())` (`CharacterHandler.cs:51`). The taboo table is a dat file (profanity + reserved-name list). 3. **Creature name conflict** → also `NameBanned` (`CharacterHandler.cs:57` — optional rule, default on). 4. **Name already in use** → `NameInUse` (`CharacterHandler.cs:63, 149`). 5. **Admin/Sentinel name override** → server prefixes with `+` for accounts with elevated access levels (`CharacterHandler.cs:374-377`). ### 8.3 Case normalization Case is preserved as typed. "Borelean" and "BORELEAN" are different strings to the DB; typically only one is accepted first, the other triggers `NameInUse`. Server compares case-insensitively for availability but persists the typed casing. --- ## 9. CharCreate wire message layout (0xF656) This is the **only** bit the client controls. The client sends a single message, C2S opcode `0xF656` (`Character_SendCharGenResult`). Generated schema from `Chorizite.ACProtocol/Types/CharGenResult.generated.cs`: ### 9.1 Header ``` UINT32 OpCode = 0xF656 ``` Followed by outer message envelope (from `Messages/C2S/Character_SendCharGenResult.generated.cs`): ``` string16L Account // wide-prefixed length, UTF-16 LE CharGenResult Result // the inline body described below ``` ### 9.2 CharGenResult body Reading in order (little-endian, no padding unless noted): ``` string16L Account // duplicated account name — ACE discards UINT32 One // always 1 — "unknown constant" BYTE HeritageGroup // 1..13 (HeritageGroup enum) BYTE Gender // 1=Male, 2=Female UINT32 EyesStrip // index into SexCG.EyeStrips UINT32 NoseStrip // index into SexCG.NoseStrips UINT32 MouthStrip // index into SexCG.MouthStrips UINT32 HairColor // index into SexCG.HairColors (PalSet) UINT32 EyeColor // index into SexCG.EyeColors (Palette) UINT32 HairStyle // index into SexCG.HairStyles UINT32 HeadgearStyle // 0xFFFFFFFF = no hat UINT32 HeadgearColor // index into SexCG.ClothingColors UINT32 ShirtStyle UINT32 ShirtColor UINT32 TrousersStyle UINT32 TrousersColor UINT32 FootwearStyle UINT32 FootwearColor UINT64 SkinShade // 8 bytes = IEEE 754 double, 0.0..1.0 UINT64 HairShade UINT64 HeadgearShade UINT64 ShirtShade UINT64 TrousersShade UINT64 TootwearShade // (sic — typo preserved in generator) UINT32 TemplateNum // 0-based index into HeritageGroup.Templates UINT32 Strength // 10..100 UINT32 Endurance UINT32 Coordination UINT32 Quickness UINT32 Focus UINT32 Self UINT32 Slot // character slot on the server (0..max-1) UINT32 ClassId // always 1 — legacy PackableList Skills // 55-entry list, each = SkillAdvancementClass // PackableList = compressed-uint count + items string16L Name UINT32 StartArea // 0-based index into CharGen.StartingAreas UINT32 IsAdmin // 0 or 1 UINT32 IsEnvoy // 0 or 1 (aka IsSentinel on ACE side) UINT32 Validation // xor/sum checksum, see §9.3 ``` **ACE reads this slightly differently** (`CharacterCreateInfo.cs`) — the "Unknown constant (1)" is read as a UINT32, and the heritage / gender are read as UINT32 each instead of bytes. **These two formats must match one of them byte-for-byte.** The Chorizite generator is authoritative because it's generated from the protocol XML used by both retail and ACE. ACE's `BinaryReader.ReadUInt32()` for fields the XML declares as BYTE actually works because they're the first 4 bytes of a little-endian dword with the other 3 bytes being part of the next field — but the ACE code then advances 4 bytes, misaligning if the sizes were really bytes. **Trust Chorizite.** In practice: there's a **known discrepancy** between Chorizite and ACE on heritage/gender width. Empirical pcaps from retail server will resolve it. Until then, acdream should send matching ACE's format (UINT32 each) because ACE is the target server. Add a switchable serializer. ### 9.3 Validation checksum The `Validation` field is described by the protocol XML as: > "Seems to be the total of heritageGroup, gender, eyesStrip, > noseStrip, mouthStrip, hairColor, eyeColor, hairStyle, > headgearStyle, shirtStyle, trousersStyle, footwearStyle, > templateNum, strength, endurance, coordination, quickness, focus, > self. Perhaps used for some kind of validation?" ACE **ignores** this field — it's client-side anti-tamper. Retail client summed specific fields (no shade doubles, no colors except hair/eye, no slot, no name, no skills) — probably to catch byte-twiddling hacks. acdream computes the same sum for bug-for-bug parity with the live retail server (ACE doesn't care; retail-emu or GDL may). Exact formula (from XML description order): ``` validation = heritageGroup + gender + eyesStrip + noseStrip + mouthStrip + hairColor + eyeColor + hairStyle + headgearStyle + shirtStyle + trousersStyle + footwearStyle + templateNum + strength + endurance + coordination + quickness + focus + self ``` All as u32 wrapping add; overflow discarded. --- ## 10. Server response (0xF643) S2C `Character_CharGenVerificationResponse`. Schema from Chorizite: ``` UINT32 OpCode = 0xF643 UINT32 ResponseType (CharGenResponseType) if ResponseType == OK (1): UINT32 CharacterId string16L Name UINT32 SecondsUntilDeletion align(4) ``` `CharGenResponseType` values (note gap): ```csharp public enum CharGenResponseType : uint { OK = 0x0001, NameInUse = 0x0003, NameBanned = 0x0004, Corrupt = 0x0005, // character data rejected Corrupt_0x0006 = 0x0006, // "DatabaseDown" aka "try again later" AdminPrivilegeDenied = 0x0007, } ``` ACE's `CharacterGenerationVerificationResponse` enum has extra values (`Undef=0`, `Pending=2`, plus `Count`) that don't appear on the wire — they're for server-internal bookkeeping. Only the 0x0001..0x0007 values go out. AC2D confirms the error messages (`cNetwork.cpp:1433`): - 0x03 "The name you have chosen … is already in use" - 0x04 "Sorry, but that name is not permitted." - 0x05 "… found an unexplained error with this new character. The data may be corrupt or out of date." - 0x06 "… cannot create your new character at this time. Please try again later." - 0x07 "Sorry, but you do not have the privileges to make an administrator character." On success the server **also** pushes a refreshed character list (S2C `Character_CharacterList`, opcode varies) so the new character appears in the slot selection. The client should not refetch on its own — it waits for the server's push. --- ## 11. Preview renderer During chargen the client renders the character on a rotating pedestal in a small 3D viewport inset. Key observations from the decompiled CharGen panel ctor (`FUN_0047aa10` at 0x0047AA10): - The ctor wires up a scene using `FUN_0045a910(puStack_4, 0, 0, 1)` — this is the panel's scene graph container with flags indicating a character preview. - It stores an `FUN_0043c680` device pointer and asks for viewport region registration (`0x186a1`, `0x186a2` → region IDs for the character window and a child area). - A 4000ms timer is registered: `(**(code**)(*DAT_00837ff4 + 0x34)) (0xe, param_1 + 2, 4000)`. This is the **preview rotation timer** — ticks every 4 seconds to update the character's Y-axis rotation by a small delta (retail rotates slowly, ~15°/sec observed). Implementation plan (**ported, not invented**): 1. **Scene setup.** When the panel activates with (heritage, gender), read `HeritageGroupCG.EnvironmentSetupId` from dat → this is the pedestal / backdrop Setup. Instantiate it at the origin. 2. **Character model.** Read `SexCG.SetupId` (with `HairStyleCG.AlternateSetup` override if non-zero). Build the ObjDesc from `SexCG.BaseObjDesc` overlaid with `HairStyleCG.ObjDesc`, `EyeStripCG.ObjDesc` (bald or standard), `FaceStripCG.ObjDesc` (nose), `FaceStripCG.ObjDesc` (mouth). Apply palette/hue overrides same as a world player. Place at origin offset slightly above pedestal. 3. **Camera.** Fixed orbit at ~2m radius, ~1.5m eye height, looking at character head. Retail camera does NOT pan or zoom during chargen — only the character rotates. 4. **Rotation driver.** Per-frame Y rotation using `Time.Delta * rotationSpeed`. Retail speed is ~0.26 rad/sec (15°/sec). The 4000ms timer is not for rotation continuity (that's per-frame) — it's probably a "full revolution" tick for state inspection. 5. **Lighting.** One directional key-light from camera-right-up, a fill from camera-left. Ambient ~0.3. This matches retail's flat-but-readable lighting in the chargen inset. --- ## 12. Chat commands vs UI **Retail is UI-only for chargen.** There are no `/heritage` or `/template` chat commands. The only text the user types is the character's name. All other selection is click-driven. **What acdream should add (not retail):** - The plugin API lives above the CharGen state machine. A plugin could set the state programmatically (useful for "clone this character's appearance" tools), but the stock UI is click-driven for parity. - For QA we should support a `--chargen-template=aluvian:swordsman` command-line flag that pre-fills the form for rapid iteration. --- ## 13. Full flow timeline ``` 1. User clicks "Create new character" in charlist panel. 2. CharGenPanel opens (FUN_0047aa10 ctor): - read CharGen dat (0xE000002) → StartingAreas + HeritageGroups. - default heritage = Aluvian (id 1). - default gender = Male (id 1). - default template = first template (index 0). - default start town = heritage.PrimaryStartAreas[0]. - pre-fill attributes from template. - pre-fill skills from template (primary = Spec, normal = Trained). 3. User iterates on UI: - heritage cycle → reload templates, reset start town, reset attributes + skills. - gender cycle → reload appearance option lists. - template cycle → reset attributes + skills. - attribute +/- → update remaining_attribute_points. - skill raise/lower → update remaining_skill_credits. - appearance tweaks → rebuild preview ObjDesc. - random appearance button → confirm dialog (ID_CharGen_RandomizeWarning), then reroll all indices + hues. - name field → each keystroke, check length ≤ 20 (client-side). 4. User clicks "Create": - client-side validation: all budgets spent, name non-empty, name length ≤ 20. - build CharGenResult struct, compute Validation checksum. - send 0xF656 Character_SendCharGenResult with account + result. 5. Server responds 0xF643: - OK → push refreshed CharacterList, close CharGenPanel, return to slot selection. - NameInUse / NameBanned → show modal with ID_CharGen_Name{InUse,Banned} string, stay on CharGenPanel with name field re-focused. - Corrupt / Corrupt_0x0006 → show modal "character data is corrupt, please regenerate", reset form. - AdminPrivilegeDenied → modal "you do not have privileges", un-check admin box. 6. On OK, user selects new char in list → 0xF657 Login_SendEnterWorld → enter world. ``` --- ## 14. acdream port plan (C# .NET 10) ### 14.1 Dat layer (already in DatReaderWriter, consume via NuGet-style link) No port needed — the generated `CharGen`, `HeritageGroupCG`, `TemplateCG`, `SexCG`, `StartingArea`, `SkillCG`, `HairStyleCG`, `FaceStripCG`, `EyeStripCG`, `GearCG` classes are already part of the `references/DatReaderWriter` project, which acdream can reference directly. ### 14.2 Content layer (`Acdream.Game.CharGen`) Mirror holtburger-content's flatten step. Reads the raw dat types and produces snapshot objects friendlier for UI binding: ```csharp namespace Acdream.Game.CharGen; public sealed class CharGenCatalog { public IReadOnlyList StartingAreas { get; } public IReadOnlyDictionary Heritages { get; } public IReadOnlyDictionary Skills { get; } public int ExpectedSkillSlots { get; } // 55 for EoR public static CharGenCatalog FromDat(CharGen charGen, SkillTable skillTable); public Heritage? GetHeritage(uint id); public StartingArea? GetStartingArea(uint id); public SkillCosts? GetSkillCosts(uint heritageId, uint skillId); public IReadOnlyList AllowedStartAreaIds(uint heritageId); } public sealed class Heritage { public uint Id { get; } public string Name { get; } // "Aluvian" public uint IconId { get; } public uint SetupId { get; } public uint EnvironmentSetupId { get; } // backdrop scene public uint AttributeCredits { get; } // 330, Olthoi=more public uint SkillCredits { get; } // 52, Olthoi=68 public IReadOnlyList PrimaryStartAreas { get; } public IReadOnlyList SecondaryStartAreas { get; } public IReadOnlyDictionary SkillOverrides { get; } public IReadOnlyList