78,000 words of grounded, citation-backed research across 13 major AC
subsystems, produced by 13 parallel Opus-4.7 high-effort agents. Plus
compact C# port scaffolds for the top-5 systems and a phase-E-through-H
roadmap update sequencing the work.
Research (docs/research/deepdives/):
- 00-master-synthesis.md (navigation hub + dependency graph)
- r01-spell-system.md 5.4K words (fizzle sigmoid, 8 tabs, 0x004A wire)
- r02-combat-system.md 5.9K words (damage formula, crit, body table)
- r03-motion-animation.md 8.2K words (450+ commands, 27 hook types)
- r04-vfx-particles.md 5.8K words (13 ParticleType, PhysicsScript)
- r05-audio-sound.md 5.6K words (DirectSound 8, CPU falloff)
- r06-items-inventory.md 7.4K words (ItemType flags, EquipMask 31 slots)
- r07-character-creation.md 6.3K words (CharGen dat, 13 heritages)
- r08-network-protocol-atlas 9.7K words (63+149+94 opcodes mapped)
- r09-dungeon-portal-space.md 6.3K words (EnvCell, PlayerTeleport flow)
- r10-quest-dialogs.md 7.1K words (emote-script VM, 122 actions)
- r11-allegiance.md 5.4K words (tree + XP passup + 5 channels)
- r12-weather-daynight.md 4.5K words (deterministic client-side)
- r13-dynamic-lighting.md 4.9K words (8-light cap, hard Range cutoff)
Every claim cites a FUN_ address, ACE file path, DatReaderWriter type,
or holtburger/ACViewer reference. The master synthesis ties them into a
dependency graph and phase sequence.
Key architectural finding: of 94 GameEvents in the 0xF7B0 envelope,
ZERO are handled today — that's the largest network-protocol gap and
blocks F.2 (items) + F.5 (panels) + H.1 (chat).
C# scaffolds (src/AcDream.Core/):
- Items/ItemInstance.cs — ItemType/EquipMask enums, ItemInstance,
Container, PropertyBundle, BurdenMath
- Spells/SpellModel.cs — SpellDatEntry, SpellComponentEntry,
SpellCastStateMachine, ActiveBuff,
SpellMath (fizzle sigmoid + mana cost)
- Combat/CombatModel.cs — CombatMode/AttackType/DamageType/BodyPart,
DamageEvent record, CombatMath (hit-chance
sigmoids, power/accuracy mods, damage formula),
ArmorBuild
- Audio/AudioModel.cs — SoundId enum, SoundEntry, WaveData,
IAudioEngine / ISoundCache contracts,
AudioFalloff (inverse-square)
- Vfx/VfxModel.cs — 13 ParticleType integrators, EmitterDesc,
PhysicsScript + hooks, Particle struct,
ParticleEmitter, IParticleSystem contract
All Core-layer data models; platform-backed engines live in AcDream.App.
Compiles clean; 470 tests still pass.
Roadmap (docs/plans/2026-04-11-roadmap.md):
- Phase E — "Feel alive": motion-hooks + audio + VFX
- Phase F — Fight + cast + gear: GameEvent dispatch, inventory,
combat, spell, core panels
- Phase G — World systems: sky/weather, dynamic lighting, dungeons
- Phase H — Social + progression: chat, allegiance, quests, char creation
- Phase J — Long-tail (renumbered from old Phase E)
Quick-lookup table updated with 10+ new rows mapping observations to
new phase letters.
56 KiB
R6 — Items, Inventory Containers, and the Property Model
Status: Deep-dive research; no code written. Audience: acdream client implementation (Phase R6 — item model + paperdoll UI). Sources:
- Decompiled retail
acclient.exe—docs/research/decompiled/chunk_004A0000.c(paperdoll),chunk_00570000.c(WError dispatcher),chunk_006B0000.c(chat/skill parser). references/ACE/Source/ACE.Entity/Enum/—ItemType.cs,EquipMask.cs,ObjectDescriptionFlag.cs,WeenieHeaderFlags.cs,WeenieType.cs,MaterialType.cs,IdentifyResponseFlags.cs,ContainerType.cs,ItemXpStyle.cs.references/ACE/Source/ACE.Entity/Enum/Properties/—PropertyType.cs,PropertyInt.cs(811 lines),PropertyBool.cs,PropertyFloat.cs,PropertyString.cs,PropertyDataId.cs,PropertyInstanceId.cs,PropertyInt64.cs.references/ACE/Source/ACE.Server/WorldObjects/—Container.cs(1013 lines),Player_Inventory.cs(3983 lines).references/ACE/Source/ACE.Server/Physics/Common/EncumbranceSystem.cs— burden math.references/ACE/Source/ACE.Server/Network/GameMessages/GameMessageOpcode.cs,GameAction/GameActionType.cs,GameEvent/GameEventType.cs.references/ACE/Source/ACE.Server/Network/Structure/AppraiseInfo.cs(866 lines) — canonical identify-response serialization.references/ACE/Source/ACE.Server/WorldObjects/WorldObject_Networking.cs—SerializeCreateObject,SerializeModelData,SerializePhysicsData.references/Chorizite.ACProtocol/Chorizite.ACProtocol/Types/PublicWeenieDesc.generated.cs— byte-for-byte wire struct, flag-gated fields.references/Chorizite.ACProtocol/Chorizite.ACProtocol/Messages/C2S/Actions/Inventory_*.generated.cs,Item_Appraise.generated.cs.references/Chorizite.ACProtocol/Chorizite.ACProtocol/Messages/S2C/Events/Item_SetAppraiseInfo.generated.cs,Item_ServerSays{MoveItem,ContainId}.generated.cs,Item_WearItem.generated.cs.references/holtburger/apps/holtburger-cli/src/pages/game/domains/inventory.rs— working client state machine.
The decompiled client is ground truth for what the client stores and displays; ACE is ground truth for what the server sends and expects; Chorizite is the canonical wire-level byte schema.
1. ItemType enum
The retail ItemType is a 32-bit bitfield (not a pure enum) — a single item can have exactly one primary type bit, but server-side matching code ORs multiple bits to create composite categories ("VendorShopKeep", "RedirectableItemEnchantmentTarget", etc.). It is transmitted in PublicWeenieDesc.Type as uint32 and is also stored as PropertyInt.ItemType (id 1).
From ACE.Entity.Enum.ItemType, every bit value the retail client understands:
| Bit | Value | Name | Notes |
|---|---|---|---|
| -- | 0x00000000 |
None | Ephemeral objects (creatures in combat mode) default here |
| 0 | 0x00000001 |
MeleeWeapon | swords, maces, axes, unarmed |
| 1 | 0x00000002 |
Armor | armored clothing (chest, arms, legs, gauntlets with AL) |
| 2 | 0x00000004 |
Clothing | non-armored (robes, shirts, breech, pants, shoes w/ 0 AL) |
| 3 | 0x00000008 |
Jewelry | necklaces, bracelets, rings, trinkets |
| 4 | 0x00000010 |
Creature | NPCs, monsters, player characters |
| 5 | 0x00000020 |
Food | comestibles (chips, eats, drinks) |
| 6 | 0x00000040 |
Money | pyreals (coin stack) |
| 7 | 0x00000080 |
Misc | catchall — quest items, loot tokens, decorations |
| 8 | 0x00000100 |
MissileWeapon | bows, crossbows, atlatls, thrown |
| 9 | 0x00000200 |
Container | packs, chests, sacks (WeenieType.Container) |
| 10 | 0x00000400 |
Useless | salvage bundles, junk |
| 11 | 0x00000800 |
Gem | raw gems, cut gems |
| 12 | 0x00001000 |
SpellComponents | taper, ivory scarab, cobalt jungle, etc |
| 13 | 0x00002000 |
Writable | books, inscribable notes |
| 14 | 0x00004000 |
Key | keys, keyrings |
| 15 | 0x00008000 |
Caster | wands/orbs/staves casting a spell |
| 16 | 0x00010000 |
Portal | portal stones, linked portals |
| 17 | 0x00020000 |
Lockable | lockboxes, locked doors |
| 18 | 0x00040000 |
PromissoryNote | MMD (mugs), trade notes |
| 19 | 0x00080000 |
ManaStone | mana charge stones |
| 20 | 0x00100000 |
Service | trainer services, vendor services |
| 21 | 0x00200000 |
MagicWieldable | wand/orb/staff wieldable subtype |
| 22 | 0x00400000 |
CraftCookingBase | raw cooking ingredient |
| 23 | 0x00800000 |
CraftAlchemyBase | raw alchemy ingredient |
| 25 | 0x02000000 |
CraftFletchingBase | raw fletching material (feathers/shafts) |
| 26 | 0x04000000 |
CraftAlchemyIntermediate | alchemical solvents/elixirs in progress |
| 27 | 0x08000000 |
CraftFletchingIntermediate | half-built arrows |
| 28 | 0x10000000 |
LifeStone | recall lifestones |
| 29 | 0x20000000 |
TinkeringTool | leather kits, oil kits, etc |
| 30 | 0x40000000 |
TinkeringMaterial | refined salvage bundles |
| 31 | 0x80000000 |
Gameboard | chess, chess pieces |
Bits 24 (0x01000000) is reserved (gap in ACE). ItemType in bit PropertyInt.TargetType is also a PublicWeenieDesc.TargetType field in the wire header — gated by WeenieHeaderFlag.TargetType = 0x00080000.
2. EquipMask — paperdoll slots
The paperdoll is the character-window UI with fixed slots; each slot corresponds to a single bit in a 32-bit EquipMask. An item's eligible slots are PropertyInt.ValidLocations (id 9). The slot the item is currently equipped in is PropertyInt.CurrentWieldedLocation (id 10). Both are written to the wire as uint32, gated by WeenieHeaderFlag.ValidLocations = 0x00010000 and CurrentlyWieldedLocation = 0x00020000.
From ACE.Entity.Enum.EquipMask — this data is sent as loc in the player description message F7B0 -0013:
| Bit | Value | Name | Paperdoll slot |
|---|---|---|---|
| 0 | 0x00000001 |
HeadWear | Head (hat / helmet underlay) |
| 1 | 0x00000002 |
ChestWear | Shirt / robe chest layer |
| 2 | 0x00000004 |
AbdomenWear | Breech / robe abdomen |
| 3 | 0x00000008 |
UpperArmWear | Shirt upper-arm |
| 4 | 0x00000010 |
LowerArmWear | Shirt lower-arm |
| 5 | 0x00000020 |
HandWear | Gloves / clothing |
| 6 | 0x00000040 |
UpperLegWear | Pants upper-leg |
| 7 | 0x00000080 |
LowerLegWear | Pants lower-leg |
| 8 | 0x00000100 |
FootWear | Shoes |
| 9 | 0x00000200 |
ChestArmor | Cuirass / plate |
| 10 | 0x00000400 |
AbdomenArmor | Abdomen armor plate |
| 11 | 0x00000800 |
UpperArmArmor | Pauldron |
| 12 | 0x00001000 |
LowerArmArmor | Vambrace |
| 13 | 0x00002000 |
UpperLegArmor | Tasset |
| 14 | 0x00004000 |
LowerLegArmor | Greave |
| 15 | 0x00008000 |
NeckWear | Necklace — paperdoll offset 0x604 |
| 16 | 0x00010000 |
WristWearLeft | Left bracelet — offset 0x608 |
| 17 | 0x00020000 |
WristWearRight | Right bracelet — offset 0x610 |
| 18 | 0x00040000 |
FingerWearLeft | Left ring — offset 0x60c |
| 19 | 0x00080000 |
FingerWearRight | Right ring — offset 0x614 |
| 20 | 0x00100000 |
MeleeWeapon | (item-tag; server matches via Weapon ready slot) |
| 21 | 0x00200000 |
Shield | Shield — offset 0x620 |
| 22 | 0x00400000 |
MissileWeapon | (item-tag) |
| 23 | 0x00800000 |
MissileAmmo | Missile ammo — offset 0x61c |
| 24 | 0x01000000 |
Held | Weapon ready slot — offset 0x618 |
| 25 | 0x02000000 |
TwoHanded | Two-handed weapon flag |
| 26 | 0x04000000 |
TrinketOne | Trinket — offset 0x62c |
| 27 | 0x08000000 |
Cloak | Cloak — offset 0x630 |
| 28 | 0x10000000 |
SigilOne | Blue Aetheria sigil — offset 0x634 |
| 29 | 0x20000000 |
SigilTwo | Yellow Aetheria sigil — offset 0x638 |
| 30 | 0x40000000 |
SigilThree | Red Aetheria sigil — offset 0x63c |
| 31 | 0x80000000 |
Clothing umbrella | or-mask only |
Cross-verification in the decompiled client
From chunk_004A0000.c (function FUN_004a5200, the paperdoll drop-target dispatcher), fixed offsets off param_1 (the paperdoll instance) are the per-slot drop-zone widget handles. The mapping below is exact:
+0x604 NeckWear "Drag necklaces here to wear them"
+0x608 WristWearLeft "Drag bracelets here to wear them"
+0x610 WristWearRight "Drag bracelets here to wear them"
+0x60c FingerWearLeft "Drag rings here to wear them"
+0x614 FingerWearRight "Drag rings here to wear them"
+0x618 Held (weapon) "Drag weapons here to wield them"
+0x61c MissileAmmo "Drag missile ammunition here to wield it"
+0x620 Shield "Drag shields here to wield them"
+0x624, +0x628 Clothing (chest/robes) "Drag clothing items here to wear them"
+0x62c TrinketOne "Drag trinkets here to activate them"
+0x630 Cloak "Drag cloaks here to activate them"
+0x634 SigilOne (Blue) "Drag a Blue Aetheria sigil here to activate it"
+0x638 SigilTwo (Yellow)"Drag a Yellow Aetheria sigil here to activate it"
+0x63c SigilThree (Red) "Drag a Red Aetheria sigil here to activate it"
+0x640 HeadWear "Drag head items here to wear them"
+0x644 ChestWear "Drag chest items here to wear them"
+0x648 AbdomenWear "Drag abdomen items here to wear them"
+0x64c UpperArmWear "Drag upper arm items here to wear them"
+0x650 LowerArmWear "Drag lower arm items here to wear them"
+0x654 HandWear "Drag glove items here to wear them"
+0x658 UpperLegWear "Drag upper leg items here to wear them"
+0x65c LowerLegWear "Drag lower leg items here to wear them"
+0x660 FootWear "Drag foot coverings here to wear them"
When a param_3 == 0 (hover/tooltip, no item dropped yet), the drop-hint text is shown. When param_3 != 0 (an item is hovered over the slot while held), the code flips a boolean bVar1 — for jewelry/clothing slots it's true ("worn" / "take off"), for weapon-family slots it's false ("wielded" / "unwield"). Then it formats "%s (%s)\nDouble-click to %s" where %s is the item name, worn/wielded, and take off/unwield. That's the retail tooltip format — acdream must reproduce it verbatim.
Slot aggregation sets
Multi-bit unions the server uses for validation:
Armor = ChestArmor | AbdomenArmor | UpperArmArmor | LowerArmArmor | UpperLegArmor | LowerLegArmor | FootWear— AL-contributing slots.ArmorExclusive—ArmorminusFootWear(feet are dual-use).Jewelry = NeckWear | WristWearLeft | WristWearRight | FingerWearLeft | FingerWearRight | TrinketOne | Cloak | SigilOne | SigilTwo | SigilThree.Sigil = SigilOne | SigilTwo | SigilThree— the three Aetheria slots.Selectable = MeleeWeapon | Shield | MissileWeapon | Held | TwoHanded— things that go in the ready-slot "weapon" area.ReadySlot = Held | TwoHanded | TrinketOne | Cloak | SigilOne | SigilTwo.
3. Property schema — seven tables
Retail AC represents every object's state as a bundle of typed property tables, keyed by ushort enum ids. This is the entity-component data model of the whole game. There are seven property tables:
| PropertyType | Key type | Value type | ACE enum | Wire tag |
|---|---|---|---|---|
| PropertyInt | ushort | int32 | PropertyInt (0–390 + custom ≥8000) |
0x0001 IntStatsTable |
| PropertyInt64 | ushort | int64 | PropertyInt64 (0–8 + custom) |
0x2000 Int64StatsTable |
| PropertyBool | ushort | uint32 (as bool) | PropertyBool (0–130 + custom) |
0x0002 BoolStatsTable |
| PropertyFloat | ushort | double (8 bytes) | PropertyFloat (0–171 + custom) |
0x0004 FloatStatsTable |
| PropertyString | ushort | WString16L | PropertyString (0–52 + custom) |
0x0008 StringStatsTable |
| PropertyDataId | ushort | uint32 (DID) | PropertyDataId (0–61 + custom) |
0x1000 DidStatsTable |
| PropertyInstanceId | ushort | uint32 (guid) | PropertyInstanceId (0–45 + custom) |
(inline per-property, see §4) |
| PropertyAttribute | ushort | attribute struct | (Strength, Endurance, Coord, Quick, Focus, Self) | sent in PlayerDescription |
| PropertyAttribute2nd | ushort | vital struct | (Health, Stamina, Mana) | sent in PlayerDescription |
| PropertyPosition | ushort | 32-byte position | (Location, Sanctuary, LastOutside, ...) | sent in PlayerDescription |
Property enum ids are stable across all clients (retail 1.0.5.x through ACE) — they are part of the wire contract.
Item-relevant PropertyInt ids (highest-traffic)
1 ItemType // see §1 — bitfield of ItemType
2 CreatureType // for creatures / slayer matches
3 PaletteTemplate // recolor palette id
4 ClothingPriority // CoverageMask — layer order
5 EncumbranceVal // this item's burden units (int)
6 ItemsCapacity // if container: main-pack slots
7 ContainersCapacity // if container: side-pack slots
9 ValidLocations // EquipMask bitfield of legal slots
10 CurrentWieldedLocation // actual EquipMask slot (when equipped)
11 MaxStackSize // hard cap (pyreals 10000, gems 100, arrows 100, etc.)
12 StackSize // current count
13 StackUnitEncumbrance // per-unit burden
14 StackUnitMass // per-unit server-side mass
15 StackUnitValue // per-unit pyreal value
16 ItemUseable // Usable enum — "double-click does what"
17 RareId // rare index, for rare-items display
18 UiEffects // IconHighlight — magic/enchanted overlay
19 Value // total pyreal sale value
20 CoinValue // for Money items: pyreal count (ephemeral on players)
21 TotalExperience // for XP-awarding containers/items
27 ArmorType // cloth/chain/scale/plate/leather
28 ArmorLevel // AL (base)
83 ActivationResponse // Use action behavior
91 MaxStructure // max charges/uses
92 Structure // current charges/uses
93 PhysicsState // PhysicsState flags
94 TargetType // ItemType mask of valid cast targets
95 RadarBlipColor // RadarColor enum
96 EncumbranceCapacity // your cap in burden units
99 PkLevelModifier // PK status mod
105 ItemWorkmanship // 1..10 workmanship
106 ItemSpellcraft // spellcraft (max spell difficulty)
107 ItemCurMana // current mana
108 ItemMaxMana // max mana
109 ItemDifficulty // arcane lore difficulty
110 ItemAllegianceRankLimit // min rank to wield
113 Gender // for creatures; also for heritage-locked items
114 Attuned // AttunedStatus (boon/bonded/untradeable)
115 ItemSkillLevelLimit // min skill value to wield
117 ItemManaCost // mana cost per tick
131 MaterialType // MaterialType enum (see §11)
134 PlayerKillerStatus // PK status
158 WieldRequirements // WieldRequirement enum (skill, level, attribute, etc.)
159 WieldSkillType // Skill enum for requirement check
160 WieldDifficulty // threshold for requirement
166 SlayerCreatureType // slayer damage multiplier target
170 NumItemsInMaterial // salvage bundle count
171 NumTimesTinkered // 0..10 tinkers applied
172 AppraisalLongDescDecoration // long-desc display decoration
174 AppraisalPages // book pages available to read
175 AppraisalMaxPages // max pages the book has
176 AppraisalItemSkill // skill used to appraise
177 GemCount, 178 GemType // set-bonus source gems
179 ImbuedEffect // ImbuedEffectType enum (attack mod, crippling, etc.)
181 ChessRank // chess score (if gameboard)
257 ItemAttributeLimit // required attribute type
258 ItemAttributeLevelLimit // required attribute level
265 EquipmentSetId // armor set id
267 Lifespan, 268 RemainingLifespan
270 WieldRequirements2-4 (271/272, 273/274/275, 276/277/278) — stacked wield gates
279 Unique // unique-cap count
280 SharedCooldown // cooldown-group id
303-306 ImbuedEffect2..5 // additional imbue bits
319 ItemMaxLevel // if item-xp, cap level
320 ItemXpStyle // Fixed / ScalesWithLevel / FixedPlusBase
322 AetheriaBitfield // AetheriaBitfield (which sigils)
Item-relevant PropertyBool
2 Open // container open state
3 Locked // lockable state
22 Inscribable
23 DestroyOnSell
38 IsFrozen // attuned / account-bound
69 IsSellable
85 AppraisalHasAllowedWielder // set when AllowedWielder IID is set
91 Retained // does not drop on death
94 AppraisalHasAllowedActivator
99 Ivoryable // can apply ivory
100 Dyable // can be dyed
108 RareUsesTimer
116 WieldOnUse // equip on first use
130 AutowieldLeft
Item-relevant PropertyFloat
3 HealthRate 22 DamageVariance 63 DamageMod
5 ManaRate 29 WeaponDefense 62 WeaponOffense
6–8 UponResurrection 22 DamageVariance 87 ItemEfficiency
13–19 ArmorMod vs (Slash, Pierce, Bludgeon, Cold, Fire, Acid, Electric)
20 CombatSpeed 21 WeaponLength 22 DamageVariance
64–75 Resist* 76 Translucency 78 Friction 79 Elasticity
92 PowerLevel 93 AccuracyLevel 94 AttackAngle
100 HealkitMod 109 BondWieldedTreasure
116 WildAttackProbability
144 ManaConversionMod 147 CriticalFrequency
149 WeaponMissileDefense 150 WeaponMagicDefense
152 ElementalDamageMod 155 IgnoreArmor (imbue effect %)
157 ResistanceModifier 159 AbsorbMagicDamage
167 CooldownDuration 168 WeaponAuraOffense
Item-relevant PropertyString
1 Name 2 Title 7 Inscription
8 ScribeName 15 ShortDesc 16 LongDesc 17 ActivationTalk
18 UseMessage 22 ActivationFailure
25 CraftsmanName (workmanship signature)
33 Quest 39 TinkerName 40 ImbuerName
42 DisplayName 47 AllegianceName 52 GearPlatingName
Item-relevant PropertyDataId
1 Setup 2 MotionTable 3 SoundTable
6 PaletteBase 7 ClothingBase 8 Icon
22 PhysicsEffectTable 24 UseTargetAnimation
28 Spell (cast on wield) 29 SpellComponent
36 MutateFilter 37 ItemSkillLimit
50 IconOverlay 51 IconOverlaySecondary 52 IconUnderlay
55 ProcSpell (attack-proc spell)
Item-relevant PropertyInstanceId
1 Owner 2 Container 3 Wielder 4 Freezer
17 Creator 18 Victim 22 Bonded 23 Wounder
24 Allegiance 32 HouseOwner 33 House
38 AllowedWielder 39 AssignedTarget 40 LimboSource
31 AllowedActivator
4. CreateObject / UpdateObject packet format
When a new entity enters the client's PVS (or an existing one changes), the server sends one of:
- CreateObject
0xF745— full snapshot. - UpdateObject
0xF7DB— forced full re-send (used when the server catches up after drift). - ForceObjectDescSend
0xF6EA— header-only re-send.
All three use the same three-struct sequence:
uint32 ObjectId (GUID)
ObjectDesc ("model" data) — appearance overlays
PhysicsDesc ("physics" data) — motion/position/tables
PublicWeenieDesc ("weenie") — gameplay properties, flag-gated
ObjectDesc layout (model)
From WorldObject_Networking.cs::SerializeModelData:
byte 0x11 // magic / version tag
byte numSubPalettes
byte numTextureChanges
byte numAnimPartChanges
if numSubPalettes > 0:
packed-DWORD-of-known-type(0x04000000) PaletteID
loop numSubPalettes:
packed-DWORD-of-known-type(0x04000000) SubPaletteId
byte offset
byte length
loop numTextureChanges:
byte partIndex
packed-DWORD-of-known-type(0x05000000) OldTexture
packed-DWORD-of-known-type(0x05000000) NewTexture
loop numAnimPartChanges:
byte index
packed-DWORD-of-known-type(0x01000000) AnimationId
align to 4
"Packed-DWORD-of-known-type" is: if the high nybble equals the expected type (0x04/0x05/0x01/0x06), the writer omits those bits and emits a compact form. ACE does this via WritePackedDwordOfKnownType.
PhysicsDesc layout
Gated by PhysicsDescriptionFlag (uint32 flags at the start). Field order (per ACE SerializePhysicsData):
uint32 physicsDescriptionFlag
uint32 physicsState
if Movement: uint32 dataLength; byte[dataLength] MovementData; uint32 IsAutonomous
else if AnimationFrame: uint32 Placement
if Position: PositionPack (32 bytes: uint32 landCell, 3×float3 origin+rotation+unit-quat)
if MTable: uint32 MotionTableId
if STable: uint32 SoundTableId
if PeTable: uint32 PhysicsTableId
if CSetup: uint32 SetupId
if Parent: uint32 parentGuid; uint32 parentLocation
if Children: uint32 numChildren; loop {uint32 guid; uint32 location;}
if ObjScale: float scale
if Friction: float friction
if Elasticity: float elasticity
if Translucency: float translucency
if Velocity: float3 velocity
if Acceleration: float3 acceleration
if Omega: float3 omega
if DefaultScript: uint32 scriptId
if DefaultScriptIntensity: float intensity
then sequences:
uint16 posSeq, ushort moveSeq, ushort teleSeq, ushort forceSeq,
ushort objectSeq, ushort descSeq,
(possibly instanceSeq).
align to 4
(Full enumeration in PhysicsDescriptionFlag; for items specifically, when an item is in inventory the server mostly sends CSetup | MTable | STable | PeTable | ObjScale without Position/Velocity/Omega since the item isn't in the world.)
PublicWeenieDesc layout — the item game-data
This is the canonical item struct. Verified in Chorizite.ACProtocol/Types/PublicWeenieDesc.generated.cs. Field order, with WeenieHeaderFlag bit to enable each optional slot:
Always:
uint32 Header (WeenieHeaderFlag bitfield)
WString16L Name (length-prefixed UTF-16-LE, padded)
packedDWORD WeenieClassId
packedDWORD Icon (dat id minus 0x06000000)
uint32 Type (ItemType)
uint32 Behavior (ObjectDescriptionFlag)
align(4)
if (Behavior & 0x04000000 IncludesSecondHeader):
uint32 Header2 (WeenieHeaderFlag2 bitfield)
Per-flag (in exact order):
0x00000001 PluralName : WString16L
0x00000002 ItemsCapacity : byte
0x00000004 ContainersCapacity: byte
0x00000100 AmmoType : uint16
0x00000008 Value : uint32
0x00000010 Usable : uint32 (Usable enum)
0x00000020 UseRadius : float32
0x00080000 TargetType : uint32 (ItemType)
0x00000080 Effects : uint32 (IconHighlight / UiEffects)
0x00000200 CombatUse : byte (WieldType)
0x00000400 Structure : uint16
0x00000800 MaxStructure : uint16
0x00001000 StackSize : uint16
0x00002000 MaxStackSize : uint16
0x00004000 ContainerId : uint32 (host container GUID)
0x00008000 WielderId : uint32 (wielder GUID)
0x00010000 ValidLocations : uint32 (EquipMask)
0x00020000 CurrentWieldedLocation: uint32 (EquipMask)
0x00040000 Priority (ClothingPriority): uint32 (CoverageMask)
0x00100000 BlipColor : byte (RadarColor)
0x00800000 RadarEnum : byte (RadarBehavior)
0x08000000 PhysicsScript : uint16
0x01000000 Workmanship : float32
0x00200000 Burden : uint16 (EncumbranceVal truncated)
0x00400000 SpellId : uint16
0x02000000 OwnerId : uint32
0x04000000 Restrictions : RestrictionDB (house perms)
0x20000000 HookItemTypes : uint16 (HookType)
0x00000040 MonarchId : uint32
0x10000000 HookType : uint16
0x40000000 IconOverlay : packedDWORD (minus 0x06000000)
Header2-0x01 IconUnderlay : packedDWORD (minus 0x06000000)
0x80000000 Material : uint32 (MaterialType)
Header2-0x02 CooldownId : uint32
Header2-0x04 CooldownDuration: uint64 (or double, ACE has it as double)
Header2-0x08 PetOwnerId : uint32
align(4)
The absence of a flag means the client uses the dat's weenie-template default (or the value is "unset"). This is why tiny items (a stone, a stick) ship tiny packets — only 2–3 flags set; a max-gear rare ships dozens.
5. Appraise flow
Request (C2S)
Item_Appraise — GameAction 0x00C8 (IdentifyObject):
uint32 ObjectId // the target GUID
That's it. The client sends this when the player right-clicks an item and picks "Assess". Fragment is wrapped in a 0xF7B1 GameAction.
Response (S2C)
GameEvent 0x00C9 IdentifyObjectResponse (inside a 0xF7B0 GameEvent envelope). Structure is Item_SetAppraiseInfo:
uint32 ObjectId
uint32 Flags // IdentifyResponseFlags bitfield
bool Success // one byte, 0/1 — false = "you don't know enough"
// Flag-gated blocks (in this order):
0x0001 IntStatsTable : PackableHashTable<ushort,int32>
0x2000 Int64StatsTable : PackableHashTable<ushort,int64>
0x0002 BoolStatsTable : PackableHashTable<ushort,uint32-bool>
0x0004 FloatStatsTable : PackableHashTable<ushort,double>
0x0008 StringStatsTable : PackableHashTable<ushort,WString16L>
0x1000 DidStatsTable : PackableHashTable<ushort,uint32>
0x0010 SpellBook : PackableList<uint32 spellId>
// top-bit (0x80000000) flags an aura/enchant
0x0080 ArmorProfile : ArmorProfile struct
0x0100 CreatureProfile : CreatureAppraisalProfile struct
0x0020 WeaponProfile : WeaponProfile struct
0x0040 HookProfile : HookAppraisalProfile struct
0x0200 ArmorEnchantmentBitfield: uint16 highlight, uint16 color
0x0800 WeaponEnchantmentBitfield: uint16 highlight, uint16 color
0x0400 ResistEnchantmentBitfield: uint16 highlight, uint16 color
0x4000 ArmorLevels : 9× uint32 per-body-part AL (head, chest, groin, bicep, wrist, hand, thigh, shin, foot)
PackableHashTable format
ushort count
ushort numBuckets // power-of-2, comparer determines
// followed by count entries, sorted by PropertyIntComparer (bucketed hash order,
// not numeric order). Within a bucket, entries are in insertion order.
loop count:
uint32 key // upcast from ushort property enum
value // type depends on the table
ACE writes:
PropertyIntComparer(numBuckets=16),PropertyInt64Comparer(8),PropertyBoolComparer(8),PropertyFloatComparer(8),PropertyStringComparer(8),PropertyDataIdComparer(8). These must match exactly or the retail client displays garbage.
Assessment success logic
The server's Player_Assess.cs (not open here, but evidenced by PropertyBool.AppraisalHasAllowedWielder and PropertyInt.AppraisalItemSkill) uses the Assessment skill (id 0x10, found in the chat/skill dispatcher at chunk_006B0000.c:1245). Skill check formula follows SkillCheck.GetSkillChance(playerSkill, itemDifficulty) — a standard AC S-curve where 50% chance occurs at skill == difficulty. Ten-hit retry timer (AppraisalHeartbeatDueTimestamp, AppraisalRequestedTimestamp) prevents spam.
Items with ItemDifficulty = 0 or NPCs always succeed (from AppraiseInfo.BuildProperties — Success = true default). Failure still sends a IdentifyObjectResponse but with Success = false and an empty table set (most properties stripped).
6. Burden and bulk (encumbrance)
Capacity formula
From ACE.Server/Physics/Common/EncumbranceSystem.cs (canonical, matches decompiled retail):
int EncumbranceCapacity(int strength, int numAugs)
{
if (strength <= 0) return 0;
int bonusBurden = 30 * numAugs; // AugmentationIncreasedCarryingCapacity: up to 5
if (bonusBurden >= 0) {
if (bonusBurden > 150) bonusBurden = 150;
return 150 * strength + strength * bonusBurden;
}
return 150 * strength;
}
A level-1 character with Strength = 10 has capacity 150 × 10 = 1500 burden units. A level-275 character with Strength = 400 and 5 augs has 150 × 400 + 400 × 150 = 60000 + 60000 = 120000 units.
Carry limit
Player_Inventory.cs::HasEnoughBurdenToAddToInventory:
return (EncumbranceVal + newItemBurden) <= (GetEncumbranceCapacity() * 3);
The hard carry-cap is 3 × capacity. At 2× capacity you're "over-encumbered" and your movement-speed mod (GetBurdenMod) drops to 0; between 1× and 2× it linearly interpolates 2 - burden in multiplier; at <1× it's 1.0.
float GetBurdenMod(float burden /* = encumbrance / capacity */)
{
if (burden < 1.0f) return 1.0f;
if (burden < 2.0f) return 2.0f - burden;
return 0.0f;
}
So the three breakpoints are:
| Carried | UI message | Movement mod |
|---|---|---|
| 0–100 % | normal | 1.0 |
| 100–200 % | "burdened" | 1.0 → 0.0 linearly |
| 200–300 % | "over-burdened" | 0.0 (can't run) |
| > 300 % | cannot pick up (error 0x2A — "You are too encumbered to carry that!") |
The WError codes 0x2A and 0x2B from chunk_00570000.c:1853–1858 are these hit.
Bulk (slots)
Unlike burden, bulk is item count per container — not mass-proportional. Main backpack: ItemsCapacity property (max 102 for the Player — Player_Inventory doesn't check it past base 102). Side packs: ContainersCapacity property (base 7 slots) — each slot holds one side pack (regular pack, foci, or pouch).
Side pack capacity: ItemsCapacity on the pack weenie itself — retail packs are 24 items, pouches typically 12, foci are ContainerType.Foci and are slot-exclusive with no sub-items. Container.cs::GetFreeInventorySlots(includeSidePacks=true) sums main + all side-pack free slots.
7. Container hierarchy
Depth model
Retail AC permits exactly two levels:
Player (primary Container)
├── Main pack items (ItemsCapacity slots, 102 total in a maxed player)
├── Side pack 1 (Container) // uses a ContainerCapacity slot
│ └── items (ItemsCapacity slots of the sub-pack)
├── Side pack 2 (Container)
│ └── items
├── Foci (ContainerType.Foci) // ContainerCapacity slot, no inventory
└── ...
A side pack may not contain another side pack. This is enforced both server-side (in Container.AddToInventory) and by the client paperdoll drag handler. From Container.cs:
private int CountContainers() =>
Inventory.Values.Count(wo => wo.UseBackpackSlot); // UseBackpackSlot = (WeenieType.Container || IsFoci)
UseBackpackSlot is true for both sub-containers and foci. Nested containers route to side-pack slots; anything else goes to main.
Encoding
Each item has:
PropertyInstanceId.Container = 2— its direct parent container's GUID.PropertyInt.PlacementPosition = 53— 0-based slot index within that container (sorted byOrderBy(wo => wo.PlacementPosition)).
So a side pack's contents are enumerated by walking Inventory[] for items whose Container == sidePack.Guid, ordered by PlacementPosition.
The wire PublicWeenieDesc.ContainerId field (WeenieHeaderFlag 0x00004000) directly carries this. On CreateObject, if an item is in a pack, it's placed inside PublicWeenieDesc with ContainerId = pack.Guid; on WieldObject, WielderId is set and ContainerId is cleared.
InventoryPutObjInContainer event (GameEvent 0x0022)
S2C event structure (GameEventItemServerSaysContainId):
uint32 itemGuid
uint32 containerGuid
uint32 placementPosition
uint32 ContainerType // 0=NonContainer, 1=Container, 2=Foci (from ACE.Entity.Enum.ContainerType)
This fires whenever the server slots an item into a container — either player-initiated (picked up, moved) or server-initiated (loot spawned in corpse).
8. Stacking rules
Which items stack
An item is stackable iff MaxStackSize > 1. In practice: pyreals (10000 cap), gems (100), arrows (100), scrolls (1 — single-use), spell components (1000 for taper-family, varies), salvage bundles (100 pieces). WeenieType.Coin and WeenieType.Stackable and WeenieType.SpellComponent are the main "stacks by default" categories.
Merge rules (from ACE's DoHandleActionStackableMerge)
- Both source + target must have the same
WeenieClassId. - Both must be unenchanted (
!IsEnchanted). WError0x428— "You cannot merge enchanted items!". - The source must belong to the player OR both must belong to the player (WError
0x429— "You must control at least one stack!"). - Target
StackSize + amount <= MaxStackSize, else an overflow stack remains as the source.
Split actions (C2S)
StackableMerge 0x0054:ObjectId (source) + TargetId (target) + Amount.StackableSplitToContainer 0x0055:ObjectId + ContainerId + SlotIndex + Amount— makes a new stack ofAmountinside a container.StackableSplitTo3D 0x0056:ObjectId + Amount— drops a split stack to the ground.StackableSplitToWield 0x019B:ObjectId + SlotIndex (EquipMask) + Amount— equip a split (e.g. arrows directly to missile ammo).
Item_UpdateStackSize S2C (opcode 0x0197)
byte Sequence
uint32 ObjectId
uint32 Amount // new stack size
uint32 NewValue // new total Value (pyreals)
Fired when a stack changes size without changing GUID (e.g. merging adds to target). If the source is entirely consumed, a ObjectDelete 0xF747 follows.
9. Equipment and wield
Equip request (C2S GetAndWieldItem 0x001A)
uint32 ObjectId // item GUID
uint32 Slot // EquipMask — which slot we want
The server validates in order:
- Item belongs to the player or is in an accessible container (WError
0x28— "The item is under someone else's control!"). - Item is
MagicWieldable,Weapon,Armor, etc — matches itsValidLocations. Slotis one of the bits inValidLocations.- Level / skill / attribute requirements via
PropertyInt.WieldRequirements..4:WieldRequirement.Level— needsPlayer.Level >= WieldDifficulty.WieldRequirement.Attrib/RawAttrib— needsAttribute.Current >= WieldDifficulty.WieldRequirement.Skill/RawSkill—Skill.Current >= Difficultyfor theWieldSkillType.WieldRequirement.HeritageType— species lock.
- Activation:
ItemAllegianceRankLimit,ItemSkillLevelLimit,Attuned,Gender, etc. - For PK-bound items:
PropertyInstanceId.AllowedWieldermust equal player GUID. - Carrying-capacity check if it came from outside the player's inventory.
On success, the server sends:
GameEventWieldItem 0x0023— instructs the paperdoll to display the item inSlot:uint32 itemGuid uint32 slot (EquipMask)- Follow-up:
UpdateObject 0xF7DBwithWielderId = player.Guid,CurrentWieldedLocation = Slot,ContainerId = 0.
Un-equip
Client sends a PutItemInContainer 0x0019 targeting the player's own GUID as the container. Server revokes wielded status and slots the item back into main pack. Visual equipment on other players is driven by the ObjDescEvent 0xF625 (appearance re-send) after the wield/un-wield.
10. Currency
Retail AC has three economic currencies:
- Pyreals — weenie
273,WeenieType.Coin,ItemType.Money. Max stack 10000 per stack (capped explicitly in starter gear and loot code). A character can carry multiple 10000-pyreal stacks;PropertyInt.CoinValuesums them across main pack + side packs (ephemeral, recalculated on inventory change viaUpdateCoinValue). - Luminance —
PropertyInt64.AvailableLuminance(id 6) /PropertyInt64.MaximumLuminance(id 7). Not an item — it's a player-level int64 tracked as a property. Spent on LuminanceAug vendor services. - Society tokens / allegiance XP — Society rank is
PropertyInt.SocietyRankCelhan / Eldweb / Radblo(ids 287–289), but the currency itself is a physical item (e.g. tokens — weenie 33613 "Pathwarden Token" in the starter gear).TotalExperience(int64) is also an XP currency for allegiance redistribution.
Coin overflow
When a stack would exceed MaxStackSize = 10000, the server creates a second stack and places it in the next free slot (DoHandleActionStackableMerge creates overflow). Players end up with Pyreal (10000x) stacks + a partial one.
11. Salvage + tinker state
Workmanship & crafting state
PropertyInt.ItemWorkmanship = 105 — the 1-to-10 workmanship score. Stored as uint32 on the item.
PropertyFloat.Workmanship in the PublicWeenieDesc (flag 0x01000000) is a float32 — workmanship is shipped twice: the quantized int on the item, and a fractional float on the wire for salvage-bundle averaging.
Tinker slots
PropertyInt.NumTimesTinkered = 171 — 0..10. Each tinker application also adds a string to PropertyString.TinkerLog = 9007 (or per tinker type).
PropertyInt.TinkerLog (str 9007) is a semicolon-separated log: "Horns;Leather;Pine" — displayed in the long description on appraise.
PropertyString.TinkerName = 39 and PropertyString.ImbuerName = 40 — the character names who tinkered/imbued.
Imbue state
PropertyInt.ImbuedEffect = 179 is an ImbuedEffectType enum: attack-skill raising, critical-freq raising, slash/pierce/blunt/fire/cold/acid/electric damage raising, armor rending, crushing blow, biting strike, hematite (lifesteal), gurog (mana-drain). Additional slots ImbuedEffect2..5 = 303..306 can stack multiple imbues.
ImbueStackingBits = 311 — bitfield for tinker-stacking augmentations (Class A, B, C, Corrupted Amber, Purchased Armor).
PropertyInt.ImbueAttempts = 205 / ImbueSuccesses = 206 — counters.
Salvage-bundle fields
PropertyInt.NumItemsInMaterial = 170 — count of items merged into this bundle.
PropertyInt.MaterialType = 131 — see MaterialType enum (§12). Bundles all share the same material; mixing materials makes separate bundles.
PropertyInt.Structure / MaxStructure = 91/92 — per-unit charge OR bundle count (reused). For a Salvage bundle, Structure = 1..100 pieces.
Usage tracking
PropertyInt.Structure = 92 / MaxStructure = 91 track uses remaining for healing kits, ivory kits, lockpicks, tinker tools. PropertyFloat.UseLockTimestamp = 99 prevents spam-use.
12. Active enchantments on items
When an item is enchanted (via Item Enchantment spells like Strength Self III, Blood Drinker VI, Infected Caress), the enchantment is stored on the item's EnchantmentManager (server-side) and sent to the client through MagicUpdateEnchantment 0x02C2 events whenever the item is visible.
On Item_SetAppraiseInfo, the SpellBook field (flag 0x0010) contains two concatenated lists:
- Innate spells — from the item's weenie (fire-and-forget cantrips). Sent as raw
SpellId(uint32). - Active auras/enchantments — sent with top bit set:
spellId | 0x80000000. The client's appraise window renders these with a different color/icon (usually yellow "aura" marker).
AppraiseInfo.cs builds this list from wo.Biota.GetEnchantments() plus, for weapons, the wielder's EnchantmentManager.GetEnchantments(MagicSchool.ItemEnchantment) filtered by SpellCategory (e.g. AttackModRaising, DefenseModRaising, DamageRaising). This lets a wielded weapon show "you see this weapon as if it had Heart Thirst VI on it" (aura) even though the spell is actually on the wielder.
The retail client also decorates the icon with a blue glow when there are any active auras — driven by PropertyInt.UiEffects = 18 (IconHighlight enum: Magical, Heroic, Diabolical, Elemental). UiEffects is written in the PublicWeenieDesc flag 0x00000080 (Effects). The value transitions visually to the "magic item" icon overlay.
13. Icon, icon-overlay, and icon-underlay
Three DID fields compose the displayed item icon (all ResourceIds with type prefix 0x06000000):
| PropertyDataId | Wire field | Purpose |
|---|---|---|
8 Icon |
PublicWeenieDesc.Icon (required) |
base image — rendered bottom-most |
50 IconOverlay |
WeenieHeaderFlag 0x40000000 |
top decoration — used for "magic item" sparkles |
52 IconUnderlay |
WeenieHeaderFlag2 0x00000001 |
bottom decoration — used for rare-item foil, socketed marker |
51 IconOverlaySecondary |
(not in wire; server-only) | alternate overlay for dye/tier |
Overlays in the dat are 32×32 RGBA icons with transparency where the base icon shows through. The client composites in this order: underlay → icon → overlay → text stack-count (rendered by UI). For Aetheria sigils the base icon is the slot (blue/yellow/red gem), the overlay marks activated state.
UiEffects (PropertyInt 18) is additive to the overlay — it's a client-side post-process glow, not a dat asset. Values are IconHighlight.Magical / Heroic / Diabolical / ... which the retail client renders by tinting the whole icon with a colored additive sprite.
14. Retail wire messages — summary
Client → Server (inside 0xF7B1 GameAction envelope, C2SMessageType ordinal)
| GameActionType | Opcode | Message | Payload |
|---|---|---|---|
| PutItemInContainer | 0x0019 |
Inventory_PutItemInContainer | uint32 objectId; uint32 containerId; uint32 slotIndex |
| GetAndWieldItem | 0x001A |
Inventory_GetAndWieldItem | uint32 objectId; uint32 slot (EquipMask) |
| DropItem | 0x001B |
Inventory_DropItem | uint32 objectId |
| StackableMerge | 0x0054 |
Inventory_StackableMerge | uint32 objectId; uint32 targetId; uint32 amount |
| StackableSplitToContainer | 0x0055 |
Inventory_StackableSplitToContainer | uint32 objectId; uint32 containerId; uint32 slotIndex; uint32 amount |
| StackableSplitTo3D | 0x0056 |
Inventory_StackableSplitTo3D | uint32 objectId; uint32 amount |
| StackableSplitToWield | 0x019B |
(analog) | uint32 objectId; uint32 slot; uint32 amount |
| GiveObjectRequest | 0x00CD |
Inventory_GiveObjectRequest | uint32 targetId; uint32 objectId; uint32 amount |
| IdentifyObject (Appraise) | 0x00C8 |
Item_Appraise | uint32 objectId |
| Use | 0x0036 |
Inventory_UseEvent | uint32 objectId |
| UseWithTarget | 0x0035 |
Inventory_UseWithTargetEvent | uint32 toolId; uint32 targetId |
| NoLongerViewingContents | 0x0195 |
Inventory_NoLongerViewingContents | uint32 containerId |
Server → Client
| GameEvent | Opcode | Message |
|---|---|---|
ObjectCreate (Item_CreateObject) |
0xF745 |
uint32 guid; ObjectDesc; PhysicsDesc; PublicWeenieDesc |
UpdateObject (Item_UpdateObject) |
0xF7DB |
uint32 guid; ObjectDesc; PhysicsDesc; PublicWeenieDesc |
ObjectDelete |
0xF747 |
uint32 guid; uint16 instanceSequence |
ObjDescEvent |
0xF625 |
uint32 guid; ObjectDesc (model-only resync) |
ForceObjectDescSend |
0xF6EA |
uint32 guid (heartbeat / trigger full re-send) |
SetStackSize (Item_UpdateStackSize) |
0x0197 |
byte seq; uint32 guid; uint32 amount; uint32 newValue |
InventoryRemoveObject |
0x0024 |
uint32 guid |
PickupEvent (Inventory_PickupEvent) |
0xF74A |
uint32 guid; uint16 instSeq; uint16 posSeq |
GameEvent envelope 0xF7B0, inner event type: |
||
InventoryPutObjInContainer |
0x0022 |
uint32 itemId; uint32 containerId; uint32 slot; uint32 ContainerType |
WieldObject |
0x0023 |
uint32 itemId; uint32 slot (EquipMask) |
IdentifyObjectResponse (Item_SetAppraiseInfo) |
0x00C9 |
see §5 |
ViewContents |
0x0196 |
uint32 containerId; PackableList<ContainedObject> (guid+weenieClassId+slot) |
ItemAppraiseDone |
0x01CB |
uint32 objectId; uint32 successOrFailure |
CloseGroundContainer |
0x0052 |
uint32 containerId |
InventoryServerSaveFailed |
0x00A0 |
uint32 itemId; uint32 weenieError (used to roll back failed client-side moves) |
WeenieError codes (client displays — from chunk_00570000.c)
Item-relevant subset:
0x29 "You cannot pick that up!"
0x2A "You are too encumbered to carry that!"
0x2B "<x> cannot carry anymore."
0x20 "You must control both objects!"
0x23,0x37-0x39 "Unable to move to object!"
0x26 "That is not a valid command."
0x28 "The item is under someone else's control!"
0x3EE "The container is closed!"
0x427 "You cannot merge different stacks!"
0x428 "You cannot merge enchanted items!"
0x429 "You must control at least one stack!"
0x3EF "<name> is not accepting gifts right now."
0x46A "<name> doesn't know what to do with that."
15. Port plan — acdream C# classes
Core model
namespace Acdream.Items;
// An item's definition (read-only, shared)
public sealed class ItemTemplate
{
public uint WeenieClassId;
public WeenieType WeenieType;
public string Name;
public string PluralName;
public uint IconDid; // 0x06xxxxxx — rendered via R4
public uint IconOverlayDid;
public uint IconUnderlayDid;
public ItemType Type; // bitfield
public EquipMask ValidLocations;
public ushort MaxStackSize;
public ushort BaseBurden;
public uint BaseValue;
public PropertyBundle DefaultProperties; // the dat-derived defaults
}
// A live instance — the thing the server owns
public sealed class ItemInstance
{
public uint Guid; // unique per session
public ItemTemplate Template;
public PropertyBundle Properties; // overrides + runtime state
public uint? ContainerGuid; // PropertyInstanceId.Container
public uint? WielderGuid; // PropertyInstanceId.Wielder
public EquipMask CurrentSlot; // PropertyInt.CurrentWieldedLocation
public ushort StackSize; // cache of PropertyInt.StackSize
public int PlacementPosition;
public int EncumbranceVal =>
Properties.GetInt(PropertyInt.EncumbranceVal) ??
Template.BaseBurden * Math.Max(1, StackSize);
public int Value => Properties.GetInt(PropertyInt.Value) ?? (int)Template.BaseValue;
public bool IsStackable => Template.MaxStackSize > 1;
public bool IsContainer => Template.WeenieType == WeenieType.Container;
}
PropertyBundle — the 7-table dictionary
public sealed class PropertyBundle
{
public Dictionary<PropertyInt, int> Ints = new();
public Dictionary<PropertyInt64, long> Int64s = new();
public Dictionary<PropertyBool, bool> Bools = new();
public Dictionary<PropertyFloat, double> Floats = new();
public Dictionary<PropertyString, string> Strings = new();
public Dictionary<PropertyDataId, uint> DataIds = new();
public Dictionary<PropertyInstanceId,uint> InstanceIds = new();
public int? GetInt(PropertyInt k) => Ints.TryGetValue(k, out var v) ? v : null;
// ... etc
// Merge an incoming AppraiseInfo table into this bundle (for identify responses)
public void MergeFromAppraise(PropertyBundle other) { /* ... */ }
// Wire serialization — binary-identical to ACE AppraiseInfo.cs §5
public void WritePackableHashTable<TKey, TValue>(BinaryWriter w,
Dictionary<TKey, TValue> dict, int numBuckets) { /* ... */ }
}
Container
public class Container : ItemInstance
{
public List<ItemInstance> MainPack = new(); // ItemsCapacity slots
public List<Container> SidePacks = new(); // ContainersCapacity slots
public int ItemsCapacity => Properties.GetInt(PropertyInt.ItemsCapacity) ?? 0;
public int ContainersCapacity => Properties.GetInt(PropertyInt.ContainersCapacity) ?? 0;
public int GetFreeInventorySlots(bool includeSidePacks = true) { /* ... */ }
public bool TryAddItem(ItemInstance item, int slot) { /* ... */ }
public bool TryMoveItem(ItemInstance item, Container target, int slot) { /* ... */ }
}
InventoryManager (top-level)
public sealed class InventoryManager
{
public Player Owner;
public Container MainInventory; // = Owner itself
public Dictionary<EquipMask, ItemInstance> Equipped = new();
public Dictionary<uint, ItemInstance> ByGuid = new();
public int GetEncumbranceCapacity()
{
int str = Owner.Attributes[Attribute.Strength].Current;
int augs = Owner.Properties.GetInt(PropertyInt.AugmentationIncreasedCarryingCapacity) ?? 0;
return EncumbranceSystem.EncumbranceCapacity(str, augs);
}
public bool HasEnoughBurdenToAddToInventory(int addBurden) =>
Owner.EncumbranceVal + addBurden <= GetEncumbranceCapacity() * 3;
// Event handlers from the wire layer
public void OnCreateObject(ItemInstance item);
public void OnUpdateObject(ItemInstance item);
public void OnDeleteObject(uint guid);
public void OnPutObjInContainer(uint item, uint container, int slot, ContainerType type);
public void OnWieldObject(uint item, EquipMask slot);
public void OnUpdateStackSize(uint item, uint newSize, uint newValue);
public void OnAppraiseInfo(uint item, PropertyBundle props, IdentifyResponseFlags flags, bool success);
// Commands the UI issues upward
public Task MoveItemAsync(uint item, uint container, int slot);
public Task SplitStackAsync(uint item, uint container, int slot, int amount);
public Task MergeStackAsync(uint source, uint target, int amount);
public Task DropItemAsync(uint item);
public Task WieldItemAsync(uint item, EquipMask slot);
public Task GiveItemAsync(uint item, uint target, int amount);
public Task AppraiseAsync(uint item);
}
Integration with R6 UI
The retail paperdoll at chunk_004A0000.c dispatches drag/drop through fixed widget offsets (§2). In acdream we mirror that with a Paperdoll UI control that binds each slot to its EquipMask bit. UI framework events map 1:1:
- Drag-enter over a slot with no held item → show tooltip "Drag X here to wear them" (translate slot bit → drop-hint string verbatim).
- Drag-enter with a held item → show "ItemName (worn)/(wielded)\nDouble-click to take off/unwield".
- Drag-drop → call
InventoryManager.WieldItemAsync(item, slotBit). - Double-click on an equipped slot →
MoveItemAsync(item, player, nextFreeSlot).
Events surfaced via the IEvents plugin API should include:
InventoryChanged(ItemInstance added, ItemInstance removed)— allows a pickup tally plugin.EquipmentChanged(EquipMask slot, ItemInstance? item)— paperdoll subscribers.AppraiseReceived(ItemInstance item, bool success)— shows the appraise popup.StackSizeChanged(ItemInstance item, ushort from, ushort to)— coin counter plugins.
Plugins interact via these events and may issue commands through an IInventoryCommands (a plugin-safe subset of InventoryManager's public methods). Drag-drop hooks to 0x15 / 0x3E are inventory events fired on the plugin bus — the dispatcher maps item GUIDs to human-readable names for macro authors.
16. Port conformance tests
At minimum acdream must port 5+ conformance tests into its test suite (mirror ACE's style):
Test 1 — known weenie properties round-trip
Load a known dat weenie (e.g. weenie 273, Pyreal) and assert:
WeenieType == Coin,ItemType == Money,MaxStackSize == 10000,BaseBurden == 4(retail value),Icon == 0x0600109c(pyreal icon).- A fresh
ItemInstancewithStackSize = 100hasEncumbranceVal == 400andValue == 100.
Test 2 — burden formula matrix
Port EncumbranceSystem.EncumbranceCapacity and GetBurden, GetBurdenMod exactly. Table-test:
| strength | augs | expected capacity | burden=1.0*cap mod | burden=1.5*cap mod | burden=2.5*cap mod |
|---|---|---|---|---|---|
| 10 | 0 | 1500 | 1.0 | 0.5 | 0.0 |
| 100 | 0 | 15000 | 1.0 | 0.5 | 0.0 |
| 200 | 1 | 36000 | 1.0 | 0.5 | 0.0 |
| 400 | 5 | 120000 | 1.0 | 0.5 | 0.0 |
| 0 | 0 | 0 | 3.0 (infinite) | 3.0 | 3.0 |
Test 3 — PublicWeenieDesc byte-exact round-trip
Serialize a fixed ItemInstance for a dagger with Name="Darkmoon Dagger", StackSize=1, ValidLocations=Held|TwoHanded, Workmanship=7.5, MaterialType=Steel, and compare the exact byte output against a golden hex capture from a live retail pcap. Flags and field order must match PublicWeenieDesc.generated.cs §4.
Edge cases:
- A pyreal stack of 10000: flags
Value | StackSize | MaxStackSize | Burden | Container. - A locked chest: flags
ItemsCapacity | ContainersCapacity | Value | Usable | UseRadius | Structure. IncludeObjectDescriptionFlag.Openable | Stuck. - An equipped cloak: flags
Value | ValidLocations | CurrentWieldedLocation | Priority | Burden | Wielder | IconOverlay.
Test 4 — PackableHashTable bucketing
Given a fixed set of 5 PropertyInt entries (e.g. Value=1000, Burden=5, ItemType=2, Workmanship=8, StackSize=1), verify the emitted PackableHashTable bucket order matches ACE's PropertyIntComparer(16) exactly. Hand-compute expected order from the ACE comparer hash or cross-check against a captured pcap.
Test 5 — stack split/merge invariants
Given two stacks:
- Source:
Value=3000, StackSize=3, MaxStackSize=10000(3 pyreals). - Target:
Value=5000, StackSize=5, MaxStackSize=10000(5 pyreals).
Merging with amount=3 yields:
- Target:
StackSize=8, Value=8000. - Source: removed (OnDelete fires).
Merging with amount=3 when target would exceed max:
- Target
StackSize=9998, merge amount=3 → TargetStackSize=10000, SourceStackSize=1. - Fire
SetStackSizefor both; GUIDs stay stable; noObjectDelete.
Splitting a stack of 100 pyreals by 40:
- Original:
StackSize=60, Value=60*unit. - New stack (new GUID):
StackSize=40, Value=40*unit. - Fire
CreateObjectfor the new stack,SetStackSizefor the old stack.
Test 6 — appraise response flag gating
Build an AppraiseInfo with only:
PropertiesInt = {ItemType=1}Success = true
Verify:
Flags == IdentifyResponseFlags.IntStatsTable.- Serialized output starts with
[Flags:uint32] [Success:uint32] [PackableHashTable<PropInt,int>:...]— no subsequent blocks emitted (because no other flag bits are set).
Then build with an ArmorProfile:
Flags == IdentifyResponseFlags.IntStatsTable | ArmorProfile = 0x0081.- After the int table comes
ArmorProfileserialized.
Test 7 — paperdoll drop-hint strings
For each EquipMask slot bit, fetch the drop-hint string and compare against the retail table from §2. Any string mismatch = regression. This is the visual-parity gate.
Test 8 — two-level container depth enforcement
Attempt to add a side pack into another side pack:
- Server rejects; verify acdream client refuses the move and shows WError
0x26("That is not a valid command.") or equivalent.
Attempt to add a non-container to a side-pack sub-slot:
- Should go to the side pack's main-pack list, not the
SidePackslist.
Summary of key facts for implementation
- ItemType is a 32-bit bitfield, not enum. Use
[Flags]in C# and serialize asuint32. - EquipMask is a 32-bit bitfield that is both the set of valid slots and the currently-wielded slot. 31 slot bits +
TwoHandedmeta-bit. - The property model is seven tables keyed by ushort. Each table is sent independently on the wire, gated by its flag bit. Bucketed hash order matters — comparers must match.
- Burden capacity =
150 × Strength + Strength × bonusBurdenwherebonusBurden = 30 × augLevelcapped at 150. Carry limit is 3×capacity. Movement drops to 0 between 2× and 3× carry. - Containers are max two-deep. Main pack + side packs; no side-pack-in-side-pack.
UseBackpackSlot = WeenieType == Container || ContainerType == Foci. - Stack messages share a common (source, target, amount) pattern for merge/split variants; the wire opcodes for split differ by destination (Container=0x0055, 3D=0x0056, Wield=0x019B).
- CreateObject always carries three structs:
ObjectDesc(model),PhysicsDesc(motion),PublicWeenieDesc(game data). For items in inventory, the physics half is minimal; for items wielded,WielderId+CurrentWieldedLocationare set. - The paperdoll UI slots at offsets
0x604..0x660define the visual-to-slot mapping. Every drop-hint string is a load-bearing retail asset — reproduce verbatim. - The Appraise response can optionally include armor/creature/weapon/hook profiles as nested structs after the property tables, and three enchantment-highlight pairs as
ushortbitfields. - Icons are
0x06000000-family DIDs; overlays (enchanted glow, rare foil) composite on top.UiEffects(PropertyInt 18) adds a client-side post-process tint — it is not a dat asset.
This is the foundation. Every other item-facing system (salvage UI, trade window, vendor list, house hook, crafting panel, weapon combat-mode indicator) reads from the same PropertyBundle on the same ItemInstance — get this model right and the rest is UI.