BuildTable & EntityBuilder System - Kamzik123/AnvilToolkit-Resources GitHub Wiki
This tutorial explains how the BuildTable and EntityBuilder systems work under the hood in Assassin's Creed games — how they assemble game entities from modular, swappable parts using a table-driven architecture.
The BuildTable system is part of the AnvilNext engine and is largely consistent across titles (AC3, AC4, Rogue, Unity, Syndicate, Origins, Odyssey, Valhalla, Mirage, etc.). While specific field names or features may vary slightly between engine revisions, the core concepts — columns, rows, references, handles, tags, and row selection — remain the same.
All examples in this tutorial are taken from real exported AC4: Black Flag game files using AnvilToolkit, but the patterns apply to any Assassin's Creed title using the AnvilNext engine. Most examples use the Schema-Based Exporter format; a dedicated section covers the differences with the Standard Exporter used for pre-Origins games.
A note on
Pathvs ID: Throughout the XML examples you'll seePathattributes on References, Handles, and ObjectPtrs. These paths are not used by the game engine — the engine locates and loads all files exclusively by their numeric ID. ThePathvalues are filled in by AnvilToolkit as a human-readable convenience so you can tell which file is being referenced. When modding, the ID is what matters.
- Core Concept: What is a BuildTable?
- Anatomy of a BuildTable
- References vs Handles — Two Ways to Point at Files
- The RowSelector — Choosing Which Row to Use
- EntityBuilder — BuildTable's Big Brother
- PropertyPath — Targeting Deep Properties
- ApplyPass — When Modifications Happen
- Tags and Weights — Conditional Row Selection
- Standard Exporter vs Schema-Based Exporter
- Practical: How to Add a New File Reference
- Practical: How to Add a New Swappable Row
- Putting It All Together — A Character Build Chain (AC4 Example)
Think of a BuildTable as a spreadsheet that the game engine reads at runtime to assemble an entity (a character, a ship, a sound configuration, etc.) from interchangeable parts.
- Columns define what kind of thing can be swapped (a skeleton, a graphic object, a material, a sound set, etc.)
- Rows define the actual options available for those columns (a character's hidden blades skeleton, a body mesh, a specific material, etc.)
- The engine picks a row (based on tags, weights, or explicit selection) and reads across the columns to get all the parts it needs.
This is what makes the Anvil engine's asset system so modular — you can swap an entire outfit by selecting a different row, without touching the entity template itself.
Here's a simplified, annotated version of a real BuildTable — a character's Hidden Blades table from AC4 (CHR_P_EdwardKenway_HiddenBlades.BuildTable). The structure is identical in other AC games, only the asset paths and IDs change:
<Object ID="41675803805" Type="BuildTable" IsManaged="true">
<!-- COLUMNS: Define the "slots" (what types of data each column holds) -->
<DynamicSmallArray Name="Columns" Type="BaseObject">
<!-- Column 1: expects a Skeleton reference -->
<BaseObject Type="BuildColumn">
<Unsigned32 Name="ColumnID">1</Unsigned32>
<DynamicProperties>
<DynamicProperty Name="Type" BaseType="Reference" ObjectType="Skeleton" />
</DynamicProperties>
</BaseObject>
<!-- Column 2: expects a GraphicObject reference -->
<BaseObject Type="BuildColumn">
<Unsigned32 Name="ColumnID">2</Unsigned32>
<DynamicProperties>
<DynamicProperty Name="Type" BaseType="Reference" ObjectType="GraphicObject" />
</DynamicProperties>
</BaseObject>
</DynamicSmallArray>
<!-- ROWS: The actual data options -->
<DynamicSmallArray Name="Rows" Type="BaseObject">
<!-- Row 0: Standard hidden blades (Weight=1, so this is the default) -->
<BaseObject Type="BuildRow">
<Float Name="Weight">1</Float>
<BaseObject Name="Tag" Type="BuildTag">
<Unsigned32 Name="EngineTagSave">3221187564</Unsigned32>
</BaseObject>
<DynamicProperties>
<!-- Column "1" data: the Skeleton file -->
<DynamicProperty Name="1" BaseType="Reference" ObjectType="Skeleton">
<Reference Type="Skeleton" Path="...\Addon_Human_Weapon_HiddenBlade.Skeleton">
5910554062
</Reference>
</DynamicProperty>
<!-- Column "2" data: the GraphicObject file -->
<DynamicProperty Name="2" BaseType="Reference" ObjectType="GraphicObject">
<Reference Type="GraphicObject" Path="...\CHR_P_EdwardKenway_HiddenBlades.LODSelector">
31080645697
</Reference>
</DynamicProperty>
</DynamicProperties>
</BaseObject>
<!-- Row 1: No hidden blades variant (Weight=0, tag-activated only) -->
<BaseObject Type="BuildRow">
<Float Name="Weight">0</Float>
<BaseObject Name="Tag" Type="BuildTag">
<Unsigned32 Name="EngineTagSave">386527440</Unsigned32>
</BaseObject>
<DynamicProperties /> <!-- Empty: no skeleton, no mesh -->
</BaseObject>
</DynamicSmallArray>
<!-- ROW SELECTOR: Default selection configuration -->
<BaseObject Name="DefaultSelections" Type="RowSelector">
...
</BaseObject>
</Object>Let's break each part down.
A BuildColumn is a slot definition. It doesn't hold data itself — it declares:
-
What type of data goes in this slot (via
DynamicProperties) - A unique ColumnID (a number that rows use to map their data back to this column)
-
When the data gets applied (via the
Passenum) -
Where on the target entity the data gets written (via
TargetProperty/ PropertyPath)
Here's a complete BuildColumn as it appears in the game files:
<BaseObject ID="41675803806" Type="BuildColumn">
<!-- WHEN to apply: AP_AllPasses means this applies during every build pass -->
<Enum Name="Pass" Type="ApplyPass" Value="AP_AllPasses">6</Enum>
<!-- WHERE to write the data on the target entity (empty = root level) -->
<BaseObject Name="TargetProperty" Type="PropertyPath">
<DynamicSmallArray Name="PathNodes" Type="BaseObject" />
<Bool Name="TargetMustBeUnique">True</Bool>
<Bool Name="WholeArray">False</Bool>
</BaseObject>
<!-- Unique ID for this column — rows reference this number -->
<Unsigned32 Name="ColumnID">1</Unsigned32>
<!-- Masking for conditional column application -->
<Unsigned32 Name="ColumnMask">0</Unsigned32>
<Unsigned32 Name="ColumnMaskToReject">0</Unsigned32>
<!-- Back-reference to the parent BuildTable -->
<ObjectPtr Name="Table" PointerType="External"
Path="...\CHR_P_EdwardKenway_HiddenBlades.BuildTable">
41675803805
</ObjectPtr>
<!-- TYPE DECLARATION: This column expects a Skeleton reference -->
<DynamicProperties>
<DynamicProperty Name="Type" ArrayType="Bool"
BaseType="Reference" ObjectType="Skeleton" Flags="00000000">
<Reference Type="Skeleton" PointerType="Null" />
</DynamicProperty>
</DynamicProperties>
</BaseObject>Key fields:
| Field | Purpose |
|---|---|
ColumnID |
Unique number within this table. Rows use this as their DynamicProperty Name to put data into this column. |
Pass |
Controls when during entity construction this column's data is applied (see ApplyPass). |
TargetProperty |
A PropertyPath describing where on the entity this data should be written. Empty PathNodes = root level. |
ColumnMask |
Bitmask for conditional column filtering. 0 = no filtering. |
ColumnMaskToReject |
Bitmask of conditions that should exclude this column. |
DynamicProperties → Type
|
Declares the data type this column accepts (Skeleton, GraphicObject, Material, BuildTable, SoundSet, etc.). |
A BuildRow is one complete set of values for every column in the table. Think of it as a row in a spreadsheet — one option that can be selected.
<BaseObject ID="41675803808" Type="BuildRow">
<!-- Back-reference to parent table -->
<ObjectPtr Name="Table" PointerType="External"
Path="...\CHR_P_EdwardKenway_HiddenBlades.BuildTable">
41675803805
</ObjectPtr>
<!-- Selection weight (1 = default/preferred, 0 = only if tag-matched) -->
<Float Name="Weight">1</Float>
<!-- This row's unique tag identifier -->
<BaseObject Name="Tag" Type="BuildTag">
<Unsigned32 Name="EngineTagSave">3221187564</Unsigned32>
</BaseObject>
<!-- Tags this row is valid for (empty = always valid) -->
<BaseObject Name="PossibleTags" Type="BuildTags">
<DynamicSmallArray Name="Tags" Type="BaseObject" />
</BaseObject>
<!-- THE ACTUAL DATA — one DynamicProperty per column -->
<DynamicProperties>
<!-- Data for ColumnID 1 (Skeleton) -->
<DynamicProperty Name="1" BaseType="Reference" ObjectType="Skeleton">
<Reference Type="Skeleton" PointerType="External" ReferenceType="0"
Path="DataPC\...\Addon_Human_Weapon_HiddenBlade.Skeleton">
5910554062
</Reference>
</DynamicProperty>
<!-- Data for ColumnID 2 (GraphicObject) -->
<DynamicProperty Name="2" BaseType="Reference" ObjectType="GraphicObject">
<Reference Type="GraphicObject" PointerType="External" ReferenceType="0"
Path="DataPC_extra_chr\...\CHR_P_EdwardKenway_HiddenBlades.LODSelector">
31080645697
</Reference>
</DynamicProperty>
</DynamicProperties>
</BaseObject>The critical connection: The DynamicProperty Name attribute in a row matches the ColumnID of a column. This is how the engine knows which data goes into which slot:
Column with ColumnID="1" ←→ Row's DynamicProperty Name="1"
Column with ColumnID="2" ←→ Row's DynamicProperty Name="2"
Visualizing the Hidden Blades BuildTable as an actual table:
| Column 1 (Skeleton) | Column 2 (GraphicObject) | |
|---|---|---|
Row 0 (Weight=1, Tag: 3221187564) |
Addon_Human_Weapon_HiddenBlade.Skeleton |
CHR_P_EdwardKenway_HiddenBlades.LODSelector |
Row 1 (Weight=0, Tag: 386527440) |
(empty) | (empty) |
When the engine builds the character's hidden blades:
- It looks at the table and picks a row (Row 0 by default, since Weight=1)
- It reads across the columns: "Column 1 needs a Skeleton → use
Addon_Human_Weapon_HiddenBlade.Skeleton" - "Column 2 needs a GraphicObject → use
CHR_P_EdwardKenway_HiddenBlades.LODSelector" - Both assets get loaded and attached to the entity
If the engine activates tag 386527440 instead, Row 1 is selected — and since it has no data, the hidden blades are effectively removed. This same pattern is used across all AC games — from toggling weapon visibility to swapping entire outfits.
This is how BuildRows enable swapping: you add a new row with different file references in the same column slots, and the engine seamlessly substitutes one set of assets for another.
The AnvilNext engine has two fundamentally different ways to point at external files. Understanding the distinction is essential for modding any AC game.
A Reference tells the engine to load a file from disk. The engine will find the file, read it, deserialize it, and make it available. It is a loading instruction.
<Reference Type="Skeleton" PointerType="External" ReferenceType="0"
Path="DataPC\Game Bootstrap Settings\Addon_Human_Weapon_HiddenBlade.Skeleton">
5910554062
</Reference>Key characteristics:
-
PointerType="External"— the file is external to this document -
ReferenceType="0"— standard file reference (triggers a file load) - The numeric ID (
5910554062) is what the engine actually uses to find and load the file — IDs are the only thing that matters to the engine - The
Pathattribute (e.g.DataPC\...\Addon_Human_Weapon_HiddenBlade.Skeleton) is not used by the engine at all — it is filled in by AnvilToolkit so that humans can see which file is being referenced. The engine resolves everything purely by ID. - The file does NOT need to already be loaded — the Reference itself causes it to be loaded
References are used inside BuildRow DynamicProperties to point to the actual assets (skeletons, meshes, materials, sound sets, etc.) that make up an entity.
A Handle assumes the file is already loaded (or will be loaded by something else). It is a lookup, not a loading instruction.
<Handle Name="Table" Type="BuildTable" PointerType="Inlined"
Path="DataPC\Game Bootstrap Settings\CHR_Weapon_AddOn.BuildTable">
451808641
</Handle>Key characteristics:
-
PointerType="Inlined"— the engine expects this object to already be in memory - No
ReferenceTypeattribute — it's not triggering a load - Used when something else (a Reference elsewhere, or the game's boot process) has already loaded the target file
- If the target isn't loaded, the game will crash. Handles do not gracefully fail — the engine assumes the asset is already in memory and will hard-crash if it isn't.
- Like References, the engine resolves Handles by ID only — the
Pathattribute is a toolkit convenience for human readability
| Context | Uses Reference | Uses Handle |
|---|---|---|
| BuildRow data (actual assets) | Yes — loads the skeleton, mesh, material, etc. | Rare — only if the asset is already prefetched (see note below) |
| RowSelector → Selections → Table | No |
Yes — the table is already loaded via the Tables array |
| EntityBuilder → Tables array | Yes — loads the sub-tables | No |
| EntityBuilder → Template | Yes — loads the base entity template | No |
| RowSelector → AssociatedEntityBuilder | No | Yes — the EntityBuilder is already the current context |
| MaterialPropertyPathNodeSolver → TargetMaterial | No | Yes — material is expected to already exist |
Note: Handles can appear in BuildRow data in some cases — when the game's prefetching system has already loaded the asset into memory ahead of time. For modding purposes, it's safest to stick with References in BuildRow data unless you're certain the asset will be prefetched.
References can be null — this is safe and commonly used as a type declaration in columns:
<!-- Null Reference (used as type declaration in columns) -->
<Reference Type="Skeleton" PointerType="Null" />Handles can also have their ID set to 0, but only in certain contexts:
<!-- Safe: TargetMaterialTemplate is optional — ID 0 means "no template" -->
<Handle Name="TargetMaterialTemplate" Type="MaterialTemplate" PointerType="Inlined" Path="0">0</Handle>
<!-- Safe: no associated EntityBuilder for this table -->
<Handle Name="AssociatedEntityBuilder" Type="EntityBuilder" PointerType="Inlined" Path="0">0</Handle>These work because the engine only references these handles — it doesn't try to load an asset from them. The TargetMaterialTemplate in a MaterialPropertyPathNodeSolver, for example, is optional: setting it to 0 simply means "don't filter by material template."
Warning: If the engine actually tries to use a Handle with ID
0to load or access an asset — for example, adding a Handle with ID0in a BuildTable's RowSelection — the game will crash. A Handle ID of0is only safe when the field is purely informational or optional. Any Handle that the engine will actively resolve to load or look up an asset must point to a valid, already-loaded ID.
There's also ObjectPtr and Link for internal references:
<!-- ObjectPtr: points to an object, can be External, Inlined, or Null -->
<ObjectPtr Name="Table" Type="" PointerType="External"
Path="...\CHR_P_EdwardKenway_HiddenBlades.BuildTable">
41675803805
</ObjectPtr>
<!-- Link: points to another object within the same file -->
<ObjectPtr PointerType="Link">
<Link Path="8063536875">8063536875</Link>
</ObjectPtr>The RowSelector (found in the DefaultSelections field of every BuildTable) controls which row is active by default — and can also select rows in other tables that this one depends on.
<BaseObject Name="DefaultSelections" Type="RowSelector">
<!-- Tags to match against rows' PossibleTags -->
<BaseObject Name="SelectedTags" Type="BuildTags">
<DynamicSmallArray Name="Tags" Type="BaseObject">
<BaseObject Type="BuildTag">
<Unsigned32 Name="EngineTagSave">3948373731</Unsigned32>
</BaseObject>
</DynamicSmallArray>
</BaseObject>
<!-- Column mask filter (0 = use all columns) -->
<Unsigned32 Name="BuildColumnMask">0</Unsigned32>
<!-- Seed for deterministic random selection when weights are involved -->
<Unsigned32 Name="StableRandomSeed">0</Unsigned32>
<!-- Explicit row selections in OTHER tables -->
<DynamicSmallArray Name="Selections" Type="ObjectPtr">
...RowSelection entries...
</DynamicSmallArray>
<!-- Which EntityBuilder owns this selector -->
<Handle Name="AssociatedEntityBuilder" Type="EntityBuilder" ... />
<!-- Additional tables to consider -->
<DynamicSmallArray Name="AdditionnalTables" Type="Handle" />
</BaseObject>The Selections array is where the RowSelector specifies which row to use in other BuildTables. Each entry is a RowSelection:
<ObjectPtr Type="RowSelection" PointerType="Inlined">
<!-- WHICH table (via Handle — must already be loaded) -->
<Handle Name="Table" Type="BuildTable" PointerType="Inlined"
Path="DataPC\Game Bootstrap Settings\CHR_Weapon_AddOn.BuildTable">
451808641
</Handle>
<!-- WHICH row index in that table (0-based) -->
<Unsigned32 Name="SelectedRow">9</Unsigned32>
<!-- Optional: which tag to use for further sub-selections -->
<BaseObject Name="SelectedTag" Type="BuildTag">
<Unsigned32 Name="EngineTagSave">0</Unsigned32>
</BaseObject>
</ObjectPtr>This says: "In the CHR_Weapon_AddOn.BuildTable, use row index 9."
Real example from an AC4 EntityBuilder (Edward Kenway's Foreigner outfit) — the RowSelector pre-selects rows in 6 different tables:
| RowSelection | Target Table | Selected Row |
|---|---|---|
| 1 | CHR_Weapon_AddOn.BuildTable |
Row 9 |
| 2 | DataAI_CharacterDefinition-Faction.BuildTable |
Row 2 |
| 3 | CHR_Sound_Switch_Dependencies.BuildTable |
Row 1 |
| 4 | CHR_Sounbank_option_ALLBanks.BuildTable |
Row 4 |
| 5 | (internal table 9880647215) |
Row 14 |
| 6 | CHR_Sound_Options_HumanSound_CharacterSelector.BuildTable |
Row 14 |
This is how one EntityBuilder configures an entire character's weapons, faction, sounds, and sound banks all in one place — by picking specific rows in each dependency table.
The engine resolves which row to use through this priority chain:
-
Explicit RowSelection — if the RowSelector has a
Selectionsentry for this table, that row index wins. -
Tag matching — if
SelectedTagscontains tags, the engine looks for rows whosePossibleTagsorTagmatch. A row'sPossibleTagslist defines which tags it's eligible for. -
Weight-based selection — among eligible rows,
Weightdetermines probability.Weight=1is the default.Weight=0means "only if explicitly tag-matched." Multiple rows withWeight > 0get random selection (optionally deterministic viaStableRandomSeed). -
ColumnMask filtering —
BuildColumnMaskcan restrict which columns are actually applied from the selected row.0= apply all columns.
- RowSelector.SelectedTags — "I want rows that match these tags"
- BuildRow.Tag — "I am identified by this tag" (the row's unique identity)
- BuildRow.PossibleTags — "I am valid when any of these tags are active" (eligibility list)
When the engine evaluates: it checks if the RowSelector's SelectedTags intersect with a row's PossibleTags. If they do, that row becomes a candidate.
An EntityBuilder is a specialized BuildTable that adds the ability to:
- Reference a Template entity (the base object to build upon)
- Include an array of sub-Tables (other BuildTables that contribute to the build)
- Override template properties via TemplateOverrides
<Object ID="32235282346" Type="EntityBuilder" IsManaged="true">
<!-- ═══ Inherited from BuildTable ═══ -->
<DynamicSmallArray Name="Columns" Type="BaseObject">
<!-- Column 44: CharacterActionSet -->
<!-- Column 45: Material (with PropertyPath + MaterialPropertyPathNodeSolver) -->
</DynamicSmallArray>
<DynamicSmallArray Name="Rows" Type="BaseObject">
<!-- Row with data for columns 44 and 45 -->
</DynamicSmallArray>
<BaseObject Name="DefaultSelections" Type="RowSelector">
<!-- RowSelections for 6 external tables -->
</BaseObject>
<!-- ═══ EntityBuilder-specific fields ═══ -->
<!-- The base entity template this builder starts from -->
<Reference Name="Template" Type="Entity" PointerType="External" ReferenceType="0"
Path="DataPC_extra_chr\CHR_P_EdwardKenway_Default\CHR_P_BaseEntity_Male.Entity">
420979552
</Reference>
<!-- Template property overrides (usually empty) -->
<DynamicSmallArray Name="TemplateOverrides" Type="BaseObject" />
<!-- Sub-tables that contribute additional build data (loaded via References) -->
<DynamicSmallArray Name="Tables" Type="Reference">
<Reference Type="BuildTable" Path="...\CHR_Inventory.BuildTable">1593930299</Reference>
<Reference Type="BuildTable" Path="...\Player_Faction.BuildTable">5357152689</Reference>
<Reference Type="BuildTable" Path="...\CHR_Sound_.BuildTable">4325172614</Reference>
<Reference Type="BuildTable" Path="...\CHR_Sounbank_option_ALLBanks.BuildTable">5508311500</Reference>
<Reference Type="BuildTable" Path="...\CHR_P_EdwardKenway_Foreigner_VisualMaster.BuildTable">32902696471</Reference>
</DynamicSmallArray>
</Object>EntityBuilders create a hierarchy:
EntityBuilder (CHR_P_EdwardKenway_Foreigner)
├── Template: CHR_P_BaseEntity_Male.Entity
├── Own Columns/Rows: CharacterActionSet, Material
├── Sub-Tables (via Tables array):
│ ├── CHR_Inventory.BuildTable
│ ├── Player_Faction.BuildTable
│ ├── CHR_Sound_.BuildTable
│ ├── CHR_Sounbank_option_ALLBanks.BuildTable
│ └── CHR_P_EdwardKenway_Foreigner_VisualMaster.BuildTable
│ ├── Column 28 → CHR_P_EdwardKenway_Default_Head.BuildTable
│ ├── Column 30 → CHR_P_EdwardKenway_Regular4_Body.BuildTable
│ └── Column 33 → CHR_P_EdwardKenway_HiddenBlades.BuildTable
│ ├── Column 1: Skeleton
│ └── Column 2: GraphicObject
└── RowSelector: Pre-selects rows in Weapon, Faction, Sound tables
Notice how BuildTables can reference other BuildTables in their row data. The VisualMaster table's columns hold References to the Head, Body, and HiddenBlades BuildTables — which in turn hold References to the actual asset files. This creates a tree of tables that the engine walks recursively.
When a BuildColumn needs to modify a specific property deep inside an entity (not at the root level), it uses a PropertyPath with PropertyPathNode entries to navigate the object hierarchy.
From Player_Faction.BuildTable:
<BaseObject Name="TargetProperty" Type="PropertyPath">
<DynamicSmallArray Name="PathNodes" Type="BaseObject">
<!-- Step 1: Navigate into "Components" array, find by class ID -->
<BaseObject Type="PropertyPathNode">
<Unsigned16 Name="Index">65535</Unsigned16> <!-- 65535 = search all -->
<Enum Name="Condition" Value="NC_CLASSID">2</Enum> <!-- Match by class -->
<Hash32 Name="TargetPropertyID" HashValue="Components" />
<Unsigned64 Name="ConditionData">7026055331649904021</Unsigned64> <!-- EntityAI class ID -->
</BaseObject>
<!-- Step 2: Navigate into "EntityAI" object -->
<BaseObject Type="PropertyPathNode">
<Unsigned16 Name="Index">0</Unsigned16>
<Enum Name="Condition" Value="NC_NOCONDITION">0</Enum>
<Hash32 Name="TargetPropertyID" HashValue="Faction" />
</BaseObject>
<!-- Step 3: Target the "Faction" property -->
<BaseObject Type="PropertyPathNode">
<Hash32 Name="TargetPropertyID" HashValue="Faction" />
</BaseObject>
</DynamicSmallArray>
</BaseObject>This navigates: Entity → Components[class==EntityAI] → Faction — and the row's data for this column gets written to that exact property.
| Field | Purpose |
|---|---|
Index |
Which array element to target. 65535 = search/match all elements. 0 = first element. |
Condition |
NC_NOCONDITION (0) = no filter. NC_CLASSID (2) = match by class ID. |
ConditionData |
The class ID to match when Condition = NC_CLASSID. |
TargetPropertyID |
Hash of the property name to navigate into. |
NodeSolver |
Optional solver for complex resolution (e.g., MaterialPropertyPathNodeSolver for finding materials by blend mode or flags). |
Each BuildColumn has a Pass that controls when during entity construction its data is applied:
| Pass | Value | When It Runs |
|---|---|---|
AP_ModificationsBeforeInitialize |
2 | Before the entity is initialized — for data that must exist before init (e.g., sound banks) |
AP_PropertyModifications |
4 | During the property modification phase |
AP_PropertyModifications2 |
5 | A second property modification phase (for changes that depend on earlier modifications) |
AP_AllPasses |
6 | Applied during every pass — safe default for most data |
The pass system ensures that dependencies are resolved in order. For example, a sound bank must be loaded (AP_ModificationsBeforeInitialize) before a sound set can reference it (AP_PropertyModifications2).
-
Weight = 1— This row is the default. If no tags are active and no explicit selection is made, this row wins. -
Weight = 0— This row is never selected randomly. It can only be activated by tag matching or explicit RowSelection. -
Weight = 0.2— (seen in enemy crew sound tables) Each row has a 20% chance of being selected, creating randomized variety.
Tags are 32-bit hash values that identify build variants. They're used throughout the system:
- A BuildRow's
Tagis that row's identity — its unique name in the table. - A BuildRow's
PossibleTagslists which active tags make this row eligible. - A RowSelector's
SelectedTagsspecify which tags the selector is looking for.
Example from AC4's ship system (Ship_HasName.BuildTable) — the same tag-based selection pattern is used for NPC variants, equipment, and other conditional content across all AC games:
Row 0 (Weight=1): Default ship — no specific tags needed
Row 1 (Weight=0): PossibleTags = [tag for "La Dama Negra"] → specific ship BuildTable
Row 2 (Weight=0): PossibleTags = [tag for "HMS Prince"] → specific ship BuildTable
Row 3 (Weight=0): PossibleTags = [tag for "Royal Fortune"] → specific ship BuildTable
...
The default row (Weight=1) handles generic ships. When the engine needs a specific named ship, it activates the corresponding tag, which matches a row's PossibleTags, overriding the default.
AnvilToolkit provides two ways to export BuildTable and EntityBuilder files into XML. The underlying data is identical — only the XML representation differs. Knowing which format you're looking at is important because field names and element structures change between them.
-
Standard Exporter — The default for all pre-Origins games (AC3, AC4, Rogue, Unity, Syndicate). Uses human-friendly element names (e.g.,
<BuildTable>,<BuildColumn>,<BuildRow>,<FileReference>). Exported as UTF-16 withtype="0"in the XML header. -
Schema-Based Exporter — The only option for Origins and later games. Also available for pre-Origins games as an alternative. Uses a direct reflection of the engine's internal schema (e.g.,
<Object Type="BuildTable">,<BaseObject Type="BuildColumn">,<Reference>). Exported as UTF-8 withtype="1"in the XML header.
All examples earlier in this tutorial use the Schema-Based format. Here's how the same data looks in each format:
Schema-Based Exporter:
<BaseObject ID="41675803806" Type="BuildColumn">
<Enum Name="Pass" Type="ApplyPass" Value="AP_AllPasses">6</Enum>
<Unsigned32 Name="ColumnID">1</Unsigned32>
<Unsigned32 Name="ColumnMask">0</Unsigned32>
<Unsigned32 Name="ColumnMaskToReject">0</Unsigned32>
<DynamicProperties>
<DynamicProperty Name="Type" BaseType="Reference" ObjectType="Skeleton">
<Reference Type="Skeleton" PointerType="Null" />
</DynamicProperty>
</DynamicProperties>
</BaseObject>Standard Exporter:
<BuildColumn ID="41675803806" Index="1">
<Value Name="Pass" Type="Enum" EnumName="BuildColumnPass" ValueName="AllPasses">6</Value>
<Value Name="MaskToApplyColumn" Type="Int32">0</Value>
<Value Name="MaskToIgnoreColumn" Type="Int32">0</Value>
<List Name="Components" Type="DynamicProperty">
<DynamicProperty>
<Value Name="DataType" Type="UInt32" HashName="Skeleton">615435132</Value>
<Value Name="Type" Type="UInt32">1835008</Value>
<Reference>
<FileReference Name="Value" IsGlobal="0" Path="0">0</FileReference>
</Reference>
</DynamicProperty>
</List>
</BuildColumn>Schema-Based Exporter:
<BaseObject Type="BuildRow">
<Float Name="Weight">1</Float>
<BaseObject Name="Tag" Type="BuildTag">
<Unsigned32 Name="EngineTagSave">3221187564</Unsigned32>
</BaseObject>
<DynamicProperties>
<DynamicProperty Name="1" BaseType="Reference" ObjectType="Skeleton">
<Reference Type="Skeleton" PointerType="External" ReferenceType="0"
Path="DataPC\...\Addon_Human_Weapon_HiddenBlade.Skeleton">5910554062</Reference>
</DynamicProperty>
</DynamicProperties>
</BaseObject>Standard Exporter:
<BuildRow ID="41675803808">
<Value Name="Weight" Type="Single">1</Value>
<BuildTag Name="Tag" ID="0" Tag="3221187564" TagName="xBFFF6BEC" />
<List Name="Components" Type="DynamicProperty">
<DynamicProperty Index="1">
<Value Name="DataType" Type="UInt32" HashName="Skeleton">615435132</Value>
<Value Name="Type" Type="UInt32">1835008</Value>
<Reference>
<FileReference Name="Value" IsGlobal="0"
Path="DataPC\...\Addon_Human_Weapon_HiddenBlade.Skeleton">5910554062</FileReference>
</Reference>
</DynamicProperty>
</List>
</BuildRow>Schema-Based Exporter:
<Handle Name="AssociatedEntityBuilder" Type="EntityBuilder" PointerType="Inlined"
Path="DataPC_extra_chr\...\CHR_P_EdwardKenway_Foreigner.EntityBuilder">32235282346</Handle>Standard Exporter:
<Handle Name="AssociatedEntityBuilder">
<Value Name="Value" Type="UInt64"
Path="DataPC_extra_chr\...\CHR_P_EdwardKenway_Foreigner.EntityBuilder">32235282346</Value>
</Handle>Schema-Based Exporter:
<ObjectPtr Type="RowSelection" PointerType="Inlined">
<Handle Name="Table" Type="BuildTable" PointerType="Inlined"
Path="DataPC\...\CHR_Weapon_AddOn.BuildTable">451808641</Handle>
<Unsigned32 Name="SelectedRow">9</Unsigned32>
</ObjectPtr>Standard Exporter:
<ObjectPtr>
<RowSelection ID="0">
<Value Name="BuildTableRef" Type="UInt64"
Path="DataPC\...\CHR_Weapon_AddOn.BuildTable">451808641</Value>
<Value Name="SelectedRow" Type="UInt32">9</Value>
</RowSelection>
</ObjectPtr>| Concept | Schema-Based Exporter | Standard Exporter |
|---|---|---|
| Root element | <Object Type="BuildTable"> |
<BuildTable> |
| Root element (EB) | <Object Type="EntityBuilder"> |
<EntityBuilder> |
| Column element | <BaseObject Type="BuildColumn"> |
<BuildColumn> |
| Column ID | <Unsigned32 Name="ColumnID"> |
Index attribute on <BuildColumn>
|
| Column mask | <Unsigned32 Name="ColumnMask"> |
<Value Name="MaskToApplyColumn"> |
| Column mask reject | <Unsigned32 Name="ColumnMaskToReject"> |
<Value Name="MaskToIgnoreColumn"> |
| Columns array | <DynamicSmallArray Name="Columns"> |
<List Name="BuildColumns"> |
| Row element | <BaseObject Type="BuildRow"> |
<BuildRow> |
| Rows array | <DynamicSmallArray Name="Rows"> |
<List Name="BuildRows"> |
| Row/Column data | <DynamicProperties> |
<List Name="Components" Type="DynamicProperty"> |
| Row data column link |
<DynamicProperty Name="1"> (Name = ColumnID) |
<DynamicProperty Index="1"> (Index = ColumnID) |
| Data type in DynProp |
ObjectType="Skeleton" attribute |
<Value Name="DataType" HashName="Skeleton"> |
| File reference | <Reference Type="..." PointerType="External"> |
<FileReference Name="Value" IsGlobal="0"> |
| Handle | <Handle Name="..." Type="..." PointerType="Inlined">ID</Handle> |
<Handle Name="..."><Value Name="Value" Type="UInt64">ID</Value></Handle> |
| Tag | <Unsigned32 Name="EngineTagSave"> |
Tag attribute on <BuildTag> element |
| Tag (with name) | (no tag name shown) |
TagName="xBFFF6BEC" attribute |
| Pass enum | Value="AP_AllPasses" |
ValueName="AllPasses" |
| Pass enum (mods) | Value="AP_PropertyModifications" |
ValueName="PropertyModifications1" |
| PropertyPath | <BaseObject Type="PropertyPath"> |
<PropertyPath> |
| PropertyPathNode | <BaseObject Type="PropertyPathNode"> |
<PropertyPathNode> |
| NodeSolver | <ObjectPtr Name="NodeSolver" Type="MaterialPropertyPathNodeSolver"> |
<MaterialPropertyPathNodeSolver> |
| RowSelection table ref | <Handle Name="Table"> |
<Value Name="BuildTableRef"> |
| Positions array | <DynamicSmallArray Name="Positions"> |
<List Name="EntityPositionSelections"> |
| WholeArray | <Bool Name="WholeArray"> |
<Value Name="SetWholeArray" Type="Bool"> |
| Encoding | UTF-8, type="1"
|
UTF-16, type="0"
|
Which should I use? For pre-Origins games, either format works — AnvilToolkit can import both. The Standard Exporter is more readable at a glance (dedicated element names like
<BuildColumn>and<FileReference>), while the Schema-Based Exporter maps directly to the engine's internal class structure. For Origins and later, only the Schema-Based format is available. The concepts, IDs, and data are identical regardless of format.
To add a new asset reference to an existing BuildTable:
<BaseObject Type="BuildColumn">
<Enum Name="Pass" Type="ApplyPass" Value="AP_AllPasses">6</Enum>
<BaseObject Name="TargetProperty" Type="PropertyPath">
<DynamicSmallArray Name="PathNodes" Type="BaseObject" />
<Bool Name="TargetMustBeUnique">True</Bool>
<Bool Name="WholeArray">False</Bool>
</BaseObject>
<!-- Pick a ColumnID not already used in this table -->
<Unsigned32 Name="ColumnID">3</Unsigned32>
<Unsigned32 Name="ColumnMask">0</Unsigned32>
<Unsigned32 Name="ColumnMaskToReject">0</Unsigned32>
<DynamicSmallArray Name="Positions" Type="BaseObject" />
<!-- Point back to YOUR BuildTable -->
<ObjectPtr Name="Table" PointerType="External" Path="YOUR_TABLE_PATH">YOUR_TABLE_ID</ObjectPtr>
<DynamicProperties>
<!-- Declare the type (e.g., Material) -->
<DynamicProperty Name="Type" ArrayType="Bool" BaseType="Reference"
ObjectType="Material" Flags="00000000">
<Reference Type="Material" PointerType="Null" />
</DynamicProperty>
</DynamicProperties>
</BaseObject>Each row needs a DynamicProperty whose Name matches the new ColumnID:
<!-- Inside the row's DynamicProperties -->
<DynamicProperty Name="3" ArrayType="Bool" BaseType="Reference"
ObjectType="Material" Flags="00000000">
<Reference Type="Material" PointerType="External" ReferenceType="0"
Path="DataPC_extra_chr\YourFolder\YourMaterial.Material">
YOUR_MATERIAL_ID
</Reference>
</DynamicProperty>Remember: Use a Reference (not a Handle) here, because the BuildRow's job is to load the file. The engine reads this Reference and loads the material from disk.
To add a new variant (e.g., a new outfit option):
<BaseObject Type="BuildRow">
<!-- Back-reference to parent table -->
<ObjectPtr Name="Table" PointerType="External" Path="YOUR_TABLE_PATH">YOUR_TABLE_ID</ObjectPtr>
<!-- Weight=0 means tag-activated only. Use Weight=1 to make it the new default -->
<Float Name="Weight">0</Float>
<!-- Unique tag for this row -->
<BaseObject Name="Tag" Type="BuildTag">
<Unsigned32 Name="EngineTagSave">YOUR_UNIQUE_TAG_HASH</Unsigned32>
</BaseObject>
<!-- Which tags activate this row (empty = must be explicitly selected) -->
<BaseObject Name="PossibleTags" Type="BuildTags">
<DynamicSmallArray Name="Tags" Type="BaseObject">
<BaseObject Type="BuildTag">
<Unsigned32 Name="EngineTagSave">YOUR_UNIQUE_TAG_HASH</Unsigned32>
</BaseObject>
</DynamicSmallArray>
</BaseObject>
<DynamicSmallArray Name="Dependencies" Type="ObjectID" />
<!-- One DynamicProperty per column in the table -->
<DynamicProperties>
<DynamicProperty Name="1" ArrayType="Bool" BaseType="Reference"
ObjectType="Skeleton" Flags="00000000">
<Reference Type="Skeleton" PointerType="External" ReferenceType="0"
Path="DataPC\...\YourCustomSkeleton.Skeleton">
YOUR_SKELETON_ID
</Reference>
</DynamicProperty>
<DynamicProperty Name="2" ArrayType="Bool" BaseType="Reference"
ObjectType="GraphicObject" Flags="00000000">
<Reference Type="GraphicObject" PointerType="External" ReferenceType="0"
Path="DataPC_extra_chr\...\YourCustomMesh.LODSelector">
YOUR_MESH_ID
</Reference>
</DynamicProperty>
</DynamicProperties>
</BaseObject>Add the new <BaseObject Type="BuildRow"> element inside the table's <DynamicSmallArray Name="Rows"> block.
If you want a parent EntityBuilder to use your new row by default, add a RowSelection to its DefaultSelections:
<ObjectPtr Type="RowSelection" PointerType="Inlined">
<Handle Name="Table" Type="BuildTable" PointerType="Inlined"
Path="YOUR_TABLE_PATH">YOUR_TABLE_ID</Handle>
<!-- Index of your new row (0-based) -->
<Unsigned32 Name="SelectedRow">2</Unsigned32>
<BaseObject Name="SelectedTag" Type="BuildTag">
<Unsigned32 Name="EngineTagSave">0</Unsigned32>
</BaseObject>
</ObjectPtr>Here's how the engine assembles a character entity from scratch, using Edward Kenway's "Foreigner" outfit from AC4 as a concrete example. The same hierarchical pattern applies in every AC game — only the specific table and asset names differ:
1. Game loads EntityBuilder: CHR_P_EdwardKenway_Foreigner.EntityBuilder
2. EntityBuilder loads its Template:
→ CHR_P_BaseEntity_Male.Entity (the base male character)
3. EntityBuilder loads its sub-Tables (via References — these trigger file loads):
→ CHR_Inventory.BuildTable
→ Player_Faction.BuildTable
→ CHR_Sound_.BuildTable
→ CHR_Sounbank_option_ALLBanks.BuildTable
→ CHR_P_EdwardKenway_Foreigner_VisualMaster.BuildTable
4. The VisualMaster BuildTable's row data references MORE BuildTables:
→ Column 28: CHR_P_EdwardKenway_Default_Head.BuildTable
→ Column 30: CHR_P_EdwardKenway_Regular4_Body.BuildTable
→ Column 33: CHR_P_EdwardKenway_HiddenBlades.BuildTable
5. Each leaf table's row data contains the actual asset References:
→ Head table: head skeleton, head mesh, head materials...
→ Body table: body skeleton, body mesh, body materials...
→ HiddenBlades table: blade skeleton, blade mesh
6. The RowSelector in the EntityBuilder pre-selects rows in shared tables:
→ CHR_Weapon_AddOn: row 9 (specific weapon loadout)
→ Faction: row 2 (player faction)
→ Sound: rows 1, 4, 14 (specific voice/sound config)
7. The engine applies all selected data through the Pass system:
AP_ModificationsBeforeInitialize → sound banks loaded first
AP_PropertyModifications → property overrides applied
AP_PropertyModifications2 → secondary overrides (materials, sounds)
AP_AllPasses → base data (skeletons, meshes, tables)
8. Result: A fully assembled Edward Kenway entity with:
- Base male body from template
- Foreigner outfit body mesh + materials
- Default head
- Hidden blades (or not, depending on tags)
- Correct weapon addon
- Player faction AI
- Character-specific sound sets and sound banks
| Concept | What It Does |
|---|---|
| BuildTable | A table of swappable data. Columns define slots, rows define options. |
| EntityBuilder | A BuildTable that also has a Template entity and sub-Tables. Top of the build hierarchy. |
| BuildColumn | Defines one data slot: its type, when it applies (Pass), and where it writes (PropertyPath). |
| BuildRow | One complete set of values for all columns. Selected by weight, tags, or explicit RowSelection. |
| ColumnID | Links a column definition to row data. Row's DynamicProperty Name must match. |
| Reference | Loads a file from disk. Used in row data for actual assets. |
| Handle | Looks up an already-loaded file. Used in RowSelectors and property solvers. |
| RowSelector | Chooses which rows are active. Can select rows in external tables via RowSelection entries. |
| BuildTag | A 32-bit hash identifying a variant. Tags enable conditional row selection. |
| Weight | Row selection probability. 1 = default, 0 = tag-only, 0.2 = 20% random chance. |
| PropertyPath | Chain of PropertyPathNodes navigating to a deep property on the target entity. |
| ApplyPass | Controls build phase ordering: BeforeInit → Modifications → Modifications2 → AllPasses. |