Knowledge base

Character-customisation file chain

Research in progress: some of this may be wrong. These notes record what XF Studio’s research has worked out so far about how Cyberpunk 2077 and its modding frameworks behave. Most of it comes from reading source code and game resources offline; not all of it has been confirmed in the running game, and claims graded hypothesis are unproven. If you spot a mistake or know more, please open an issue or suggest an edit.

Evidence grades

source
Read in engine/framework/tool source or decompiled data.
resource
Observed in extracted game or mod resources.
wiki
Documented in the Modding Docs, text or image.
runtime
Observed in the running game.
hypothesis
Not yet established.

Maturity: Draft. Consolidated and cross-checked for vanilla 2.31 female and male resources, ArchiveXL 1.27.3 source, one decoded 2.31 save and the implemented resolver's run on the reference installation; consolidated category by category on 30 September 2026 in the file-chain map, which also lists the source contradictions and the resolver's ranked gaps. Not yet runtime-tested in the areas marked hypothesis; the creator checks run in session 4 are in in-game results. Evidence grades follow the knowledge rules: source engine/framework/tool source, resource extracted game or mod resources, wiki Modding Docs text or image, runtime running game, hypothesis not yet established.

This page answers four questions for XF Studio agents:

  1. How does a character-creator (CC) choice become geometry and materials? (render any V in the Studio viewport)
  2. What options exist, and how are they identified? (expose every CC option as a Studio control)
  3. How does a save store the choices? (later write-back and shareable CC presets)
  4. How do mods add options? (our own eye-makeup selector via ArchiveXL/CCXL)

Material and shader internals (.mt/.remt templates, skin, hair, eye and decal shaders) are covered in materials and shaders; this page stops at "which material instance and which inputs".

Evidence snapshot

SourceVersionWhat it established
Installed game resources2.31 (resource GameVersion 2310). female_cco, male_cco and their _ep1 twins; representative .app, player .ent and .charcustpreset filesOption lists, groups, links, .app → .ent → component wiring. Extracted with WolvenKit CLI 8.17.4 (unbundle, convert serialize) into ignored research/consumers/cc-file-chain/. SHA-256 of the four .inkcharcustomization resources are in the option inventory.
Vanilla CCO evidence2026-09-25Extraction steps, resource hashes and the detailed observations behind sections 2, 3, 6 and 7.
CC option inventoryxfs/cc-option-inventory-1, generated from the aboveMachine-readable, asset-free: every vanilla group, option, choice name, .app path, link family, preset and version-migration entry for both body genders; EP1 files as diffs.
ArchiveXL source1.27.3, commit 5474e34dCCXL merge, fixes, scopes, dynamic appearances (merge boundary).
WolvenKit sourcecommit 11720772Save node reader and writer, resource types (gameuiCharacterCustomizationPreset, …UiPreset, CC controller components).
Reference save2.31, save version 269, preset version 12One real female V: groups, resolved appearances, morphs (save import).
Modding Docs clonebe2f44eeDocumented relationships and editor screenshots (wiki chain map); CCXL guides.
XF Studio character resolver2026-09-25, reference installation (MO2 route)Resolves the reference save and PRC option 12 from data; reproduces the hand-traced chains and records every unproven precedence rule. Validation
Head CC render evidence2026-09-25, resolver at afb474e on four synthetic creator states (direct route) plus the reference save; one MO2 profile scanned for skin overridesMaterial templates and key parameters of every head option, the skin type/tone split, decoded skin tint arithmetic, and how complexion mods take effect (head CC rendering).
Installed game scripts, TweakDB and texts2.31 final.redscripts decompiled with redscript-cli 0.5.31; tweakdb_ep1.bin; English onscreens.json (base and EP1); ArchiveXL 1.27.3 Localization source; 2026-09-26How the creator lists, orders, groups, labels and colours options, and where each part lives (presentation); the Studio's creator catalogue on the reference installation (CC controls backlog).
File-chain map2026-09-30: the vanilla resources re-read, ArchiveXL's bundled per-category files and ResourcePatch source, the decompiled creator scripts, the Studio catalogue of the reference installation (26 September) and a read-only scan of its 1,052 .xl files; the wiki clone at a54b0873 (no CC page changed since be2f44ee)Every category's chain in two tables, 13 resolved or narrowed contradictions, the resolver's ranked gaps
Legacy xf-omega generatorcommit 28c822ea (2025-08-10), design notes in the legacy sx-cp2077 docs at b139ccab (2026-04-18)The predecessor female eye-makeup CCXL generator. The reference save still contains three of its selections, so the game accepted its resources. Reference only: its lessons are summarised in legacy lessons.

1. The chain at a glance

Diagram. Its boxes and arrows are listed here as text; GitHub draws it as a flowchart. View the drawn diagram on GitHub.

Arrows

  1. .archive.xl arrow to Sex-specific .inkcharcustomization (ArchiveXL merges)
  2. Sex-specific .inkcharcustomization arrow to SwitcherInfo choice
  3. SwitcherInfo choice arrow to AppearanceInfo choice
  4. SwitcherInfo choice arrow to MorphInfo choice
  5. Sex-specific .inkcharcustomization arrow to AppearanceInfo choice
  6. Sex-specific .inkcharcustomization arrow to MorphInfo choice
  7. AppearanceInfo choice arrow to .app appearance
  8. MorphInfo choice arrow to MorphTargetManager
  9. .app appearance arrow to inline components[]
  10. .app appearance arrow to partsValues: component .ent
  11. .app appearance arrow to partsOverrides:
  12. inline components[] arrow to Component
  13. partsValues: component .ent arrow to Component
  14. partsOverrides: arrow to Component
  15. Component arrow to .morphtarget (morphResource)
  16. Component arrow to .mesh (mesh)
  17. .morphtarget arrow to .mesh
  18. .mesh arrow to meshAppearance → chunkMaterials
  19. meshAppearance → chunkMaterials arrow to materialEntries → local instance
  20. materialEntries → local instance arrow to .mi chain → .mt / .remt
  21. MorphTargetManager dotted arrow to Component

Boxes

.archive.xl
customizations: female / male
Sex-specific .inkcharcustomization
female_cco(_ep1) / male_cco(_ep1); head · body · arms: options + groups
SwitcherInfo choice
activates other named options
AppearanceInfo choice
.app resource + definition name
MorphInfo choice
region + target name
.app appearance
(name = CC definition)
MorphTargetManager
applies (target, region) to every; morph component that has it
partsOverrides:
componentName → meshAppearance, chunkMask
Component
MorphTargetSkinnedMesh · SkinnedMesh · GarmentSkinnedMesh
.morphtarget
baseMesh + named deltas
meshAppearance → chunkMaterials
chunkMask hides submeshes
materialEntries → local instance
or external .mi
.mi chain → .mt / .remt
+ .xbm, .hp, .mlsetup, .sp
Mermaid source
flowchart TD
    XL[".archive.xl<br/>customizations: female / male"] -->|ArchiveXL merges| CCO
    CCO["Sex-specific .inkcharcustomization<br/>female_cco(_ep1) / male_cco(_ep1)<br/>head · body · arms: options + groups"]
    CCO --> SW["SwitcherInfo choice<br/>activates other named options"]
    SW --> AO
    SW --> MO
    CCO --> AO["AppearanceInfo choice<br/>.app resource + definition name"]
    CCO --> MO["MorphInfo choice<br/>region + target name"]
    AO --> APP[".app appearance<br/>(name = CC definition)"]
    MO --> MTM["MorphTargetManager<br/>applies (target, region) to every<br/>morph component that has it"]
    APP --> INL["inline components[]"]
    APP --> PV["partsValues: component .ent"]
    APP --> PO["partsOverrides:<br/>componentName → meshAppearance, chunkMask"]
    INL --> COMP["Component<br/>MorphTargetSkinnedMesh · SkinnedMesh · GarmentSkinnedMesh"]
    PV --> COMP
    PO --> COMP
    COMP -->|morphResource| MT[".morphtarget<br/>baseMesh + named deltas"]
    COMP -->|mesh| MESH[".mesh"]
    MT --> MESH
    MESH --> MA["meshAppearance → chunkMaterials<br/>chunkMask hides submeshes"]
    MA --> ME["materialEntries → local instance<br/>or external .mi"]
    ME --> MI[".mi chain → .mt / .remt<br/>+ .xbm, .hp, .mlsetup, .sp"]
    MTM -.-> COMP

Read the diagram top-down. The three option types branch differently: an appearance option names an .app and one of its definitions; a morph option names a morph target that is applied by (target, region) across components; a switcher only activates other options. An .app appearance can list components inline and/or pull them from component .ent files; both are overridden by name. Rendered and inspected: see diagram review.

Naming conventions resource

Resource names are systematic, which makes a resolver and a human reviewer much faster. These meanings are inferred from consistent use across the extracted resources, not from CDPR documentation.

TokenMeaningExamples
pwa / pmaplayer woman / man, average buildh0_000_pwa__morphs.morphtarget, player_man_average\
wa / mawoman / man (shared or NPC body)t0_000_wa_base__full_breast_big
h0_head skin meshh0_000_pwa_c__basehead.mesh
he_eyes mesh (eyeballs and vanilla lashes as separate chunks)he_000_pwa_c__basehead.mesh
heb_eyebrowsheb_000_pwa__morphs.morphtarget
hel_eyelash appearance resource (it re-uses the he_ mesh)hel_000__basehead.app
ht_teeth and mouth interiorht_000_pwa__morphs.morphtarget
hx_head decals and overlays: makeup, freckles/blush, pimples, tattoos, scars, face cyberware, personal-link decalhx_000_pwa__morphs_makeup_eyes_01.morphtarget
hh_ / hb_head hair / facial hair (beards)hh_000_pwa__hairs_059.app, hb_000_pma__big_beard.app
i0_ / i1_intimate items (nipples, genitals, pubic hair) / head items (earrings)i0_000_base__genitals.app, i1_000_pwa__morphs_earring_01.morphtarget
t0_ / tx_torso and full body / body tattoost0_000_pwa_base__full.morphtarget, tx_000_pwa_base__full_tattoo_01
a0_arms, hands, nails, arm cyberwarea0_000_base__nails.app
l0_ / n0_legs and feet / FPP neckl0_000_base__cs_flat.app, n0_000_base_fpp__neck.app
_c__a .mesh (the "component" mesh)h0_000_pwa_c__basehead.mesh
__morphsa .morphtargethe_000_pwa__morphs.morphtarget
NN_xx_nameskin tone: number, ethnicity family, name01_ca_pale, 05_bl_espresso; variant 01_ca_pale_00_warm_ivory

2. The .inkcharcustomization resource

Root type gameuiCharacterCustomizationInfoResource resource source. There is one per body gender, and the game ships two pairs:

ResourceHead / body / arms optionsHead / body / arms groups
base\gameplay\gui\fullscreen\main_menu\female_cco.inkcharcustomization405 / 73 / 3210 / 7 / 18
base\gameplay\gui\fullscreen\main_menu\male_cco.inkcharcustomization483 / 52 / 2311 / 5 / 13
ep1\gameplay\gui\fullscreen\main_menu\female_cco_ep1.inkcharcustomizationsame option names as base; 34 head, 2 body option diffssame names; reordered groups, two extra arms group entries
ep1\gameplay\gui\fullscreen\main_menu\male_cco_ep1.inkcharcustomizationsame option names; 15 head, 1 body option diffsTPP/TPP_photomode gain eyelash_color and pimples

The EP1 twin is the one in use when Phantom Liberty is installed. The reference save's tpp_head_face_rig hash 14034739559546167190 equals FNV-1a64 of ep1\characters\head\player_base_heads\appearances\head\face_rig\h0_000__basehead_face_rig_ep1.app, a path only the _ep1 resources reference resource. ArchiveXL's bundled fixes target all four paths [source: bundle/source/resources/PlayerCustomization*Fix.xl]. A resolver must therefore pick the pair that matches the installation, not assume the base\ path. The diffs are small (skin-colour definition metadata with unchanged names, a few index and link fixes, male lash/pimple group membership, the EP1 face rig) and recorded in the inventory.

Root fields

