using System; using System.Collections.Generic; namespace AcDream.Core.Items; // ───────────────────────────────────────────────────────────────────── // Scaffold for R6 — items + inventory data model. // Full research: docs/research/deepdives/r06-items-inventory.md // ───────────────────────────────────────────────────────────────────── /// /// AC's ItemType is a 32-bit flags enum — a single dat weenie can /// assert multiple type bits. From ACE.Entity.Enum.ItemType /// cross-checked against the decompile paperdoll tooltip dispatcher. /// Full bit list in the research doc §1. /// [Flags] public enum ItemType : uint { None = 0, MeleeWeapon = 0x00000001, Armor = 0x00000002, Clothing = 0x00000004, Jewelry = 0x00000008, Creature = 0x00000010, Food = 0x00000020, Money = 0x00000040, Misc = 0x00000080, MissileWeapon = 0x00000100, Container = 0x00000200, Useless = 0x00000400, Gem = 0x00000800, SpellComponents = 0x00001000, Writable = 0x00002000, Key = 0x00004000, Caster = 0x00008000, Portal = 0x00010000, Lockable = 0x00020000, PromissoryNote = 0x00040000, ManaStone = 0x00080000, Service = 0x00100000, MagicWieldable = 0x00200000, CraftCookingBase = 0x00400000, CraftAlchemyBase = 0x00800000, CraftFletchingBase = 0x01000000, CraftAlchemyIntermediate= 0x02000000, CraftCookingIntermediate= 0x04000000, CraftFletchingIntermediate = 0x08000000, LifeStone = 0x10000000, TinkeringTool = 0x20000000, TinkeringMaterial = 0x40000000, Gameboard = 0x80000000u, Vestements = Armor | Clothing, Weapon = MeleeWeapon | MissileWeapon | Caster, WeaponOrCaster = Weapon, Item = Weapon | Armor | Clothing | Jewelry | Container, } /// /// Equipment slot bitmask. 31 slots from head to Aetheria. Paperdoll /// widget offsets +0x604..+0x660 in the retail panel correspond /// to these bits 1:1 (see r06 §2 and UI slice 05 paperdoll section). /// [Flags] public enum EquipMask : uint { None = 0, HeadWear = 0x00000001, ChestWear = 0x00000002, AbdomenWear = 0x00000004, UpperArmWear = 0x00000008, LowerArmWear = 0x00000010, HandWear = 0x00000020, UpperLegWear = 0x00000040, LowerLegWear = 0x00000080, FootWear = 0x00000100, ChestArmor = 0x00000200, AbdomenArmor = 0x00000400, UpperArmArmor = 0x00000800, LowerArmArmor = 0x00001000, HandArmor = 0x00002000, UpperLegArmor = 0x00004000, LowerLegArmor = 0x00008000, FootArmor = 0x00010000, Necklace = 0x00020000, LeftBracelet = 0x00040000, RightBracelet = 0x00080000, LeftRing = 0x00100000, RightRing = 0x00200000, MeleeWeapon = 0x00400000, Shield = 0x00800000, MissileWeapon = 0x01000000, Held = 0x02000000, // lit torch, book in hand MissileAmmo = 0x04000000, Cloak = 0x08000000, TrinketOne = 0x10000000, AetheriaRed = 0x20000000, AetheriaYellow= 0x40000000, AetheriaBlue = 0x80000000u, } /// /// AC's property model is split across 7 typed tables. See r06 §3 for /// the full property enumeration. This struct is a thin wrapper; real /// bundles come over the wire in ObjectDesc / IdentifyResponse. /// public sealed class PropertyBundle { public Dictionary Ints { get; } = new(); public Dictionary Int64s { get; } = new(); public Dictionary Bools { get; } = new(); public Dictionary Floats { get; } = new(); public Dictionary Strings { get; } = new(); public Dictionary DataIds { get; } = new(); public Dictionary InstanceIds { get; } = new(); public int GetInt (uint k, int def = 0) => Ints.TryGetValue(k, out var v) ? v : def; public bool GetBool (uint k, bool def = false) => Bools.TryGetValue(k, out var v) ? v : def; public double GetFloat (uint k, double def = 0) => Floats.TryGetValue(k, out var v) ? v : def; public string GetString(uint k, string def = "") => Strings.TryGetValue(k, out var v) ? v : def; } /// /// Per-object live state (the data side of every server object — items and creatures alike). /// Retail ACCWeenieObject. /// public sealed class ClientObject { public uint ObjectId { get; init; } public uint WeenieClassId { get; set; } // "blueprint" public string Name { get; set; } = ""; public ItemType Type { get; set; } public EquipMask ValidLocations { get; set; } public EquipMask CurrentlyEquippedLocation { get; set; } public uint IconId { get; set; } // 0x06xxxxxx public uint IconUnderlayId{ get; set; } // "magic" underlay public uint IconOverlayId { get; set; } // "enchanted" overlay /// /// UiEffects bitfield (retail PublicWeenieDesc._effects, acclient.h:37183). /// Drives the icon's effect-overlay recolor (Magical=0x1 … Nether=0x1000). /// CreateObject-only (weenieFlags 0x80) + live PublicUpdatePropertyInt(0x02CE); /// appraise never carries it. 0 = no effect. /// public uint Effects { get; set; } public int StackSize { get; set; } = 1; public int StackSizeMax { get; set; } = 1; public int Burden { get; set; } // per-stack total public int Value { get; set; } // pyreals public uint ContainerId { get; set; } // parent container ObjectId, or 0 public int ContainerSlot { get; set; } = -1; public bool Attuned { get; set; } public bool Bonded { get; set; } public uint WielderId { get; set; } // PropertyInstanceId.Wielder; 0 = not wielded public int ItemsCapacity { get; set; } // main-pack slots (containers) public int ContainersCapacity{ get; set; } // side-pack slots (containers) public uint Priority { get; set; } // ClothingPriority / CoverageMask layer order public int Structure { get; set; } // charges/uses remaining public int MaxStructure { get; set; } public float Workmanship { get; set; } // 0..10 (fractional on the wire) public PropertyBundle Properties { get; } = new(); } /// /// The wire-delivered patch from a CreateObject (0xF745). Nullable fields /// were gated by a WeenieHeader flag that was ABSENT — the merge upsert /// (ClientObjectTable.Ingest) leaves the existing value untouched /// for those, matching retail's SetWeenieDesc (patches only present fields). /// Non-nullable id/effect fields use 0 = "not sent". Effects is assigned /// unconditionally (0 clears) — the D.5.2 icon contract. Quantity fields are /// int? (ACE PropertyInt convention); id/mask fields are uint?. /// public readonly record struct WeenieData( uint Guid, string? Name, ItemType? Type, uint WeenieClassId, uint IconId, uint IconOverlayId, uint IconUnderlayId, uint Effects, int? Value, int? StackSize, int? StackSizeMax, int? Burden, uint? ContainerId, uint? WielderId, uint? ValidLocations, uint? CurrentWieldedLocation, uint? Priority, int? ItemsCapacity, int? ContainersCapacity, int? Structure, int? MaxStructure, float? Workmanship); /// /// Container = inventory pack. Hierarchy is strictly 2-deep: character /// → side packs; a side pack cannot hold another side pack (r06 §7). /// public sealed class Container { public uint ObjectId { get; init; } public int Capacity { get; set; } = 102; // main inv default public int SideCapacity { get; set; } = 0; // 0 for side-pack public int BurdenLimit { get; set; } public List Items { get; } = new(); public List SidePacks { get; } = new(); // empty for side-pack public bool IsSidePack => SideCapacity == 0; } /// /// Burden math — r06 §6. maxBurden = 150 × Strength + Strength × bonusBurden; /// carry limit is 3 × maxBurden before you can't pick up at all. /// public static class BurdenMath { public const int BurdenPerStrength = 150; public static int ComputeMax(int strength, int bonusBurden) => BurdenPerStrength * strength + strength * bonusBurden; public static int ComputeCarryLimit(int strength, int bonusBurden) => 3 * ComputeMax(strength, bonusBurden); /// /// Retail's "encumbered" multiplier interpolates between 1.0 at /// zero burden and a low value at max. See r06 §6 for the curve. /// public static float ComputeEncumbranceMod(int currentBurden, int maxBurden) { if (maxBurden <= 0) return 1f; float ratio = (float)currentBurden / maxBurden; // Roughly 1.0 until 50%, then linear decay to ~0.7 at 100%, 0.1 at 300%. if (ratio <= 0.5f) return 1f; if (ratio <= 1.0f) return 1f - (ratio - 0.5f) * 0.6f; // 1.0 → 0.7 if (ratio <= 3.0f) return 0.7f - (ratio - 1.0f) * 0.3f; // 0.7 → 0.1 return 0.1f; } }