FieldContentMerged by ArchiveXL?
headCustomizationOptions, bodyCustomizationOptions, armsCustomizationOptionsHandles to options (below)Yes
headGroups, bodyGroups, armsGroupsgameuiOptionsGroup {name, options: CName[]}Yes, existing group names only
perspectiveInfo{name, fpp, tpp} group pairs for body and arms (e.g. holstered_strong → holstered_strong_fpp / _tpp)No
uiPresetsthree named .charcustpreset resources: corpo, nomad, streetNo
excludedFromRandomizeoption names (genitals, pubic hair, etc.)No
version12 (matches the save's preset version 12)No
versionUpdateInfoper-version option migrations for older saves, e.g. version 7 activates hair_color_fpp_01 (definition default) wherever hair_color1 is active, without replacing itNo

Merge behaviour: source ArchiveXL Extension.cpp lines 254–332; see the merge boundary.

Option types resource source

All options share name, index, defaultIndex, localizedName (a localisation key such as UI-CharacterCreation-eyes), uiSlot, link, linkController, editTags, hidden, enabled, randomizeCategory, censorFlag, censorFlagAction, onDeactivateActions.

TypeExtra fieldsChoice listWhat a choice points at
gameuiAppearanceInforesource (.app, soft), useThumbnailsdefinitions[]: gameuiIndexedAppearanceDefinition {name, index, localizedName, color, icon (TweakDBID, e.g. OptionsIcons.BrownLiquorice), tags, actions, randomizationInfo}The .app appearance whose name equals definition.name. An empty resource plus definitions is a colour-only controller (skin_color).
gameuiSwitcherInfouiSlots[], switchVisibilityoptions[]: gameuiSwitcherOption {index, localizedName, names[], actions, tags}Other option names to activate. Only the chosen choice's names are active; the switcher's other targets are inactive even when enabled (see slots and switchers). One choice can activate several options at once, e.g. hairstyle N activates hair_colorN and hair_color_fpp_N.
gameuiMorphInfononemorphNames[]: {index, localizedName, morphName}; None means no morphA morph target name. Region = option name.

Identity, and why indices are not identity

  • Option identity: name within the head/body/arms list. Names are stable across base/EP1 resource.
  • Choice identity: (option, definition name) for appearances, (region, morphName) for morphs, localizedName for switcher choices. ArchiveXL merges choices on exactly these keys [source: Extension.cpp 465–557].
  • Indices are regenerated. ArchiveXL rewrites every choice index after merging [source: RegenerateIndexes, lines 605–637], and vanilla itself contains duplicate option indices (makeupEyes_09 and _10 are both 460) resource. Never persist an index as identity. The Studio should match saves by (app hash, definition) and (region, target) as the resolver contract already requires.

link is a free-form key shared by a family of options. Options with linkController = 1 are the ones the UI drives; followers with linkController = 0 (usually hidden) receive the same choice index. Evidence: every member of a family has the same number of definitions, and the vanilla UI presets store equal values across the family.

Link keyController(s)FollowersChoices
skin colorskin_color (empty resource, 12 colour definitions)97 female / 72 male options: all skin_type_0N, head/neck proxies, face cyberware, facial tattoos, body, arms (every cyberware and holster variant), nipples, genitals, feet12 skin tones
hairstyle / hairstyle colorhairstyle, hairstyle_cyberware / every hair_colorNFPP hair shadows51 styles / 35 colours
eyebrows coloreach eyebrows_colorN–35 colours
beard color (male)each beard_colorN_M–35 colours
makeupEyes color, makeupLips color, makeupCheeks color, makeupPimples color, piercings coloreach style option–14, 1–14, 4 or 16, 6, 16
makeupLips typemakeupLips, makeupLips_glossy, makeupLips_matte–38 lip styles per finish
nails_color, nails_sizenails_color(_tpp), nails_lnails_color_fpp, launcher/mantis nail variants, nails_r54 colours, 2 lengths
breast_size, genitals_size, nipples type, body_tattoo, genitals hairstyle type/colorthe visible option(s)FPP twins or the alternative anatomy options–

The wiki's CC options page (manavortex and redacted-c01, 2025) states that link syncs a selection across options wiki; its example, that hair colour also sets the beard, does not hold for 2.31: hair colours use hairstyle color and beard colours beard color, so nothing links the two resource (map contradiction C3). The vanilla UI preset ui_preset_female_corpo.charcustpreset stores skin_color = 10 and the value 10 for body_color, nipples_01, genitals_04, every arm option and neck; nails_color_tpp = nails_color_fpp = 31 resource. This is the strongest offline evidence for the link semantics; the exact propagation code is native and unread [hypothesis for edge cases, e.g. followers with a different choice count].

EP1 anomaly resource: in female_cco_ep1, eye-makeup styles 21–36 carry the link key LocKey#43269 instead of makeupEyes color, so under the link rule a colour chosen on styles 1–20 would not carry over to 21–36 (test ask 8).

Consequence for the Studio: choosing a skin tone is one control, but the renderer must apply that index to every linked option that is active, including tattoos and face cyberware whose colour follows skin tone.

Switcher link members: hairstyle and hairstyle_cyberware are both link controllers of hairstyle, but list their styles in different orders (they differ at 20 positions on the reference installation) resource. The Studio's character context carries a switcher member's choice as the choice that activates the same options, not the same position, and leaves a member without such a choice at its own (CORE-61). In game, a face cyberware that swaps the row keeps the same hair, at the same position for style 5 runtime (session 4); a style at one of the 20 differing positions would separate the two rules.

How the Studio stores and applies links (character context, src/character-context.ts): only the choice a person set is stored, one per link family (the latest wins); every other member, controllers and followers, is derived when the V is prepared (CORE-50, CORE-51). Resetting a member resets the family to the V's own. A portable preset therefore holds one hair colour, not every hairstyle's colour.

Slots, switchers and visibility resource

  • A switcher's uiSlots name the slots whose content it swaps. makeupLips_type (slot makeupLips_type) → makeupLips/_glossy/_matte switchers (slot makeupLips) → 38 appearance options each (slot makeupLips_color). This is a three-level tree: finish → style → colour.
  • A switcher owns its targets. An option that any switcher choice names is active only while an active switcher's current choice names it. Its own enabled flag does not keep it active beside another choice. Most targets are enabled = 0, but each switcher's default target is enabled = 1 (skin_type_01, eyebrows_color1, hair_color1, cyberware_00, hairstyle under cyberware), and the vanilla UI presets store it inactive whenever another choice is chosen. Targets of an inactive switcher are inactive too. An option that no switcher names is active when enabled. Evidence: all six vanilla UI presets, 1,736 target checks with no exception, and the reference save; the native code is unread (evidence). This matches the wiki's rule that a uiSlot holds one active option at a time wiki.
  • "Off" is what the creator labels Off. The creator shows a choice's localizedName and nothing else (see Labels below), so a choice is Off exactly when its label is the game's Off text (Common-Off, "OFF" in English) [source + resource]. In 2.31 that label sits on 17 feminine rows' choices (eye, lip, cheek and blemish makeup, scars, tattoos, piercings, pubic hair, body scars, and the legacy XF Eye Artistry layers on the reference installation). Adding nothing is not Off: the first nipples choice activates nipples_01, an option with no .app because those nipples are in the skin's albedo, and reads 01; every face morph's None is the base shape (01); a breast size's None is Default; the bald hairstyle is 51 resource. The other way round, the choice labelled Off on nipples (nipples_04), genitals and face cyberware adds an appearance resource. What is true of adding nothing: those choices are a None definition (on an option with no .app, such as piercings_00, makeupEyes_00, hair_color_none, or as definition 0 of one with an .app, such as scars), a switcher choice whose options add nothing, or a morph's None; they name no appearance, and the save stores no entry for them.
  • hidden = 1 options never get their own UI row (proxies, FPP twins, colour followers).
  • switchVisibility and actions exist on switchers and choices; vanilla actions are rare. switchVisibility does not decide target deactivation: skin_type and cyberware have it at 0 and still deactivate their defaults resource. Its meaning is unknown. source ArchiveXL does not copy switchVisibility, hidden, enabled or linkController onto an existing option during a merge, only onto a wholly new option.
  • editTags = NewGame, HairDresser, Ripperdoc: which editing context may change the option (new game, mirror/hairdresser, ripperdoc) resource. Body skin colour is NewGame/Ripperdoc; makeup and hair add HairDresser. Which context applies is the menu's, not the caller's wish: a creator opened from the pause menu's scenario switch reports NewGame and makes every NewGame row editable even when asked for HairDresser runtime (below).

Groups: who consumes a choice resource

Groups do not define the UI; they tell each consumer which active options belong to it. The same option can sit in several groups (e.g. eyes_color is in TPP, TPP_photomode and character_customization).

Group (head)ContentsConsumer
TPP, TPP_photomodehead skin, eyes/eye colour, lashes, brows, teeth, facial tattoos, scars, pimples, morph regions, face rigThe third-person head (the head is an item in AttachmentSlots.TppHead [source: ArchiveXL PuppetState/Handler.cpp:19]); photo-mode puppet. Consumer wiring hypothesis.
faceface cyberware, piercings, eye/lip/cheek makeupgameuiCharacterCustomizationFaceController {groupName: face} on the player entities resource. CCXL makeup, including the legacy selectors in the reference save, lands here. An XF Eye Artistry build whose option sat only in character_customization rendered in the creator but not in photo mode; vanilla makeupEyes_NN sits in both groups [runtime, 25 September (finish board results): consistent with this consumer, not a trace of it].
hairs, FPP_hairshair style/colour; FPP shadow-only hair. Multi-part hairstyles put each colourable part here as its own option (multi-part hairstyles); on the reference installation hairs holds only hair_color and mch_hair_part_* optionsgameuiCharacterCustomizationHairstyleController {groupName: hairs}
beards (male only)beard optionsgameuiCharacterCustomizationBeardController
FPP, TPP_proxy, FPP_proxyFPP neck; low-detail head/neck proxiesFPP body; LOD/shadow proxies hypothesis
character_customizationnearly every head optionThe creator preview puppet. The save duplicates choices here.
finalSceneBruisesfinalSceneBruises (quest q307 beaten-up face)gameuiCharacterCustomizationBrokenNoseController {finalSceneGroup} type exists source; quest-driven

Body groups: FPP_Body/TPP_Body (perspective pair), character_creation, genitals and breast (consumed by gameuiCharacterCustomizationGenitalsController {bottomBodyGroupName: genitals, upperBodyGroupName: breast}), lifted_feet/flat_feet (…FeetController). Arms groups are holster/cyberware states (holstered_default, holstered_strong, holstered_nanowire, holstered_launcher, holstered_mantis, their unholstered_*, personal_link_*) consumed via perspectiveInfo, plus nails (…NailsController) and …ArmCyberwareController. Female arms groups are split _tpp/_fpp; male groups are not resource.

Presentation: order, rows, sections, labels and swatches

How the creator presents the options, and where the Studio's catalogue (src/cc-catalogue.ts) takes each part from. Sources: the installed 2.31 game scripts (r6\cache\final.redscripts, SHA-256 2119046f…ee86, no redscript backup beside it, decompiled with redscript-cli 0.5.31 decompile -f into a private scratch folder; files cyberpunk/UI/fullscreen/pregame/characterCreationBodyMorphMenu.script, characterCreationBodyMorphListItem.script, characterCreationPunkRandomizerMenu.script and orphans.script), the compiled TweakDB (r6\cache\tweakdb_ep1.bin, SHA-256 89c7ee67…14c5; tweakdb.bin 918f0acf…13f9 holds the same category list) and the English text resources.

QuestionAnswerGrade
One list or pages?One list. InitializeList takes GetUnitedOptions(true, true, true) (head, body and arms together) and creates a row for each option that is isEditable && isActive && !isCensored. The creator has no section headers.source
OrderThe options' index ("determines order of switchers in Character Creator"; two top-level options with the same index show only the first) [wiki: icxrus's switcher theory and the CCXL overview]. Vanilla head indices run 5 (skin tone) to 700, arms 800–820, body 1010 and up. The two creators number differently: teeth and eye makeup are 310 and 450 in female_cco_ep1 but 540 and 550 in male_cco_ep1, where 300–380 hold the beard options (311 is beard_color5_0), so a mod option needs its index chosen per creator (XF Eye Artistry uses 311 and 541) resource. The sort itself is native and unread.wiki + resource + [hypothesis: native sort by index]
RowsA row is found again by uiSlot (UpdateOption compares lookupOption.info.uiSlot with the row's option): when a switcher changes its target, the new option takes the old one's row. So options sharing a slot are one row.source
Which options get a rowisEditable and isActive are native. In the data, every hidden option and every link follower (a link key with linkController = 0: the neck, FPP twins, the skin types' tone) lacks a label; the Studio shows neither. editTags must include the editing context (NewGame, HairDresser, Ripperdoc; the menu calls ApplyEditTag).source + [hypothesis for followers]
Selector or colour griduseThumbnails on an appearance option spawns the colour picker (a grid, six per row); anything else a stepper.source
SectionsThe creator's randomizer lists categories from TweakDB CharacterRandomization.CharacterRandomizationCategories.list: CharacterRandomizationCategoryUI records whose categoryName is a text key and whose categoryType.enumName equals the options' randomizeCategory. 2.31 order: Skin, Hair, Eyes, Eyebrows, Face, FacialHair, Scars, Tattoos, FaceModification, Makeup, Nails, Body (UI-CharacterCreation-skin to -body). The Studio uses this as its section order and titles. A slot's Off placeholder may carry the default Body (masculine beard_color0), so a row is filed under the category most of its options name.resource + [source: InitializeRandomizerLocksList]
LabelsOptions and choices pass localizedName straight to the text widget (SetText(label, option.info.localizedName)), so a key shows its text and anything else shows as written (the choice numbers 01, 02). LocKey#<n> is a primary key; UI-CharacterCreation-eyes and Common-Off are secondary keys.source
Where texts livebase\localization\<lang>\onscreens\onscreens.json in archive/pc/content/lang_<code>_text.archive (60,303 English entries) and ep1\localization\<lang>\onscreens\onscreens.json in archive/pc/ep1/lang_<code>_text.archive (10,276; no key shared with base). Each is a JsonResource holding localizationPersistenceOnScreenEntries {primaryKey, secondaryKey, femaleVariant, maleVariant}. onscreens_final.json beside each is byte-identical in 2.31. No creator text has a male variant.resource
Player's language%LOCALAPPDATA%\CD Projekt Red\Cyberpunk 2077\UserSettings.json, group /language, option OnScreen (e.g. en-us).resource
Mod textsArchiveXL localization.onscreens in any .xl (a map of language to a path or list; the first language is the fallback; a bare path or list means English; extend appends to another file's unit). Per unit in load order: the player's language merges first, then (unless it is the fallback) the fallback language, which only adds keys that are missing; a unit without files for the language merges its fallback's, adding only (OnLoadTexts). Every entry is identified by its primary key alone: a mod entry with an existing primary key replaces that whole entry, secondary key included, and one with a new primary key but an existing secondary key replaces nothing by ID. An entry without a primary key is added twice, under FNV-1a 32 of its secondary key (keeping the key) and under FNV-1a 64 of it (without) (MergeTextResource, MergeTextEntry). The Studio's text table keys entries the same way (src/game-text.ts, PIPE-47); which of two entries holding the same secondary key the native lookup answers is not read (the Studio answers with the one merged last) hypothesis. The reference installation declares 924 English files in 735 archives. A language whose lang_<code>_text.archive isn't installed (no mounted archive holds its texts) falls back to English, with the gap texts-language-missing; texts that are there but couldn't be read keep the language and say so instead (texts-transient, texts-need-wolvenkit).source
SwatchesThe swatch tints its background with the choice's color and draws its icon, a UIIcon record, from atlasResourcePath (an .inkatlas) and atlasPartName (SetTintColor in the list item). Icons added by TweakXL mods are not in the compiled blob: CCXL mods declare them in r6/tweaks YAML, usually one $instances template per mod (OptionsIcons.<mod>_$(name) of type gamedataUIIcon_Record). On the reference installation the compiled blob and the installed YAML together resolve every eye colour's icon, across 11 atlases (1 vanilla, 10 from mods; 551 parts used by colour rows). The vanilla creator atlas keeps its parts in slot 0, with none at the top.source + resource
Camera per optionGetSlotName picks the creator camera by option name and slot (UI_Skin, UI_Hairs, UI_Eyes, UI_Teeth, UI_Nose, UI_Lips, UI_Jaw, UI_FingerNails, else a head or body preview). This is the one grouping the game keeps only in UI code; the Studio doesn't use it yet.source

The TweakDB blob is read with WolvenKit's layout (flats by type, (TweakDBID, index) keys). A record's field ID is the CRC-32 of the record name continued over .<field>, so fields of records known only by ID (the category list's entries) are addressable without their names (src/tweakdb-flats.ts). Every count in the blob is bounded by the bytes that could hold it before anything is allocated (PIPE-49).

In the Studio. The host builds each body gender's catalogue once per installation and sends the Character panel a compact projection (src/cc-panel.ts, xfs/cc-panel-5, about 319 KB for the reference installation's 943 options and 131,856 choices): sections, rows, options (label, type, choice count, Off and default keys, link, the switchers that turn it on), with mods and coverage notes in tables by index and each mod's maker group (the base game; XF Studio's own mods, known by the xfs_ prefix of their creator resource; the author Vortex records as author or uploader; else the mod's own name, since MO2's meta.ini records no author [observed: 1,000 meta.ini files on the reference instance]), so a row can group its choices by who made them; an option's choices come in pages of 240 when its row opens, and a search over every choice runs on the host. A switcher choice is identified by the options it activates as well as its name (the creator picks by position, so two same-named choices stay apart; CORE-70). An option's Off is the choice the creator labels Off (labelledOff: the game's Off text, or "Off" written out), never a choice inferred from adding nothing (off, kept only as structure: a save stores no entry for it).

Swatches in the Studio (src/cc-swatch.ts, src/cc-swatch-host.ts): every colour row's choices get a swatch derived from the resource that wins for them, the way the preview draws it: a hair or eyelash profile's baked root-to-tip row (hair shading §3) as five colours, the iris ring's base colour, the skin's toned albedo, a decal's colour times its texture's coverage-weighted colour (brows through their gradient map). Choices that follow one link share a swatch per choice key, answered by one member's chain (a hair colour across 522 hairstyles); a colour-only controller (the skin tone) is answered by a drawn follower at the same position, as the link applies it. On the reference installation 2,231 swatches cover 127,648 of the 129,064 choices in colour rows; the rest are multilayered eyes (which have icons) and a few chains that resolve nothing. Hair-tone packs that replace vanilla profiles at their paths make every vanilla hair, brow and lash swatch a mod's colour: a colour whose profile, gradient, colour texture or colour-setting material instance comes from a mod other than the choice's own supplier is marked replaced (24,888 choices), and the panel shows its derived colour instead of the icon, which would show the replaced colour. Icons come from the compiled TweakDB overlaid with the installed TweakXL YAML (src/tweakxl-overlay.ts, src/cc-icon-host.ts). What the preview draws of each option is the preview side's projection (catalogueCoverage, src/cc-render-coverage.ts), never stored in the catalogue (CORE-60); a conditional option settles from the parts the shown V's record draws. A head option whose resource names no category (the enum's default, Body) is filed with the rows around its position instead (UI-60). A base creator resource that a mod archive replaces names that mod as the source of its options and choices (PIPE-46).

3. From .app to pixels

.app definitions resource wiki

Every vanilla CC .app holds both body genders' appearances, distinguished by name (…pwa…/…pma…, female__…/male__…, w__/m__) and visualTags (Female/Male). The CC definition name equals the appearance name. Typical definition:

  • partsValues[]: one or more component .ent files, e.g. …\appearances\entity\face_decals\hx_000_pwa__basehead_makeup_eyes_01.ent.
  • partsOverrides[].componentsOverrides[]: {componentName, meshAppearance, chunkMask} applied to that .ent's components. The chunk mask only hides: it is ANDed with the component's own, so an override can't show a chunk the component hides [source: ArchiveXL ComponentState::AddHidingChunkMaskOverride] [wiki: the .app page, "You can't un-hide something via partsOverrides"]. A mod's copy of an .app can therefore mask a creator part out entirely (body animation §4).
  • components[]: inline components. In vanilla CC .apps they mirror the partsValues components with the overrides applied, but inline components can also be the only source of geometry (PRC and the legacy generator rely on this; see section 6). The wiki chain map covers partsValues/partsOverrides screenshots and ArchiveXL's empty-partResource handling.

Three variant mechanisms appear, often combined:

MechanismUsed byEvidence
meshAppearance selects material setskin tones (01_ca_pale), eye colours (gradient_green), hair/brow/lash colours (blonde_platinum__01, eyelashes__…), makeup colours (black_01), lip finish (black_01_02 glossy, black_01_03 matte), piercing metalsresource
chunkMask selects submeshesscars (one scars_01 appearance; each scar is a different mask), eyes vs vanilla lashes (same he_ mesh: eyes mask …551614 hides chunk 0, lashes mask …551609 hides chunks 1–2), earrings, body/feetresource; the he_ mesh has three chunks (12,393 / 668 / 152 vertices) per native-eye intake
Different morphtarget / meshmakeup styles (makeup_eyes_01…36), tattoo designs, hairstyles, beards, cyberware variantsresource

Makeup and decal .app overrides also name the head skin component MorphTargetSkinnedMesh7243, which the decal .ent does not own; its purpose is unknown [hypothesis: harmless carry-over from a shared authoring template].

Components resource

Component typeUsed forDeformation
entMorphTargetSkinnedMeshComponent (morphResource)head, eyes, brows, teeth, all head decals (makeup, tattoos, scars, cyberware), earrings, female body/breast, genitals, beardsSkinned and receives CC morph targets
entSkinnedMeshComponent (mesh)hair, hair shadow meshes, FPP neck, seam-fix body piecesSkinned only
entGarmentSkinnedMeshComponent (mesh)arms, nails, nipples (FPP), feet, body scars/tattoos (male), censorship underpantsSkinned; participates in garment/visual-tag hiding ([ArchiveXL VisualTags.xl])
entAnimatedComponentface rig graph (…paperdoll_sermo.animgraph), hair dangle physics, genital dangleAnimation

entMorphTargetManagerComponent on the player entity resource is the likely applier of the region/target pairs to all morph components hypothesis.

Morph targets resource wiki

  • .morphtarget = baseMesh reference plus an embedded geometry blob and targets[], each {name, regionName}, e.g. {h091, eyes} resource. WolvenKit's GLB export names the shape keys <name>_<region> (h091_eyes), which is what the Studio's assets use; the head/plate master carries 105 female targets (5 regions × 21).
  • Female morph options offer None plus 21 targets per region (h011…h211 for eyes, h0N2 nose, h0N3 mouth, h0N4 jaw, h0N5 ear); male offers 20 (…h201) resource. The last digit encodes the region.
  • Every component that follows the face carries matching targets, but only for the regions it needs: head and brows carry all 105; eyes and lashes the 21 eyes targets; a vanilla earring placeholder the 21 ear targets; a nose ring the 21 nose targets; a PRC linked-mesh ring all 105 resource. The pair (target, region) is the key, and the save stores both. A component without a region's targets simply does not follow that slider [hypothesis; consistent with every inspected resource].
  • The eyes follow the eye shape through their own morph resource. The installed eye entity he_000_pwa__basehead.ent binds he_000_pwa__morphs.morphtarget (21 eyes targets, baseMesh = he_000_pwa_c__basehead.mesh), separate from the head's h0_000_pwa__morphs.morphtarget; both carry the pair {h091, eyes} and the other 20, and the 34 joint matrices the two h091 targets share are identical resource (experiment 015 trace). A shape moves the eyeball, lash and wetness chunks by up to about 4 mm. XF Studio's derived preview applies each eye-shape choice to every mesh carrying that (target, region) pair (src/face-morphs.ts); with the eyes left static, 18 of the 21 shapes left the eyeball showing through 35–354 of the 680 lid/socket rim vertices; with the eye targets, 14 shapes are within one vertex of the neutral assembly's 12, the worst is 41 (h121), and h181/h191 rise from 3/7 to 16/19 (offline, derived 2.31 assets, nearest-vertex signed-gap proxy) [resource; game rendering not compared].
  • Resource order is not option order. The eyes option's morphNames (None, h011 … h211) follow the morph resources' target order, but the vanilla female nose option lists h112 after h162 resource. Choice identity is the (region, target) pair; a selector must take its order and labels from the merged CCO, never from a mesh. The creator's choice labels are two-digit positions (localizedName 01 = None, 10 = h091) resource.
  • The morphtarget guide (no page author evidenced) says the engine activates targets by name and region, face regions blend together while chest size is exclusive, and mods cannot add new activation names, only reuse existing ones wiki. The creator side can grow: ArchiveXL appends a morph choice whose localizedName is new (Extension.cpp 504–528) source; whether the engine then applies a target name that only a mod's morph resources carry is untested hypothesis, and no installed mod adds morph choices.
  • Facial rigs. Each head .app uses a face-rig component; the game ships 22 female and 21 male per-shape skeleton rigs, and the NPV rig guide (saltypigloaf, 2025) reports that vanilla V always uses rig 000 whatever the sliders, a known cause of eye clipping that community rig-fix mods address wiki. The Studio's saved-head morph binds should account for this (brow idle gap, experiments 012/015).
  • Exporting a morph target with its rig. WolvenKit resolves a .morphtarget's baseMesh itself: CLI uncook of the morph target from the game archives (or export … -gp <game> of an extracted copy) writes a bone-bound GLB with the base mesh's skin (254 joints, two four-influence sets for the female head) and every target as a shape key. No custom exporter is needed. CLI 8.17.4 reproduces experiment 012's bound head byte for byte; 9.0.1 gives identical positions, UVs, influences and morph deltas but gives joint nodes rest rotations with matching inverse binds, so rest deformation is identical resource (experiment 013). The export keeps native vertex and triangle order, so the eye plate recipe's native face IDs select the same rows in the GLB resource.
  • Body morphs: breast (female: t0_000_wa_base__full_breast_small / none / _big), nails_l/nails_r (long nails), penis_base/penis_circumcised size. There is no vanilla body-shape slider beyond these.

Meshes and materials

meshAppearance → chunkMaterials names → materialEntries (local buffer or external .mi) → .mi chain → template. Resolved examples: skin → skin.mt with a four-level .mi chain (saved skin chain); teeth → teeth_base.mi → skin.mt with its own skin profile; hair → hair.mt + .hp profile (hair profile); brows → mesh_decal_double_diffuse-family instance with a gradient (brow audit); eye makeup → makeup_NN_<colour>.mi → makeup_color__NN_<colour>.mi → mesh_decal.mt on one shared mesh for all 36 styles; lipstick → mesh_decal_double_diffuse or mesh_decal depending on the style; cheeks, pimples, scars, facial tattoos, face cyberware and male stubble → mesh_decal; piercings → multilayered.mt; the eye mesh's third chunk (wetness) → eye_shadow.mt resource. The per-option templates, key parameters and chunk roles are tabulated in head CC rendering §1. Shader behaviour: materials and shaders.

4. Every CC detail, and what drives it

The file-chain map carries the same categories with their save groups, ArchiveXL support, installed CCXL extensions and the Studio's status. Female and male names differ only where stated. "Opt" is the option type (A = appearance, S = switcher, M = morph). All counts are vanilla 2.31 resource; per-option detail is in the inventory. The wiki's CCXL overview has a matching table of vanilla uiSlot names and option types wiki. Its character-creator cheat sheet (last updated January 2025) lists fewer choices than the 2.31 resources (e.g. eye makeup 1–20 against 36 styles, lip styles 1–20 against 38, face cyberware 1–8 against 16); the resource counts are current, but which choices the UI actually shows is test ask 9.

DetailOptions (type).app / resourcesVariant mechanismNotes
Voice toneNone: row 0 of the creator list is a separate voice-over switcher widget, not a creator option [source: characterCreationBodyMorphMenu.script UpdateVoiceOverWidget, OnVoiceOverSwitched]None–Sets the state's IsBrainGenderMale and plays a sample (TriggerVoiceToneSample); picking a body gender sets both genders alike (characterCreationGenderBackstoryMenu.script) source. Saved as isBrainGenderMale (§7)
Skin toneskin_color (A, colour-only, 12 tones, controller of skin color)none of its own; drives every linked optionLinked index → each follower's meshAppearance. On the head the tone is only the material chain's TintColor/TintScale (multiply, or overlay when the scale is negative); the albedo comes from the skin type (head CC rendering §2)Tones: 01_ca_pale, 01_ca_pale_00_warm_ivory, 02_ca_limestone, 02_ca_limestone_00_beige, 03_ca_senna, 03_ca_senna_00_amber, _01_honey, _02_band, 04_ca_almond, 04_ca_almond_00_umber, 05_bl_espresso, 06_bl_dark; same for both genders
Skin type (complexion)skin_type (S, 5) → skin_type_01…05 (A, 12 defs each)h0_000__basehead.app, …_d02…d05.app → h0_000_pwa__basehead.ent → h0_000_pwa__morphs.morphtarget → h0_000_pwa_c__basehead.meshDifferent .app per type; the head mesh has 60 local materials, one per (tone, type), whose Albedo is the type's h0_000_pwa_c__basehead_d0N.xbmChain to textures: saved skin chain
Facial morphs (eyes, nose, mouth, jaw, ears)eyes, nose, mouth, jaw, ear (M, 21/20 targets + None)head and all face-following morphtargetsNamed morph target per regionNo separate cheek/brow sliders in vanilla
Eye coloureyes_color (A, 71)he_000__basehead.app → he_000_pwa__morphs.morphtarget (21 eye targets)meshAppearance (e.g. gradient_brown), chunk 0 hiddenArchiveXL remaps this app to archive_xl\…\he_000_pwa__basehead.app and adds @eyes material names source; mod example: modded eye resolution
Eyelasheseyelash_color (A, 35 colours)hel_000__basehead.app re-uses the eye component he_000_pwa__baseheadmeshAppearance eyelashes__<colour> with chunks 1–2 hiddenNo lash style option in vanilla; CCXL lash styles need a core mod that adds an Eyelashes switcher (eyelashes_options on the reference installation), per icxrus's CCXL eyelashes guide wiki (head details)
Eyebrowseyebrows (S, 14 choices: 13 styles plus eyebrows_color0, which has no resource) → eyebrows_color1…13 (A, 35 colours each, link eyebrows color)heb_000__basehead_NN.app → face_decals\heb_000_pwa__basehead.ent → heb_000_pwa__morphs.morphtarget (105 targets)One .app per style, one shared mesh; style = texture set, colour by meshAppearance (<colour>__NN)ArchiveXL remaps to per-sex archive_xl\…\heb_000_pwa__basehead_NN.app source
Hair style + colourhairstyle (S, 51) → hair_colorN (A, 35 colours, tags e.g. brown_liquorice) + hair_color_fpp_N (A, hidden shadow); hairstyle_cyberware twin when face cyberware changes the scalphh_NNN_pwa__hairs_XXX(.app) → entity\hairs\…ent → hair .mesh (e.g. hh_033_wa__player.mesh) + shadow mesh + dangle animgraphmeshAppearance = colour name → material with .hp profileHair colour tags are appended to TweakDB ItemFactory.HairColors.hairColors by ArchiveXL for CCXL colours source; the reference save's tags are ash_brown and Short: the chosen colour definition's tag and the chosen hairstyle switcher choice's length tag. Every non-bald vanilla hairstyle choice carries one of Long, Short, Buzz or Dreads resource
Beard (male)beard (S, Off + 12) → 13 part switchers beard0…12 (S, slot beard_part, 1–7 parts each, 57 in all) → 57 beard_colorN_M (A, slot beard_color, 35 colours)facial_hairs\hb_000_pma__*.app → beard morphtarget + beard_shadow_01Two-level switcher; colour by meshAppearance with @beard dynamic names [source: PlayerCustomizationBeardFix.xl]Group beards
Eye makeupmakeupEyes (S, Off + 36) → makeupEyes_01…36 (A, 14 colours)makeup_eyes\hx_000__basehead_makeup_eyes_NN.app → face_decals\hx_000_pwa__basehead_makeup_eyes_NN.ent → hx_000_pwa__morphs_makeup_eyes_NN.morphtargetStyle = resource; colour = meshAppearance (black_01)Group face. The XF Studio product target
LipstickmakeupLips_type (S: none / regular / glossy / matte) → makeupLips, _glossy, _matte (S, 38 styles) → 114 style options (A, 1–14 colours)makeup_lips\hx_000__basehead_makeup_lips_NN(_02/_03).app → one hx_000_pwa__morphs_makeup_lips_01.morphtargetFinish = .app suffix and meshAppearance suffix _02/_03; style/colour by meshAppearanceNamed NPC looks (nicola, trauma, mox, …) are single-colour styles; regular-finish styles 30–38 have no edit tags
Cheeks / freckles / blushmakeupCheeks (S, Off + 24) → makeupCheeks_01…24 (A: 1–4 freckles, 4 colours; 5–24 blush makeup_cheeks_01…20, 16 colours)makeup_freckles\hx_000__basehead_makeup_{freckles,cheeks}_NN.app → hx_000_pwa__morphs_makeup_freckles_01.morphtargetStyle = resource; colour = meshAppearanceGroup face. The game also ships a makeup_cheeks\ folder with a differing hx_000__basehead_makeup_cheeks_01.app that no vanilla creator resource names resource
Pimples / blemishesmakeupPimples (S) → makeupPimples_01…03 (A, 6)pimples\hx_000__basehead_pimples_01.appmeshAppearance + chunkMaskGroup TPP
Facial tattoosfacial_tattoo (S, Off + 15) → facial_tattoo_01…15 (A, 12 tones, link skin color); legacy hidden tattoo (A, 7)tattoos\hx_000__tattoo_NN.app → entity\tattoo\hx_000_pwa__tattoo_NN.ent → tattoo morphtargetDesign = resource; colour follows skin tone (meshAppearance 01_ca_pale)
Face scarsscars (A, Off + 13)hx_000__scars.app → hx_000_pwa__morphs_scars_01.morphtargetSame meshAppearance, different chunkMask per scar
Face cyberwarecyberware (S, Off + 16) → cyberware_01…16 (A, 12 tones)hx_000__cyberware.app (386 appearances) → entity\cyberware\hx_000_pwa__cyberware_NN.entDesign = component; tone = linked meshAppearanceSwitcher choice also swaps hairstyle ↔ hairstyle_cyberware. Since patch 2.2, options 8–16 are appearances inside other decal meshes (e.g. makeup_eyes_01.mesh carries cyberware_01), per the head cheat sheet wiki. In 2.31 the resolver also finds this for lower-numbered choices (option 03 draws the freckle mesh's cyberware_04), and the appearances exist in the player meshes, contrary to the sheet's warning resource
Piercingspiercings (S, Off + 14 female, Off + 16 male) → piercings_NN (A, 16 metals/colours)piercings\i0_000__earring_NN.app → up to three entity\items\i1_000_pwa_earring__basehead_0N.ent + morphtargetschunkMask per style over four shared piece banks, meshAppearance per colourPiercings and jewellery
Teethteeth (A, 5: default, silver, gold, …)ht_000__basehead.app → ht_000_pwa__basehead.ent → ht_000_pwa__morphs.morphtargetmeshAppearanceMouth shape is the mouth morph, not this option
Neck / proxies / face rigneck, tpp_head_proxy, fpp_head_proxy (hidden, skin color), tpp_head_face_rig, …_photomoden0_000_base_fpp__neck.app, proxy\h0_000__basehead_proxy.app, face_rig\h0_000__basehead_face_rig(_ep1).app (animgraph + facial anims)–Not user-visible; required for a full in-game assembly
Body skinbody_color, fpp_body_color (+ _censored) (A, 12, hidden, skin color)t0_000_base__full.app → female t0_000_pwa_base__full.morphtarget; male t0_000_pma_base__full.meshTone by meshAppearance; chunkMask hides regions
Breast size (female)breast (M: small / none / big)t0_000_pwa_base__full.morphtarget targetsMorphGroup breast, genitals controller
Nipplesnipples (S) → nipples_01…04 female, _01…02 male (A, 12 tones) + FPP twinsi0_000_base__nipple.app, i0_000_fpp__nipple.appTone link
Genitals and pubic hairgenitals (S, 4 combinations of genitals_0N + penis_* size morph + *_hairstyle switcher)i0_000_base__genitals.app (88 appearances, both anatomies), i0_000_base__genitals_hairstyle_NN.appTone link; size morphs; hairstyle colour linkAvailable to both body genders; excluded from randomise; censorship options (underpants, *_censored)
Body tattoos / scarsbody_tattoo (S, Off + 7) + FPP twin; body_scars (S, 3 choices over 5 options)t0_000_base__tattoo_NN.app, t0_000_fpp__tattoo_NN.app, scars_000_base.appTone link; scars by chunkMaskFemale body tattoos are morph-skinned; male ones garment meshes
Feetlifted_feet, flat_feet (A, hidden, tone)l0_000_base__full.app, l0_000_base__cs_flat.appTone link, chunkMaskChosen by footwear (feet controller), not by the player
Nailsnails_l/nails_r (M, long/short) + nails_color(_tpp/_fpp) (A, 54: plain, …__multilayer designs)a0_000_base__nails.app, a0_000_fpp__nails.appMorph length; colour/design by meshAppearanceArms part; nails group
Arms and arm cyberware~25 hidden arm options per holster/weapon state (A, 12 tones)a0_000_* and arms\<cyberware>\…appTone link; state chosen by equipped cyberware via perspectiveInfo groupsNot a CC choice beyond tone

5. How ArchiveXL/CCXL extends the lists

Details and line references: CCXL merge boundary and catalogue probe. Summary source ArchiveXL 1.27.3:

  1. Registration. An .xl lists customizations: {female: [...], male: [...]}. ArchiveXL prefetches each resource and merges it into the loaded base male or female resource (MergeCustomEntries). There is no male/female sharing: a mod that supports both body genders registers two resources (e.g. *_pwa and *_pma).
  2. Fixes first. FixCustomizationOptions applies resource.fix.paths to base options before any mod merge: e.g. vanilla he_000__basehead.app → archive_xl\…\he_000_pwa__basehead.app, heb_000__basehead_NN.app and hel_000__basehead.app likewise. Each remap registers an app override so saved descriptors using the old path still resolve.
  3. Groups. Group entries are appended only to existing groups with the same name; a new group name is ignored.
  4. Named options merge into the same-named, same-typed option (definitions by name, morphs and switcher choices by localizedName), otherwise the whole option is appended. A new option may carry any metadata; a merge into an existing option does not change its top-level metadata. A merged choice whose key already exists replaces the existing choice (targetChoice = sourceChoice, lines 473, 517, 544), so an addition is additive only when its keys are new. Vanilla switcher choices are often labelled by position (01, 02, …): a mod choice labelled 15 in the piercings row would overwrite masculine V's vanilla style 15 (jewellery §5).
  5. Anonymous overlays (no name) target existing options by uiSlot and/or link, with * suffix wildcards, in a second pass. Source note: the link-wildcard flag is computed from the slot string (Extension.cpp:379 reads sourceSlotStr), so a link-only wildcard does not appear to take effect [source reading; not runtime-tested].
  6. App overrides. When a merged definition's option names a different .app than the target option, ArchiveXL records (target app, definition) → (mod app, definition) and rewrites appearance descriptors at OnGetAppearances/OnChangeAppearance(s). This is why a saved choice can carry the vanilla-option-associated app hash yet render from a mod app (see the Unique Eyes example in the catalogue probe).
  7. Dynamic customization appearances. FixCustomizationAppearance clones a template appearance inside the player_customization.app scope for a missing requested name; the suffix after __ becomes the mesh appearance. Combined with mesh names/context fixes (@eyes, @lashes, @beard, @cap, @dread), one template can serve many colours. The scopes and extensions guide (manavortex) illustrates the makeup variant: an @context material naming MakeupBaseMaterial, an @makeup material pointing at *…\{material}.mi, and one small colour .mi per colour; it warns that several styles in one mesh block later colour extension wiki.
  8. Indices are regenerated; hair-colour tags go to the TweakDB flat.
  9. Older saves. A new option that is enabled, not hidden and absent from the loaded state is added to it (appearance and morph options) or activated (switchers) at its default (OnInitAppOption, OnInitSwitcherOption, lines 97–143), so a save made before a CCXL mod was installed shows the new row at its default choice.
  10. Removal. ArchiveXL can remove every custom entry it registered (RemoveCustomEntries) source; whether the game calls this on reload is runtime behaviour hypothesis. What the game draws for a saved choice whose mod is gone is unread; icxrus's eyelash guide tells players to select the vanilla lashes before uninstalling a lash mod, because the lashes otherwise disappear [wiki; community runtime, not ours].

Which categories ArchiveXL itself supports [source: bundle/source/resources/]. Only five categories have a bundled fix, scope and colour patch: eyes (@eyes), lashes (@lashes), brows (@brows), hair (@long, @short, @cap, …) and the masculine beard (@beard). They alone are in the player_customization.app scope, so only their mod options get the dynamic appearance cloning of rule 7. A CCXL option for makeup, piercings, tattoos, scars, cyberware, teeth, nails or the body must define every appearance its definitions name; the mesh side's @context / *{material} expansion still works for any mesh (mod loading §5). The body-tattoo guide's CCXL route is an unnamed switcher with only link: body_tattoo, which the overlay pass adds to both the third- and first-person tattoo switchers, since they share that link [wiki: ccxl_tattoo_mesh_file.png] source.

Entity patches: always-on parts outside the creator [source: ArchiveXL ResourcePatch/Extension.cpp 252–345, 740–857]. A resource.patch whose source is an .ent merges into the target entity template its appearances (replacing same-named ones), resolved dependencies and visual tags, and, when the entity or a part .ent is built, adds the patch entity's components. The wiki's facial-cyberware guide (mana vortex) uses this to put a custom decal on every player entity (third person, photo mode, cutscene, reflection, EP1): always drawn, never a creator choice and absent from the save wiki. On the reference installation three mods patch .ent files, among them a face-rig fix that patches every feminine and masculine player entity resource. The Studio does not apply .ent patches yet (R6b).

XF Studio's single eye-makeup selector (one switcher with Off and complete-look choices in the face group) follows this model; see experiment 005 and the ArchiveXL strategy.

Hair-colour additions

Every CCXL hair-colour pack on the reference installation (six, 150 colours) follows the wiki's hair-profiles template (study):

  • Anonymous slot overlays [source + resource]. The pack's creator resources hold only unnamed appearance options with uiSlot hair_color, eyebrows_color, eyelash_color (and beard_color in the masculine resource), each listing the colours. ArchiveXL merges every mod's named options before any overlay (MergeCustomEntries), so the colours reach all 73 feminine and 78 masculine hairstyle colour options, cyberware twins and CCXL hairstyles included, whatever the load order. Each overlay appends the same list to every matching option, which keeps the hairstyle color link index aligned for styles that start from the 35 vanilla colours. Exact-slot overlays also reach hair_color_none (the bald choice) and feminine eyebrows_color0 (brows Off); what those rows then show is untested.
  • Definition name → mesh appearance source. FixCustomizationAppearance strips a leading NN_ (two digits and an underscore), takes what follows the last __, or else uses the name whole (38_ash_brown → ash_brown; sigh4_mch_blondeice unchanged). A pack's patch.mesh, patched into the player hair, hat-hair, brow, lash and beard scopes, adds that empty appearance with ten @family templates whose paths expand {material} to the pack's .hp and cap gradient (mod loading §5). ArchiveXL's own fix gives 295 vanilla meshes the matching template families.
  • Tags [source + wiki]. For hair_color targets the choice's tags go to TweakDB ItemFactory.HairColors.hairColors; garment dynamic appearances substitute the chosen colour's tag as {hair_color}, so hair under hats needs the tag to equal the mesh appearance name. One installed pack leaves all 21 tags at the template placeholder.
  • Brows, lashes and beard get the same names in their own rows, with no engine link to the hair row. Brows draw the cap gradient's last texel; lashes draw the profile's tip (eyebrows §3, hair reference §9).
  • Rendering never consults the creator list: a saved (app, definition) is cloned from the requested name alone, so a colour whose definition is gone but whose patch appearance and files remain should still render [hypothesis; the study's checklist item 9].

Multi-part hairstyles (Multicolored Hair)

The Multicolored Hair framework (Nexus 21613, 1.1) makes multicoloured hair from geometry: a hairstyle is cut into up to three meshes, each an ordinary CCXL dynamic hair with its own colour row (study).

  • Its own switcher [source + resource]. The vanilla hairstyle switcher's uiSlots are fixed at hair_color and hair_color_fpp, and a merge never changes an existing option's top-level fields (§5 rule 4), so extra colour rows need a new switcher. The framework's mch_hair_switcher (link mch_hairstyle feminine, mch_hairstyle_pma masculine) owns slots mch_hair_part_01…03 and mch_hair_fpp. Its Off choice names four resource-less placeholder options.
  • Hairstyles join by link [source + resource]. An MCH hairstyle ships an anonymous switcher with only link: mch_hairstyle. The overlay pass (§5 rule 5) appends its choice to the framework's switcher. The choice names one named appearance option per part, each a link controller of hairstyle_color_N, plus an FPP shadow option. Switcher choices merge by localizedName, so two MCH hairstyles with the same name replace each other.
  • Parts render like any CCXL hair resource. Each part has its own .app with one template appearance, in the player_wa_hair.app scope, and its own mesh with @context/@long/@cap entries, in the player_wa_hair.mesh scope. Vanilla and pack colours therefore reach every part mesh through the existing patch meshes. A colour appears in a part's creator row only when its pack also overlays mch_hair_part_01…03. A pack that doesn't needs a creator-only patch listing its definitions on those slots, which is what the framework's compatibility patch for redacted-c01's pack is.
  • Consumer resource. Part options sit in hairs and character_customization, so the hairstyle controller draws them beside, and independently of, the vanilla hairstyle. The framework's instructions set the vanilla hairstyle to 51 while MCH is on. The Studio draws every active option in hairs as hair by this consumer rule (no framework slot names), so each part draws with its own colour.

6. Case study: why PRC piercings work in game

eagul's PRC Fully Modular Jewellery Framework predates CCXL and uses no .xl, .inkcharcustomization, TweakXL record, script or ArchiveXL feature [resource: archive listing in the PRC inventory]. It works purely by replacing files that vanilla already reads. Once the Studio interprets archives the way the game does, PRC support is a side effect, not an adapter.

Diagram. Its boxes and arrows are listed here as text; GitHub draws it as a flowchart. View the drawn diagram on GitHub.

Arrows

  1. Vanilla CCO option arrow to i0_000__earring_14.app
  2. i0_000__earring_14.app arrow to Kept vanilla part:
  3. i0_000__earring_14.app arrow to 128 inline components fpm1…fpm128 (mpm for male)
  4. 128 inline components fpm1…fpm128 (mpm for male) arrow to fpmN.morphtarget placeholder (unfilled slot)
  5. 128 inline components fpm1…fpm128 (mpm for male) arrow to fpmN.morphtarget from an item archive (filled slot)
  6. fpmN.morphtarget from an item archive arrow to baseMesh: vanilla earring mesh or linked .mesh
  7. baseMesh: vanilla earring mesh or linked .mesh arrow to chunkMaterials → .mi → multilayered.mt / .mlsetup

Boxes

Vanilla CCO option
female piercings_12 · male piercings_14; resource = i0_000__earring_14.app, 16 colours
i0_000__earring_14.app
same depot path, replaced by the PRC framework archive; (mod archive beats base archive)
Kept vanilla part:
i1_000_pwa_earring__basehead_04.ent; + override, chunk mask …610
128 inline components fpm1…fpm128 (mpm for male)
all with the definition's one meshAppearance
fpmN.morphtarget placeholder
zero render chunks → draws nothing
fpmN.morphtarget from an item archive
same depot path, sorts before PRC_z_999 → wins
baseMesh: vanilla earring mesh or linked .mesh
has appearances silver, gold, … wood
Mermaid source
flowchart TD
    CCO["Vanilla CCO option<br/>female piercings_12 · male piercings_14<br/>resource = i0_000__earring_14.app, 16 colours"] --> APP
    APP["i0_000__earring_14.app<br/>same depot path, replaced by the PRC framework archive<br/>(mod archive beats base archive)"]
    APP --> PART["Kept vanilla part:<br/>i1_000_pwa_earring__basehead_04.ent<br/>+ override, chunk mask …610"]
    APP --> BANK["128 inline components fpm1…fpm128 (mpm for male)<br/>all with the definition's one meshAppearance"]
    BANK -->|unfilled slot| PH["fpmN.morphtarget placeholder<br/>zero render chunks → draws nothing"]
    BANK -->|filled slot| ITEM["fpmN.morphtarget from an item archive<br/>same depot path, sorts before PRC_z_999 → wins"]
    ITEM --> LM["baseMesh: vanilla earring mesh or linked .mesh<br/>has appearances silver, gold, … wood"]
    LM --> MAT["chunkMaterials → .mi → multilayered.mt / .mlsetup"]
StepMechanismGrade
1. OptionVanilla female piercings_12 and male piercings_14 both point at i0_000__earring_14.app with 16 colour definitions. The author's "option 12 for female, 14 for male" is simply this vanilla mapping (the female list skips earring_12/13).resource
2. .app replacementPRC_z_999_Framework_128.archive contains the same depot path base\characters\head\player_base_heads\appearances\head\piercings\i0_000__earring_14.app. A mod archive's copy of a path wins over the base game's.Tool sources (WolvenKit's resolver and ArchiveXL's archive groups put mods first) plus the mod working for users; native engine rule not read [source + community runtime]
3. Kept vanilla geometryEach of the 32 appearances keeps vanilla style 12's partsValues part and override, with chunk mask changed from …612 to …610. Selecting option 12 shows that vanilla piece plus the bank.resource
4. Slot bankEach appearance adds 128 entMorphTargetSkinnedMeshComponents directly to components[] (fpm1…fpm128 female, mpm… male), each referencing eagul\piercingmorphs\female\fpmN.morphtarget. Inline components are part of the appearance just like partsValues components.resource [wiki: .app guide, components/partsValues]
5. Empty slotsThe placeholders are byte-identical morph targets (based on vanilla i1_000_pwa_c__basehead_earring_01.mesh, 21 ear targets) whose embedded geometry has zero render chunks, so an unfilled slot renders nothing.resource (SHA-256 417b1ff3… for fpm1, fpm9, fpm50 placeholders)
6. Filling a slotItem archives such as PRC_f_72_nostril_ring_right_1.archive ship the same depot path …\fpm72.morphtarget with real geometry. They win because PRC_f_… sorts before PRC_z_999_… under the default first-alphabetical-wins archive order.[source: MO2 Cyberpunk guide and plugin] [hypothesis for the native engine order]
7. Shared colourEvery one of the 128 components in a definition uses the same meshAppearance (silver, gold, plastic_black, cooper, …, wood). Each item's base mesh must define mesh appearances with those names; its chunkMaterials then choose per-submesh materials (the nose stud's second chunk is always default__02 → base\eagul\mat_1.mi → silver .mlsetup). Hence one colour choice for all installed items.resource (PRC catalogue audit)
8. Following the faceItem morph targets carry region targets matching the CC morph names (a nose ring the 21 nose targets, a linked-mesh ring all 105), so face sliders move them like vanilla piercings.resource + [hypothesis: manager applies by (target, region)]
9. SaveThe save stores (FNV-1a64 of earring_14.app, i0_000_pwa__earring__NN_colour) in the face group, identical to vanilla option 12. Uninstalling PRC reverts to vanilla style 12 with no broken reference.[resource: save structure] [hypothesis: behaviour after uninstall]

Limits that follow from the design: one colour for every item; slot numbers collide between packs (three septum packs all use fpm17); a pack's slot is only visible if its archive wins; vanilla style 12 cannot be used on its own while PRC is installed.

What the Studio needs from this: nothing PRC-specific. The generic rules in section 8 — archive precedence, inline plus partsValues components, zero-chunk geometry, mesh-appearance lookup on each component's own base mesh, and resolving baseMesh through the same mount set — produce the PRC result, and the implemented resolver does so on the reference installation (validation).

7. How a save stores CC choices

Full format and reader: save import. Summary:

  • Node CharacetrCustomization_Appearances (engine spelling): dataExists, an unknown u32, preset version (12), isMale (body gender), isBrainGenderMale (voice), then head, arms and body group lists, perspectiveInfo and tags [source: WolvenKit CharacterCustomizationAppearancesParser.cs].
  • Each group is {name, appearances[{resourceHash u64, definition, name (option name), censorFlag, censorFlagAction}], morphs[{region, target, censorFlag, censorFlagAction}]}.
  • resourceHash = FNV-1a64 of the lower-case .app depot path. Verified for teeth, lip, cheek, neck and EP1 face-rig entries of the reference save resource.
  • The save stores resolved output, not UI state. It lists, per consumer group, the active appearance options with their chosen definition, and the non-None morphs. It does not store switcher choices, link indices or colour-only controllers (skin_color never appears; its choice is visible only as the definition of skin_type_05, body_color, arms and so on). The same choice is repeated in every group that contains the option: in the reference save, eye colour appears in TPP, TPP_photomode and character_customization.
  • The node mirrors the creator resource [resource: two 2.31 saves against the installed feminine resources]: its head, arms and body group lists are the resource's headGroups, armsGroups and bodyGroups in the same order, empty groups included; its perspective list is the resource's perspectiveInfo; its preset version is the resource's version (12); vanilla options appear within each group in the resource's option order. Censor fields copy the option's own (Censor_Nudity = 1; Activate = 0, Deactivate = 1), so body entries carry non-zero values and both the censored and uncensored body variants are stored. The two tags are the chosen hair colour's and hairstyle's tags (colour name and length). Re-encoding a decoded node reproduces it byte for byte.
  • By contrast, the vanilla UI preset .charcustpreset (gameuiCharacterCustomizationUiPreset) stores the index state: {optionName, isActive, value} for every option, value = 0xFFFFFFFF when unset, plus isMaleVO resource source. WolvenKit also registers .ccstate as a gameuiCharacterCustomizationPreset resource, the same type as the save node [source: FileTypeHelper.cs].

Writing back: what is known

KnownUnknown
WolvenKit has a byte-level writer for this node and a whole-save writer (CyberpunkSaveWriter) source. The layout is simple length-prefixed strings and integers.Whether the game rebuilds the editable CC state (for the mirror/ripperdoc) from the stored appearances, or trusts them as-is. A write that changes only one group could render inconsistently between TPP, photo mode, FPP and the creator.
The Studio reader decodes the node completely with zero trailing bytes; no Studio writer exists.Whether version differences trigger versionUpdateInfo migration on load.
Consistency rules can be derived offline: for a chosen UI state, every group containing an active option must list it with the same definition, and linked followers must use the linked index.How censor flags, Unknown1, isBrainGenderMale and tags (hair colour and length) are validated.
Removing a mod leaves unresolvable hashes; ArchiveXL maps some old paths through app overrides.How the game treats an entry whose app hash or definition no longer exists (drop, default, or corrupt).

The safe order is: generate the complete group set from an option state using the effective CCO, write to a copy, re-read with both the Studio reader and WolvenKit, then verify in game. The container side is now measured: the node is root-level, childless and sits in the last compressed chunk of both saves, and sav.dat has no checksum; the save write-back design sets out a minimal-diff writer, its integrity gates and an alternative that applies the look in game through the bridge. CC presets for sharing should store the UI state (option names + choice identities, like .charcustpreset but by name rather than index) and derive the save representation, not the reverse.

8. Generic resolver specification

Principle: the Studio interprets game files the way the game and its core frameworks do. Vanilla piercings, PRC, CCXL packs, hair-colour packs and future mods must all resolve from data through the same rules; there are no mod-specific adapters. Where a rule is not yet proven, the resolver reports the uncertainty instead of guessing.

Status: phase 1 implemented in projects/xf-studio/authoring/src (character-resolver.ts and the pure modules it uses; host adapter resolver-host.ts; CLI tools/resolve-character.ts). It reproduces the hand-traced eyes, hair, brows, lashes, skin and PRC chains of the reference installation from data alone; see the resolver validation. Archive precedence and ArchiveXL semantics are consolidated in mod loading. The Studio preview consumes it for the skin, face details, brows, lashes, hair, eyes and piercings (the shown V's resolved components, planned by character-detail-plan.ts and exported into the character render record; head CC rendering P0); Build uses its archive layer for the eye plate's head.

Inputs

  1. Installation view: base game archive/pc/content, archive/pc/ep1 (if present), archive/pc/mod, the ArchiveXL bundle directory, plus the mod manager's virtual view (MO2 profile with overwrite, or manual files). Vortex needs no adapter for the files, since it hard-links them into the game folder (Vortex §5); REDmod is a later adapter. Source discovery enumerates the files; resolver-host.ts reads RDAR indexes and reads resources natively first (archive formats), falling back to WolvenKit CLI per resource with an ignored JSON cache (mod loading §6).
  2. Framework declarations: every visible .xl in ArchiveXL's load order (customizations, resource.scope/fix/patch/copy/link). TweakXL records are presentation only (R11) and not read yet.
  3. Character input: a decoded save node (inputFromSave) or a UI option state (descriptorsFromUiState: option → definition, morph or switcher choice). The character context (src/character-context.ts) holds such a state, built from the default V, a save (recovering switcher and link-controller choices the save doesn't store) or an xfs/cc-preset-1 preset.

Rules, in application order

#RuleImplemented fromStatus and grade
R1Archive precedence. MO2 virtual path first (overwrite, then the first modlist.txt row); then the mod group (visible archive/pc/mod/modlist.txt, else first-alphabetical), the ArchiveXL bundle group, EP1, content. Every candidate is kept.Mod loading §1–2Implemented (archive-precedence.ts). Tool-source rules source; the native engine order is unread, so every mod-over-base, base-internal and collation-sensitive decision carries an ambiguity record
R2CCO selection. Body gender picks the male or female CCO; the _ep1 twin when EP1 archives are mounted.Save face-rig hash (section 2)Implemented (ccoPath). resource; confirm with a male EP1 save
R3ArchiveXL resource metadata: scopes (transitive), fixes (paths, mesh names, context), copies and links (rejected over existing paths), patches of meshes, morph targets and .app definitions.ArchiveXL ResourceMeta, ResourceLink, ResourcePatchImplemented (archivexl-config.ts, resource-graph.ts) source. .ent patches (R6b) and !exclude tags not yet
R4CCO merge: fix base option paths (registering app overrides), then per custom resource: groups, unnamed expansion, named options, a second pass for anonymous uiSlot/link overlays; regenerate indices; collect hair-colour tags.ArchiveXL Customization/Extension.cpp 254–637Implemented (cco-model.ts), including the link-wildcard quirk source
R5State → descriptors. The Studio's character context feeds it the whole creator state: the base (the default V, or the state recovered from a save) with the choices a person set on top; from a save, the save's own descriptors are kept for every option the choices neither reach nor change (deriveCharacter). From a UI state: roots are the enabled options that no switcher names. An active switcher activates only the options its current choice names, so a switcher target is active only through a switcher, whatever its enabled flag. Propagate link indices from appearance controllers to followers. Emit (app, definition) and (region, target) per group, skipping an Off choice (a definition whose name is None). From a save: the stored descriptors.Slots and switchers; vanilla UI presets; reference save; a new-game save made to test it [runtime, 25 September]Implemented. Switcher ownership and Off choices resource: the output matches the active appearance options and definitions of all six vanilla UI presets. Link propagation [resource-inferred]; a follower with fewer choices keeps its default and records link-index-out-of-range hypothesis. Activation of a switcher link controller's followers (tattoo under facial_tattoo) is not modelled hypothesis
R6Descriptor → appearance. Apply app overrides; load the winning .app; pick the appearance by name; if absent and in the player_customization.app scope, build ArchiveXL's dynamic appearance.ArchiveXL ApplyAppOverride, FixCustomizationAppearanceImplemented source
R6bEntity patches: a patch .ent's appearances, dependencies, visual tags and components merged into the player entities (always-on parts) and into part .ent files.ArchiveXL ResourcePatch/Extension.cpp 252–345, 740–857; wiki facial cyberware guide; ArchiveXL PlayerBaseScope.xlNot implemented source. Consumers on the reference installation: a face-rig fix on every player entity, a body-type tag mod (§5); ranked gap 3 in the file-chain map
R7Appearance → components. Inline components (compiled package first) ∪ components of each existing partsValues .ent (missing parts dropped, as ArchiveXL does); partsOverrides by component name; in the customization scope, ArchiveXL's first-override mesh appearance on the appearance's own mesh components.wiki .app guide; ArchiveXL FixCustomizationComponents, Garment OnResolveDefinitionImplemented. Same-named inline and part components are treated as one; several same-named components in one appearance are all kept and flagged hypothesis
R8Component → geometry. Morph targets after patches (a copied or patched blob counts), then baseMesh through the same mount set; zero render chunks draw nothing; the chunk mask selects visible chunks.PRC placeholders; head details; PRC previewImplemented (provenance, chunk counts, each chunk's LOD mask, and drawnFrom: the resource whose blob draws, a patch or copy source when ArchiveXL supplies it). The preview exports that resource to GLB from its winning archive resource
R9Morphs. Apply each (target, region) to every morph component whose targets[] contain it.Save + morph resourcesImplemented as a report of which components each pair reaches; manager semantics hypothesis
R10Materials. Mesh appearance (with patches, names fixes and expansion) → chunk material → target entry, patch-mesh entry or @template → instance chain to .mt/.remt, with @context parameters and *{attr} path expansion; effective parameters nearest-first.ArchiveXL Mesh extension; materials and shadersImplemented to the template and resource parameters; shader semantics are not interpreted source
R11Presentation: texts, sections, icons.PresentationImplemented for the game's texts (keyed as ArchiveXL merges them), ArchiveXL text declarations and the compiled TweakDB (src/cc-catalogue.ts, src/game-text.ts, src/cc-presentation.ts) [source + resource], and shown in the Character panel (src/cc-panel.ts). Mod icons declared in TweakXL YAML are overlaid on the compiled TweakDB (src/tweakxl-overlay.ts) source
R12Full body: visual-tag hiding, garment offsets, feet/genital controllers.ArchiveXL VisualTags.xlPartly implemented for worn clothing: .xl tag rules and entity-wide partsOverrides onto the body and arms, and the feet group from footwear (worn clothing §6); garment offsets and garment support deferred

Output: a renderable description

ResolvedCharacter (character-resolver.ts, schema xfs/resolved-character-1) lists one entry per unique saved or derived (option, definition) with the groups that use it, the requested and effective .app (with any override), who provided the choice (base CCO or which custom resource), the appearance status (defined, dynamic with its template, or missing) and its components. Each component carries its origin (inline or part), final mesh appearance and chunk mask, geometry provenance (.morphtarget, .mesh, render-chunk count, visible chunks, drawsNothing, patch sources), morph regions and the saved morphs that reach it, and per visible chunk the material route, instance chain, template and effective parameters. Every resource reference is a Provenance: depot path and hash, winning archive and provider, losing alternatives, the rule and its grade, copy/link hops and the extracted-byte SHA-256. Character-level ambiguities and gaps list everything unproven or unresolved. Reports stay local because they name installed mods.

Everything is data: a vanilla piercing, a PRC slot and a CCXL earring produce the same shape, differing only in provenance.

Retiring the PRC-specific code

Done 26 September 2026. With PRC installed, resolving vanilla piercing option 12 yields the framework's .app, its 128-slot bank (placeholders drawing nothing, filled slots from their item archives by alphabetical order), the kept vanilla part with the framework's chunk mask, and the stud's default__02 → base\eagul\mat_1.mi material (validation). The preview now draws exactly that: piercings are a slot of the character record, selected by the creator slot piercings_color, each resolved drawing component with its own chunk mask, through the layered adapter (head CC rendering). What the old code did and what replaced it:

RemovedReplaced by
tools/intake_prc.ts, the xfs/local-prc-piercings-1 manifestThe resolver's provenance and the generic exporter's content-addressed geometry, keyed by depot hash and winning archive
tools/intake_piercings.ts and xfs/local-vanilla-piercings-2The Piercings row of the Character panel: the creator catalogue's switcher choices and colours on this installation (the record lists no choices since xfs/render-detail-7)
aggregatePrcStyle and prc_active_bank in the retired piercing-preview.tsThe resolved appearance's own component list with its shared meshAppearance
Single-slot prc_fpmNN stylesNothing yet: per-component visibility from the resolved list is a possible later control
PRC notes in the preview panel, prcError/prcAvailable in the statusThe same per-slot outcome and plain line as every other detail

With PRC installed, the "Piercing 12" choice is the PRC bank, as in game; there is no separate PRC style. Trying a style in the viewport asks the host to resolve the V with that creator choice in place of its own.

9. Male and female, NPCs and later full-body work

  • Body gender = entity. The player is player_wa_* or player_ma_* (TPP, FPP, photo mode, cutscene, reflection variants, plus EP1 copies; list in ArchiveXL PlayerBaseScope.xl) source. The save's isMale corresponds to the CCO and entity choice; isBrainGenderMale (the presets' isMaleVO) is independent: the voice, set by the creator's voice-over switcher row and by the body-gender screen (§4, Voice tone) source.
  • Separate option catalogues. Male has beards (a 13-choice beard switcher over 13 per-style part switchers and 57 colour options), 20 instead of 21 face targets per region, no breast morph, a two-choice nipple switcher, no split _tpp/_fpp arms groups, and different body .app choices (t0_000_pma_base__full.mesh, male body is not morph-skinned) resource. Genital choices are available to both.
  • Shared .app files. Most CC .app files contain both genders' appearances; a resolver keys on the definition name, never on the file alone.
  • NPC reuse. Vanilla NPC and photo-mode entities reuse the same head bases (PhotoModeScope.xl lists NPC photo-mode puppets such as Judy and Panam alongside player_wa_photomode) source. h0_000_tattoo.app carries named NPC tattoos (tygerclaw, valentino) and lip styles carry NPC names, showing the same resources dress NPCs resource. Converting a V into an NPC ("NPV") bakes the chosen options into a standalone .app: the NPV guide (manavortex, crediting NoraLee for the process) reads the CC numbers in game, keeps only the used head parts, applies the matching shape keys to every head part in Blender and re-imports them; its app structure screenshot is an illustrated map from CC categories to components (h0_head, he_eyes, ht_teeth, heb_eyebrows, hx_makeup_eyes with black_07, hx_makeup_lips_01 with peach_04_03, i1_earring, tattoo, cyberware, hair, body) [wiki, editor illustration]. The Studio's renderer is effectively a live NPV: the same parts, but morphs applied at runtime instead of baked.
  • Full-body customisation later. The preview draws the vanilla body since 26 September (body rendering). Body coverage in the creator is limited to skin tone, breast size, nipples, genitals, body tattoos/scars, nails and feet. There is no vanilla body-shape morph. Body mods (e.g. replacer bodies) change t0_/a0_/l0_ meshes and rely on garment refitters; they are out of scope for the head-focused Studio but the same chain applies. The wiki's garment-support page gives the component-prefix layering scores (s0 0, l0 10, a0 20, t0 30, h0 40, … hh 90) that decide which garment deforms over which body part wiki.

10. Studio rendering readiness

Source: audit of projects/xf-studio/authoring/src on 2026-09-25, rows updated as later steps landed. Since 26 September the Character panel (src/cc-panel.ts) lists every creator option, so any detail the preview draws can be changed there as well as read from a save. The per-option templates each gap needs and the ranked render plan are in head CC rendering. "Selectable" means a Studio control exists; "Save" means it renders only when an imported save matches.

DetailNeeded to renderPath todayStatusGap
Head mesh + facial morphshead .morphtarget/.mesh, 105 targetsDerived from the user's installed game with all 105 targets and the eye plate cut from the same rows (src/preview-core-*.ts over src/game-asset-export.ts), loaded through one typed render record (src/core-detail-loader.ts); this is the only source of the preview headSave (5 regions) + selectable eye shape (22 choices, listed from the head's eyes targets; the derived eyes follow it)Nose, mouth, jaw and ear shapes selectable in the Character panel; male head absent; vanilla base-game head only, not the effective mod winner
Skin tone / type5 type .apps × 12 tones → skin.mt chain (albedo, packed normal, roughness R/B, microdetail, skin profile)Resolved skin chain of the shown V through the skin adapter (src/skin-material.ts, P1: winning albedo including complexion mods, tint, detail and microdetail, secondary albedo, skin profile) (head CC rendering)Save, default V or any creator choiceScreen-space SSS not ported (a per-channel wrap stands in)
Eye colourhe_ mesh appearance → eye .mi → texturesResolved eye component of the shown V (creator slot eyes_color) through the eyeball and wetness-shell adapters (src/eye-material.ts): colour sampled as the game does, the iris ramp baked from the resolved gradient stops, the refracted iris under the cornea and the Eye-class light on both normals, layered designs through the layered adapter (eye rendering §6.6)Save or default VCreator selector; heterochromia (rank 6)
Eyebrows + colourone heb_ morph/mesh for every style, a texture set per style, decal material + gradient (eyebrows)Resolved component of the shown V through the double-diffuse adapter (src/character-detail-*.ts, src/brow-material.ts)Save or default V + visibilityThe brow's normal and roughness writes; colour accuracy unverified in game
Eyelashes + colourvanilla: he_ chunk 0 + eyelashes__<colour>; CCXL: own meshResolved component with its .hp through the hair.mt adapterSave or default V + visibilityColour accuracy unverified in game
Hair + colourhair .app → mesh(es) + .hp + capResolved components (strands and cap, highest LOD) through the hair.mt and gradient-recolour adapters (src/hair-shading.ts)Save or default V + visibilityPhysics (dangle physics); shadow-only parts are skipped by render mask. Hair is every active option the hairs group lists, whatever its slot, so multi-part hairstyles draw part by part, and a hairstyle's accessories (earrings on metal_base.remt) draw with it
XF Studio eye makeuprecipe → platesrc/engines/layered-makeup/render/makeup-stack.tsSelectable (authoring)Game parity of finishes
Vanilla eye/lip/cheek makeup, pimpleshx_ morphtarget per style + decal .miResolved face-detail components through the decal family (src/face-decal-material.ts; head CC rendering rank 2); CCXL makeup options the same waySave or default VDraw order within a priority; in-game comparison
Facial tattoos, face scars, face cyberwarehx_ morphtargets, chunk masks, tone linkSame path (tone from the saved definition; scars' and cyberware's normal writes included)Save or default VDecal templates other than the three drawn are reported, not drawn
Piercingsi1_ earring morphtargets + chunk masks + multilayered .miResolved components through the layered adapter (src/layered-material.ts); any installed framework by the same rulesSave or default V, with any creator choice set in the Character panel (either body)The colour-mask and contrast mappings (materials open question 10)
Teethht_ morph/mesh; teeth_base.mi (skin.mt) or a finish's multilayered.mtResolved component of the shown V (creator slot teeth) through its template's adapter: skin with the teeth's own profile, or layered (head CC rendering §1)Save or default V, with any creator choice setNo mouth occlusion; not compared with the game
Beard (male)hb_ morphtargets + beard .miThe stubble decal through the face-decal family; the hair cards are not drawnStubbleThe cards (a beard slot on the hair adapter; masculine V plan phase 3)
Body, nails, arms, feet, body tattoos and scarst0_/a0_/l0_/tx_/i0_ resources in the third-person body groupsResolved components of the shown V (the body slot) through the skin, layered and decal adapters, with the body's own shapes and the game's underwear cover (body rendering)Save, default V or any creator choice (either body)Genitals and nipples stay under the cover unless the viewer opts in to the game's uncensored mode (body rendering §3); first-person parts and arm cyberware states aren't drawn
Idle/face rigface rig animgraph + facial animsvanilla CC idle clip (src/idle-animation.ts)Selectable (motion panel), femaleThe masculine idle (his head holds still); wrinkle maps
Male Vpma variants of all the aboveHis own core head (head, eyes on chunk 2, a preview-only plate cut), prepared on first use; every detail above through the same resolver and adapters (masculine V plan)Default V, save or presetHis idle and blink; the beard cards; an audited plate and a male: selector for XF Eye Artistry

Common prerequisites: wiring the implemented generic resolver into the preview (done: the skin, face details, brows, lashes, hair, eyes and piercings go through it and the generic exporter into the character render record); one morph-target mesh path for all hx_ decals (makeup, pimples, tattoos, scars and face cyberware share morph-skinned decal meshes over the head, so one geometry path plus the decal material family covers them; done in rank 2); and the skin shader adapter (done in P1). The derived head.glb reads the base game's content archives only; a resolver-driven head would load the winning h0_000_pwa__morphs.morphtarget, which matters because installed mods (for example a facial-rig fix) can replace it (saved skin chain).

Lessons from the legacy generator

From the legacy xf-omega eye-makeup generator (female only, reference only) and its design notes:

  • It registered one switcher per layer in character_customization and face, with an Off option and colour lists linked through a link string; its variant options were enabled = 0 behind the switcher, mirroring vanilla. Vanilla uses "<option> color" link keys with a space; the legacy code used colour, which still works because the key is only a shared string.
  • It placed a morph-skinned component directly in each .app appearance, instead of vanilla's partsValues .ent plus partsOverrides. The game accepted this (so does PRC, section 6); the vanilla structure is still preferable for ArchiveXL's customization fallback, which operates on partsOverrides.
  • Its option indices collided across layers. ArchiveXL regenerates choice indices but not option indices; keep option indices unique and never rely on them for identity.
  • A 4 layers × 20 designs × 49 colours × 4 finishes matrix (15,680 appearances) made the mod slow to open in WolvenKit (measured structure). XF Studio compiles only authored complete looks.
  • UI icon atlases imported as TEXG_Generic_Color looked washed out in game; they must be TEXG_Generic_UI.

Diagram review

DateToolObservations
2026-09-25Mermaid CLI 11.17.0 (npx, headless Chrome); both diagrams rendered to temporary PNGs at 800 px and 1600 px and inspected at both sizesFirst render of the chain diagram drew switchers as a self-loop whose label collided with the morph label; it was redrawn with separate choice nodes and an explicit inline-components branch. All labels, arrow directions, the dotted morph-manager edge and the PRC filled/unfilled branches are legible at 800 px. Renders contain only documentation text and were not committed.

Open questions

  1. How exactly does link propagation handle a follower whose choice count differs from the controller (e.g. tpp_head_proxy has 16 definitions against 12 tones; the corpo preset stores 0 for it)?
  2. Which consumer applies the TPP, TPP_photomode, FPP, proxy and character_customization groups? The CC controller components cover hairs, face, beards, nails, genitals/breast and feet; the head item is unconfirmed. For photo mode, character_customization alone is not enough for a face-decal option (runtime, 25 September, finish board results); whether face alone is enough is being tested.
  3. On load, does the game rebuild option state from the save's appearance lists (needed for the mirror to show the right selection), and what happens to entries whose hash or definition is missing? The only evidence so far is community advice that a removed lash mod's lashes disappear (§5, rule 10); it matters for every rebuilt XF Eye Artistry collection.
  4. Does entMorphTargetManagerComponent silently skip components without a target, or does it fail? (Affects CCXL accessories lacking a region.)
  5. Is the link-wildcard branch in ArchiveXL's anonymous overlay intentionally tied to the slot suffix?
  6. Why do decal .app overrides name the head component MorphTargetSkinnedMesh7243?
  7. How does the runtime use censorFlag/censorFlagAction? The save copies each option's values (non-zero on the body's nudity-dependent entries, §7), and stores both censored and uncensored variants.
  8. Does ArchiveXL remove and re-merge custom entries on every load, so that uninstalled options disappear cleanly?
  9. What is the native engine rule for a depot path present in both a base-game and a mod archive, and for bundle order? Tool sources (WolvenKit, ArchiveXL) put mods first; PRC depends on it, but the native lookup has not been read or measured.
  10. Are eye-makeup styles 21–36 in the EP1 CCO deliberately on a separate link key, and are all resource-listed choices visible in the UI?
  11. How does the engine treat several components with the same name in one appearance (the reference installation's teeth .app from a morph-additions mod has 16)? The preview draws the first drawable one hypothesis; in that mod's case the other 15 carry LOD mask 0 on their only chunk, so the question does not change what is drawn there resource.
  12. The EP1 CCO and the reference save use i0_000_pwa_base__vagina__01_ca_pale_00_warm_ivory, which no installed i0_000_base__genitals.app defines. What does the engine fall back to?
  13. Does the engine load resources that WolvenKit 8.17.4 and 9.0.1 reject as an older layout (a replacement feet .app with castShadows stored as Bool)?
  14. Does the native GetUnitedOptions sort by option index alone, and what makes an option isEditable? The Studio leaves out hidden options and link followers; holstered_data (feminine arms, no label) is neither and still gets a row.
  15. Which TweakDB blob does the game load with Phantom Liberty (tweakdb_ep1.bin is assumed), and does the text lookup pick maleVariant for a masculine V?

In-game results (session 4)

From the creator on 28 September 2026 (game 2.31, the test profile, feminine V; experiment 029) runtime:

  • Visible choice counts match the resources, not the wiki. With Off counted: eye makeup 37, lip styles 38 per finish, cheeks 25, face cyberware 17, facial tattoos 16, piercings 15, teeth 5. That is the resources' 36/38/24/16/15 plus Off. The wiki cheat sheet's 20/20/14/8/11 is outdated. With the installed packs: eyebrows 70, hairstyles 283 (285 cyberware variants), eye colours 254. Cheek choice 1 is a freckle pattern, not a blush.
  • Link propagation. With skin_color 3, switching skin_type 1 → 5 keeps tone 3, and the body keeps its colour within 3 % (upper chest; the creator's swatch is identical). The creator's framing doesn't show the hands, so they are unchecked.
  • Lip finish tree. makeupLips_type is 0 Off, 1 Default, 2 Glossy, 3 Matte. The style position carries across finishes: style 8 stayed at 8 through Default → Glossy → Matte → Off → Glossy, each finish's own style row taking the position. Colour carry-over is untested: the colour row can't be set from the lips page (the bridge answered that makeupLips_08 "can't be changed on this screen").
  • Linked hairstyle. Hairstyle 5 (label 06); face cyberware 1 switches the row to hairstyle_cyberware at the same index 5 (label 06), and the hair looks the same. Cyberware 2–4 don't switch the row. So the Studio's rule holds for this style. Whether position 5 is one of the 20 where the two lists differ wasn't checked, so the test doesn't yet tell "same options" from "same position" (§2, switcher link members).
  • Hairstyle switcher order. The row's index follows the option list's order, installed packs included: 90 Grace Bob V4, 96 Kala Messy Pixie, 100 Viessa Bun, 217 Valby Curtain Bob, 220 Viv Loose Waves; the reference V's 142 is "LONG PAK - #011" (lm097_hair).
  • Piercing colour order (the same for each style): 0 silver, 1 gold, 2 pearl, 3 copper, 4 red, 5 pink, 6 black, 7 blue, 8 mixed, 9 mixed 2, 10 neon teal, 11 pink metallic, 12 rainbow, 13 rose gold, 14 steel, 15 wood.
  • Teeth choices 0–4: base, silver, gold, copper (labelled "cooper") and pink.

In-game results (sessions 5 and 6)

From the creator opened by the XF bridge on 29 September 2026 (game 2.31, the test profile, feminine V; experiment 032) runtime:

  • The creator opens anywhere through the pause menu, and the game decides the edit context. Opened from the street or a megabuilding corridor, away from any mirror, by switching the pause menu's scenario to the mirror's (Character Customization Anywhere's route, with CCA itself disabled), the creator was fully drawn and editable. Asked for the mirror's HairDresser tag or for NewGame, the menu reported NewGame both times and listed the same 860 editable options of 1,209, skin tone, skin type and face shape included, which HairDresser doesn't allow. So editTags filtering (§2) follows the menu's own mode, and this route always gets the new-game set (runtime access §5).
  • The lip colour row is per finish and style. With the glossy finish and style 8 chosen, the colour row in use is makeupLips_glossy_08 (14 colours), in the slot makeupLips_color; makeupLips_08 isn't in use and can't be set. Setting the slot changed the colour (red → violet). Session 4's refusal came from naming a row not in use (experiment 029 guessed makeupLips_09); the colour carry-over across finishes (ask 2) can now run through the slot.
  • Hairstyle labels resolve to the same indices as session 4: 217 VALBY CURTAIN BOB, 142 LONG PAK - #011.

In-game test asks

Each is a small check for one prepared session; record game, ArchiveXL, TweakXL and Codeware versions and whether Phantom Liberty is installed. Asks 1, 2, 9 and 11 ran in session 4 (above); what remains of them is noted in each.

  1. Link propagation (hands still open). In the creator, pick skin tone 3, then switch skin type 1 → 5 and toggle face tattoo 6. Do skin type, tattoo, neck and hands all keep tone 3? Session 4 confirmed the body; the hands need a framing that shows them.
  2. Lip finish tree (colour still open). Choose lipstick style 8 colour 6, then switch finish regular → glossy → matte → none → glossy. Session 4 showed that the style position carries; set the colour through the slot makeupLips_color (the row in use is the finish's own, e.g. makeupLips_glossy_08; sessions 5 and 6, above) to test whether it carries too.
  3. Save representation. Save once with a vanilla-only look (no CCXL), change only eye makeup colour at a mirror, save again. Hand both saves over for diffing: we expect exactly the face and character_customization entries to change.
  4. Mirror reconstruction. Load a save, open the mirror and confirm every slider shows the saved choice (not index 0). This tests whether option state is rebuilt from the save.
  5. Missing-mod behaviour. With a throwaway save using one CCXL eye colour, disable that mod, load, note what the eyes show, open the mirror, then re-enable and load the original save. (Do not overwrite the original.)
  6. Photo mode. In photo mode, check that makeup, piercings and brows match TPP. This tests the TPP_photomode versus face group split.
  7. PRC as vanilla option 12. With the PRC framework and its two enabled item packs, choose Piercings 12 silver, then gold. Expected: the vanilla style-12 piece plus the nose stud and both nose rings appear together and all change colour together (the stud's small second part stays silver). Then move the nose slider: the rings should follow. This confirms archive precedence, the zero-chunk placeholders and the shared meshAppearance in one look.
  8. EP1 eye-makeup colour carry-over. Choose eye makeup style 5 with colour teal, then switch to style 25. Does teal carry over? The EP1 CCO gives styles 21–36 a different link key, so we expect it may not.
  9. Visible choice counts. Answered in session 4: the creator shows the resources' counts plus Off (above).
  10. Creator rows. On the body page of a new game, list every row in order (feminine and masculine), and note whether any unlabelled row appears. The Studio's catalogue expects the options' index order and no rows for link followers.
  11. Linked hairstyle switchers (one style checked). In the creator, pick hairstyle 5 without face cyberware, then choose a face cyberware that swaps the hairstyle row to the cyberware variant. Is it still the same hair (the Studio expects the style that turns on the same hair option), or the style at the same position? Screenshot both. Session 4 checked style 5 (same hair, same position); repeat with a style at a position where the two lists differ.
  12. Makeup Off on a legacy build. On the reference character (wearing an older XF Eye Artistry build), set each of its layer rows and the vanilla lipstick and blush rows to Off at a mirror. Does nothing of them remain drawn? The Studio's Hide my V's own makeup does exactly this.
  13. Multi-part hair. With the Multicolored Hair Core and an MCH hairstyle, set the vanilla hairstyle to 51 and give two parts different colours. Are both drawn with their own colour, and does the MultiColor Hair Off row show a colour grid? Then put on a beanie and note what hair shows under it (hair colour study checklist items 6, 14 and 15).

Head CC rendering · Mod loading · Resolver validation · Materials and shaders · Wiki file-chain map · CCXL merge boundary · Catalogue probe · Source resolution · Save import · CC controls and presets backlog · Body rendering · Worn clothing · Piercings and jewellery · Archive and resource formats · Vortex

Corrections and credit

This page is generated from knowledge/cc-file-chain.md in the repository. If something here is wrong or missing, please open an issue or suggest an edit; links to sources, captures or tests help most.

It builds on the Cyberpunk 2077 modding community’s work. The inline citations link to the wiki pages, mods, tools and repositories each finding came from, and the credits acknowledge everyone we learned from. Citing a source does not mean its authors endorse these notes.