diff --git a/CLAUDE.md b/CLAUDE.md index a23fbbb..b4d41dc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -597,9 +597,10 @@ literal, in this order: (`D2C_UnitTypes`), `D2Monsters.h` (`D2C_MonTypeFlags`). Most are already in scope through `imports/D2Common.h`. Platform values likewise: `WM_LBUTTONDOWN`, not `0x201`. 2. **`src/contract/game/`** - `Constants.h` and the enums in `Types.h` (`UnitType`, - `MonsterSpecType`, `ItemLocation`, ...). The frontend and the contract cannot see - D2MOO, so these mirror the handful of values the JS API needs; use them there - instead of re-typing the literal. + `Stat`, `State`, `Skill`, `StatListFlags`, `ItemLocation`, ...). The frontend and the + contract cannot see D2MOO, so these mirror the values the JS API needs; use them there + instead of re-typing the literal. A backend takes the contract enum in its typed + imports and `static_assert`s it against D2MOO's constant where the two meet. 3. **The backend's shared headers** - e.g. `SUBTILE_SCALE` in `backends/lod114d/game/DrlgHelpers.h`. @@ -607,7 +608,7 @@ When a value genuinely has to be restated, derive it from the canonical definiti rather than repeating the literal: ```cpp -constexpr uint32_t STAT_FIXED_POINT_FIRST = STAT_HITPOINTS; +constexpr uint32_t UNIT_TYPE_NPC = UNIT_MONSTER; ``` **Scope it to its use.** A constant used by one file stays in that file's anonymous diff --git a/scripts/gen_dts.py b/scripts/gen_dts.py index e088eb9..496c1ce 100644 --- a/scripts/gen_dts.py +++ b/scripts/gen_dts.py @@ -45,6 +45,10 @@ def ts_type(t, *, is_return=False): t = str(t).strip() if t in _NUMBER_TYPES: return "number" + # A bitfield / namespace name inside a composite type (`CollisionFlag[][]`, + # `{flags:StatListFlags}`) is `number` too. + if _NUMBER_TYPES: + t = re.sub(r"\b(?:" + "|".join(map(re.escape, sorted(_NUMBER_TYPES))) + r")\b", "number", t) if is_return and t == "null": return "void" # "returns null" is the API's way of saying "no result" # a bare `function` param type with no callback detail -> the Function type diff --git a/src/backends/lod114d/game/Control.cpp b/src/backends/lod114d/game/Control.cpp index 85e983f..b4a1898 100644 --- a/src/backends/lod114d/game/Control.cpp +++ b/src/backends/lod114d/game/Control.cpp @@ -162,15 +162,12 @@ uint32_t Control::SelectEnd() const { return ctrl ? ctrl->dwSelectEnd : 0U; } -bool Control::HasLocaleText(int32_t localeId) const { - if (localeId < 0) { - return false; - } +bool Control::HasLocaleText(uint16_t localeId) const { const auto ctrl = Resolve(); if (!ctrl) { return false; } - const auto* localeText = lod114d::imports::d2lang::D2LANG_GetLocaleText(static_cast(localeId)); + const auto* localeText = lod114d::imports::d2lang::D2LANG_GetLocaleText(localeId); if (localeText == nullptr) { return false; } diff --git a/src/backends/lod114d/game/DrlgHelpers.h b/src/backends/lod114d/game/DrlgHelpers.h index 5c68934..0358f16 100644 --- a/src/backends/lod114d/game/DrlgHelpers.h +++ b/src/backends/lod114d/game/DrlgHelpers.h @@ -38,7 +38,7 @@ constexpr int32_t SUBTILE_SCALE = 5; // `D2UnitStrc::pDrlgAct` is typed as D2MOO's `::D2DrlgActStrc*`, but the bytes // at that address follow the 1.14d allocation modeled by // `extras::D2DrlgActStrc`. Reinterpret at the unit boundary. -inline D2DrlgLevelStrc* FindLevelInChain(uint32_t levelId) { +inline D2DrlgLevelStrc* FindLevelInChain(LevelId levelId) { auto* player = lod114d::imports::d2client::UNITS_GetPlayerUnit(); if (player == nullptr) { return nullptr; @@ -48,7 +48,7 @@ inline D2DrlgLevelStrc* FindLevelInChain(uint32_t levelId) { return nullptr; } for (auto* level = act->pDrlg->pLevel; level != nullptr; level = level->pNextLevel) { - if (static_cast(level->nLevelId) != levelId) { + if (static_cast(level->nLevelId) != levelId) { continue; } if (level->pFirstRoomEx == nullptr) { diff --git a/src/backends/lod114d/game/GameHelpers.cpp b/src/backends/lod114d/game/GameHelpers.cpp index 0b2e200..426279e 100644 --- a/src/backends/lod114d/game/GameHelpers.cpp +++ b/src/backends/lod114d/game/GameHelpers.cpp @@ -285,11 +285,11 @@ void SetAutomapOn(bool value) { } bool GetAlwaysRun() { - return *d2client::gbAlwaysRun != 0; + return *d2client::gbAlwaysRun != MoveMode::Walk; } void SetAlwaysRun(bool value) { - *d2client::gbAlwaysRun = value ? 1U : 0U; + *d2client::gbAlwaysRun = value ? MoveMode::Run : MoveMode::Walk; } bool GetNoPickUp() { @@ -304,15 +304,19 @@ uint32_t GetWeaponSwitch() { return *d2client::gnWeaponSwitch; } -uint32_t GetGameType() { - return *d2client::gbExpCharFlag; +GameType GetGameType() { + return static_cast(*d2client::gbExpCharFlag); } uint32_t GetMercReviveCost() { return *d2client::gnMercReviveCost; } -uint32_t GetLocale() { +static_assert(std::to_underlying(Language::English) == LANGUAGE_ENGLISH); +static_assert(std::to_underlying(Language::ChineseSimplified) == LANGUAGE_CHINESESIN); +static_assert(std::to_underlying(Language::ChineseTraditional) == LANGUAGE_CHINESETWN); + +Language GetLocale() { return *d2client::gnLang; } @@ -680,7 +684,7 @@ bool ClickMapAt(uint32_t clickType, bool shift, Point pos) { const auto savedY = *d2client::gnMouseY; *d2client::gnMouseX = 0; *d2client::gnMouseY = 0; - const uint32_t runFlag = (*d2client::gbAlwaysRun != 0) ? 0x08U : 0U; + const uint32_t runFlag = GetAlwaysRun() ? 0x08U : 0U; const uint32_t flag = shift ? 0x0CU : runFlag; // Force-NULL through P5 so the click is unambiguously coord-only -- // matches reference Core.cpp:168-173 (bClickAction=TRUE, unit=NULL). @@ -726,7 +730,7 @@ bool ClickMapAt(uint32_t clickType, bool shift, const Unit& unit) { // Stage the unit for P5 -- unless it's the player, in which case // reference treats it as a coord-only click (Core.cpp:155). const bool unitIsPlayer = (pUnit == d2client::UNITS_GetPlayerUnit()); - const uint32_t runFlag = (*d2client::gbAlwaysRun != 0) ? 0x08U : 0U; + const uint32_t runFlag = GetAlwaysRun() ? 0x08U : 0U; const uint32_t flag = shift ? 0x0CU : runFlag; lod114d::hooks::intercepts::WithSelectedUnit(unitIsPlayer ? nullptr : pUnit, [&] { d2client::UI_ClickMap(clickType, static_cast(click.x), static_cast(click.y), flag); @@ -836,8 +840,8 @@ bool HasWaypoint(uint32_t waypointId) { return d2common::WAYPOINTS_IsActivated(*d2client::gpWaypointTable, static_cast(waypointId)) != 0; } -bool IsTownByLevelNo(uint32_t levelNo) { - return d2common::DUNGEON_IsTownLevelId(levelNo); +bool IsTownByLevelNo(LevelId levelNo) { + return d2common::DUNGEON_IsTownLevelId(std::to_underlying(levelNo)); } std::string GetLocaleString(uint16_t localeId) { @@ -873,7 +877,7 @@ static_assert(std::to_underlying(QuestFlag::RewardGranted) == QFLAG_REWARDGRANTE static_assert(std::to_underlying(QuestFlag::Started) == QFLAG_STARTED); static_assert(std::to_underlying(QuestFlag::CompletedBefore) == QFLAG_COMPLETEDBEFORE); -bool GetQuestFlag(uint32_t quest, QuestFlag flag) { +bool GetQuestFlag(Quest quest, QuestFlag flag) { return d2common::QUESTRECORD_GetQuestFlag(d2client::QUESTRECORD_GetQuestInfo(), quest, flag) != 0; } @@ -892,7 +896,7 @@ void SwapWeapon() { // Reference Game.cpp:14-26 UseStatPoint -- packet 0x3A with the stat id, sent // `count` times spaced by 500ms. We mirror the same cadence; the timing keeps // the server's accept-state machine happy. -void UseStatPoint(uint32_t stat, uint32_t count) { +void UseStatPoint(Stat stat, uint16_t count) { if (count == 0) { return; } @@ -900,7 +904,7 @@ void UseStatPoint(uint32_t stat, uint32_t count) { if (!myUnit) { return; } - if (myUnit.GetStat(STAT_STATPTS, 0) < static_cast(count)) { + if (myUnit.GetStat(Stat::StatPoints) < count) { return; } std::array packet{}; @@ -917,7 +921,7 @@ void UseStatPoint(uint32_t stat, uint32_t count) { // Reference Game.cpp:28-40 UseSkillPoint -- packet 0x3B, otherwise identical // shape to UseStatPoint. -void UseSkillPoint(uint32_t skill, uint32_t count) { +void UseSkillPoint(Skill skill, uint16_t count) { if (count == 0) { return; } @@ -925,13 +929,12 @@ void UseSkillPoint(uint32_t skill, uint32_t count) { if (!myUnit) { return; } - if (myUnit.GetStat(STAT_SKILLPTS, 0) < static_cast(count)) { + if (myUnit.GetStat(Stat::NewSkills) < count) { return; } std::array packet{}; packet[0] = 0x3B; - const auto skill16 = static_cast(skill); - std::memcpy(packet.data() + 1, &skill16, sizeof(uint16_t)); + std::memcpy(packet.data() + 1, &skill, sizeof(skill)); for (uint32_t i = 0; i < count; ++i) { d2net::CLIENT_Send(packet.size(), 1, packet.data()); if (i + 1 != count) { @@ -1482,235 +1485,235 @@ uint32_t CheckUnitCollision(const Unit& unit1, const Unit& unit2, CollisionFlag struct SkillEntry { std::string_view name; - int32_t id; + Skill id; }; // 1.14d skill table extracted from reference/d2bs/D2Skills.h (216 entries). // Future game versions could read from game data files or memory instead. // NOLINTBEGIN(cppcoreguidelines-avoid-c-arrays,modernize-avoid-c-arrays) static constexpr SkillEntry SKILLS_1_14D[] = { - {.name = "Attack", .id = 0}, - {.name = "Kick", .id = 1}, - {.name = "Throw", .id = 2}, - {.name = "Unsummon", .id = 3}, - {.name = "Left Hand Throw", .id = 4}, - {.name = "Left Hand Swing", .id = 5}, - {.name = "Magic Arrow", .id = 6}, - {.name = "Fire Arrow", .id = 7}, - {.name = "Inner Sight", .id = 8}, - {.name = "Critical Strike", .id = 9}, - {.name = "Jab", .id = 10}, - {.name = "Cold Arrow", .id = 11}, - {.name = "Multiple Shot", .id = 12}, - {.name = "Dodge", .id = 13}, - {.name = "Power Strike", .id = 14}, - {.name = "Poison Javelin", .id = 15}, - {.name = "Exploding Arrow", .id = 16}, - {.name = "Slow Missiles", .id = 17}, - {.name = "Avoid", .id = 18}, - {.name = "Impale", .id = 19}, - {.name = "Lightning Bolt", .id = 20}, - {.name = "Ice Arrow", .id = 21}, - {.name = "Guided Arrow", .id = 22}, - {.name = "Penetrate", .id = 23}, - {.name = "Charged Strike", .id = 24}, - {.name = "Plague Javelin", .id = 25}, - {.name = "Strafe", .id = 26}, - {.name = "Immolation Arrow", .id = 27}, - {.name = "Decoy", .id = 28}, - {.name = "Evade", .id = 29}, - {.name = "Fend", .id = 30}, - {.name = "Freezing Arrow", .id = 31}, - {.name = "Valkyrie", .id = 32}, - {.name = "Pierce", .id = 33}, - {.name = "Lightning Strike", .id = 34}, - {.name = "Lightning Fury", .id = 35}, - {.name = "Fire Bolt", .id = 36}, - {.name = "Warmth", .id = 37}, - {.name = "Charged Bolt", .id = 38}, - {.name = "Ice Bolt", .id = 39}, - {.name = "Frozen Armor", .id = 40}, - {.name = "Inferno", .id = 41}, - {.name = "Static Field", .id = 42}, - {.name = "Telekinesis", .id = 43}, - {.name = "Frost Nova", .id = 44}, - {.name = "Ice Blast", .id = 45}, - {.name = "Blaze", .id = 46}, - {.name = "Fire Ball", .id = 47}, - {.name = "Nova", .id = 48}, - {.name = "Lightning", .id = 49}, - {.name = "Shiver Armor", .id = 50}, - {.name = "Fire Wall", .id = 51}, - {.name = "Enchant", .id = 52}, - {.name = "Chain Lightning", .id = 53}, - {.name = "Teleport", .id = 54}, - {.name = "Glacial Spike", .id = 55}, - {.name = "Meteor", .id = 56}, - {.name = "Thunder Storm", .id = 57}, - {.name = "Energy Shield", .id = 58}, - {.name = "Blizzard", .id = 59}, - {.name = "Chilling Armor", .id = 60}, - {.name = "Fire Mastery", .id = 61}, - {.name = "Hydra", .id = 62}, - {.name = "Lightning Mastery", .id = 63}, - {.name = "Frozen Orb", .id = 64}, - {.name = "Cold Mastery", .id = 65}, - {.name = "Amplify Damage", .id = 66}, - {.name = "Teeth", .id = 67}, - {.name = "Bone Armor", .id = 68}, - {.name = "Skeleton Mastery", .id = 69}, - {.name = "Raise Skeleton", .id = 70}, - {.name = "Dim Vision", .id = 71}, - {.name = "Weaken", .id = 72}, - {.name = "Poison Dagger", .id = 73}, - {.name = "Corpse Explosion", .id = 74}, - {.name = "Clay Golem", .id = 75}, - {.name = "Iron Maiden", .id = 76}, - {.name = "Terror", .id = 77}, - {.name = "Bone Wall", .id = 78}, - {.name = "Golem Mastery", .id = 79}, - {.name = "Raise Skeletal Mage", .id = 80}, - {.name = "Confuse", .id = 81}, - {.name = "Life Tap", .id = 82}, - {.name = "Poison Explosion", .id = 83}, - {.name = "Bone Spear", .id = 84}, - {.name = "Blood Golem", .id = 85}, - {.name = "Attract", .id = 86}, - {.name = "Decrepify", .id = 87}, - {.name = "Bone Prison", .id = 88}, - {.name = "Summon Resist", .id = 89}, - {.name = "Iron Golem", .id = 90}, - {.name = "Lower Resist", .id = 91}, - {.name = "Poison Nova", .id = 92}, - {.name = "Bone Spirit", .id = 93}, - {.name = "Fire Golem", .id = 94}, - {.name = "Revive", .id = 95}, - {.name = "Sacrifice", .id = 96}, - {.name = "Smite", .id = 97}, - {.name = "Might", .id = 98}, - {.name = "Prayer", .id = 99}, - {.name = "Resist Fire", .id = 100}, - {.name = "Holy Bolt", .id = 101}, - {.name = "Holy Fire", .id = 102}, - {.name = "Thorns", .id = 103}, - {.name = "Defiance", .id = 104}, - {.name = "Resist Cold", .id = 105}, - {.name = "Zeal", .id = 106}, - {.name = "Charge", .id = 107}, - {.name = "Blessed Aim", .id = 108}, - {.name = "Cleansing", .id = 109}, - {.name = "Resist Lightning", .id = 110}, - {.name = "Vengeance", .id = 111}, - {.name = "Blessed Hammer", .id = 112}, - {.name = "Concentration", .id = 113}, - {.name = "Holy Freeze", .id = 114}, - {.name = "Vigor", .id = 115}, - {.name = "Conversion", .id = 116}, - {.name = "Holy Shield", .id = 117}, - {.name = "Holy Shock", .id = 118}, - {.name = "Sanctuary", .id = 119}, - {.name = "Meditation", .id = 120}, - {.name = "Fist of the Heavens", .id = 121}, - {.name = "Fanaticism", .id = 122}, - {.name = "Conviction", .id = 123}, - {.name = "Redemption", .id = 124}, - {.name = "Salvation", .id = 125}, - {.name = "Bash", .id = 126}, - {.name = "Sword Mastery", .id = 127}, - {.name = "Axe Mastery", .id = 128}, - {.name = "Mace Mastery", .id = 129}, - {.name = "Howl", .id = 130}, - {.name = "Find Potion", .id = 131}, - {.name = "Leap", .id = 132}, - {.name = "Double Swing", .id = 133}, - {.name = "Pole Arm Mastery", .id = 134}, - {.name = "Throwing Mastery", .id = 135}, - {.name = "Spear Mastery", .id = 136}, - {.name = "Taunt", .id = 137}, - {.name = "Shout", .id = 138}, - {.name = "Stun", .id = 139}, - {.name = "Double Throw", .id = 140}, - {.name = "Increased Stamina", .id = 141}, - {.name = "Find Item", .id = 142}, - {.name = "Leap Attack", .id = 143}, - {.name = "Concentrate", .id = 144}, - {.name = "Iron Skin", .id = 145}, - {.name = "Battle Cry", .id = 146}, - {.name = "Frenzy", .id = 147}, - {.name = "Increased Speed", .id = 148}, - {.name = "Battle Orders", .id = 149}, - {.name = "Grim Ward", .id = 150}, - {.name = "Whirlwind", .id = 151}, - {.name = "Berserk", .id = 152}, - {.name = "Natural Resistance", .id = 153}, - {.name = "War Cry", .id = 154}, - {.name = "Battle Command", .id = 155}, - {.name = "Scroll of Townportal", .id = 219}, - {.name = "Book of Townportal", .id = 220}, - {.name = "Raven", .id = 221}, - {.name = "Poison Creeper", .id = 222}, - {.name = "Werewolf", .id = 223}, - {.name = "Shape Shifting", .id = 224}, - {.name = "Firestorm", .id = 225}, - {.name = "Oak Sage", .id = 226}, - {.name = "Summon Spirit Wolf", .id = 227}, - {.name = "Werebear", .id = 228}, - {.name = "Molten Boulder", .id = 229}, - {.name = "Arctic Blast", .id = 230}, - {.name = "Carrion Vine", .id = 231}, - {.name = "Feral Rage", .id = 232}, - {.name = "Maul", .id = 233}, - {.name = "Fissure", .id = 234}, - {.name = "Cyclone Armor", .id = 235}, - {.name = "Heart of Wolverine", .id = 236}, - {.name = "Summon Dire Wolf", .id = 237}, - {.name = "Rabies", .id = 238}, - {.name = "Fire Claws", .id = 239}, - {.name = "Twister", .id = 240}, - {.name = "Solar Creeper", .id = 241}, - {.name = "Hunger", .id = 242}, - {.name = "Shock Wave", .id = 243}, - {.name = "Volcano", .id = 244}, - {.name = "Tornado", .id = 245}, - {.name = "Spirit of barbs", .id = 246}, - {.name = "Summon Grizzly", .id = 247}, - {.name = "Fury", .id = 248}, - {.name = "Armageddon", .id = 249}, - {.name = "Hurricane", .id = 250}, - {.name = "Fire Blast", .id = 251}, - {.name = "Claw Mastery", .id = 252}, - {.name = "Psychic Hammer", .id = 253}, - {.name = "Tiger Strike", .id = 254}, - {.name = "Dragon Talon", .id = 255}, - {.name = "Shock Web", .id = 256}, - {.name = "Blade Sentinel", .id = 257}, - {.name = "Burst of Speed", .id = 258}, - {.name = "Fists of Fire", .id = 259}, - {.name = "Dragon Claw", .id = 260}, - {.name = "Charged Bolt Sentry", .id = 261}, - {.name = "Wake of Fire", .id = 262}, - {.name = "Weapon Block", .id = 263}, - {.name = "Cloak of Shadows", .id = 264}, - {.name = "Cobra Strike", .id = 265}, - {.name = "Blade Fury", .id = 266}, - {.name = "Fade", .id = 267}, - {.name = "Shadow Warrior", .id = 268}, - {.name = "Claws of Thunder", .id = 269}, - {.name = "Dragon Tail", .id = 270}, - {.name = "Lightning Sentry", .id = 271}, - {.name = "Wake of Inferno", .id = 272}, - {.name = "Mind Blast", .id = 273}, - {.name = "Blades of Ice", .id = 274}, - {.name = "Dragon Flight", .id = 275}, - {.name = "Death Sentry", .id = 276}, - {.name = "Blade Shield", .id = 277}, - {.name = "Venom", .id = 278}, - {.name = "Shadow Master", .id = 279}, - {.name = "Phoenix Strike", .id = 280}, + {.name = "Attack", .id = Skill::Attack}, + {.name = "Kick", .id = Skill::Kick}, + {.name = "Throw", .id = Skill::Throw}, + {.name = "Unsummon", .id = Skill::Unsummon}, + {.name = "Left Hand Throw", .id = Skill::LeftHandThrow}, + {.name = "Left Hand Swing", .id = Skill::LeftHandSwing}, + {.name = "Magic Arrow", .id = Skill::MagicArrow}, + {.name = "Fire Arrow", .id = Skill::FireArrow}, + {.name = "Inner Sight", .id = Skill::InnerSight}, + {.name = "Critical Strike", .id = Skill::CriticalStrike}, + {.name = "Jab", .id = Skill::Jab}, + {.name = "Cold Arrow", .id = Skill::ColdArrow}, + {.name = "Multiple Shot", .id = Skill::MultipleShot}, + {.name = "Dodge", .id = Skill::Dodge}, + {.name = "Power Strike", .id = Skill::PowerStrike}, + {.name = "Poison Javelin", .id = Skill::PoisonJavelin}, + {.name = "Exploding Arrow", .id = Skill::ExplodingArrow}, + {.name = "Slow Missiles", .id = Skill::SlowMissiles}, + {.name = "Avoid", .id = Skill::Avoid}, + {.name = "Impale", .id = Skill::Impale}, + {.name = "Lightning Bolt", .id = Skill::LightningBolt}, + {.name = "Ice Arrow", .id = Skill::IceArrow}, + {.name = "Guided Arrow", .id = Skill::GuidedArrow}, + {.name = "Penetrate", .id = Skill::Penetrate}, + {.name = "Charged Strike", .id = Skill::ChargedStrike}, + {.name = "Plague Javelin", .id = Skill::PlagueJavelin}, + {.name = "Strafe", .id = Skill::Strafe}, + {.name = "Immolation Arrow", .id = Skill::ImmolationArrow}, + {.name = "Decoy", .id = Skill::Dopplezon}, + {.name = "Evade", .id = Skill::Evade}, + {.name = "Fend", .id = Skill::Fend}, + {.name = "Freezing Arrow", .id = Skill::FreezingArrow}, + {.name = "Valkyrie", .id = Skill::Valkyrie}, + {.name = "Pierce", .id = Skill::Pierce}, + {.name = "Lightning Strike", .id = Skill::LightningStrike}, + {.name = "Lightning Fury", .id = Skill::LightningFury}, + {.name = "Fire Bolt", .id = Skill::FireBolt}, + {.name = "Warmth", .id = Skill::Warmth}, + {.name = "Charged Bolt", .id = Skill::ChargedBolt}, + {.name = "Ice Bolt", .id = Skill::IceBolt}, + {.name = "Frozen Armor", .id = Skill::FrozenArmor}, + {.name = "Inferno", .id = Skill::Inferno}, + {.name = "Static Field", .id = Skill::StaticField}, + {.name = "Telekinesis", .id = Skill::Telekinesis}, + {.name = "Frost Nova", .id = Skill::FrostNova}, + {.name = "Ice Blast", .id = Skill::IceBlast}, + {.name = "Blaze", .id = Skill::Blaze}, + {.name = "Fire Ball", .id = Skill::FireBall}, + {.name = "Nova", .id = Skill::Nova}, + {.name = "Lightning", .id = Skill::Lightning}, + {.name = "Shiver Armor", .id = Skill::ShiverArmor}, + {.name = "Fire Wall", .id = Skill::FireWall}, + {.name = "Enchant", .id = Skill::Enchant}, + {.name = "Chain Lightning", .id = Skill::ChainLightning}, + {.name = "Teleport", .id = Skill::Teleport}, + {.name = "Glacial Spike", .id = Skill::GlacialSpike}, + {.name = "Meteor", .id = Skill::Meteor}, + {.name = "Thunder Storm", .id = Skill::ThunderStorm}, + {.name = "Energy Shield", .id = Skill::EnergyShield}, + {.name = "Blizzard", .id = Skill::Blizzard}, + {.name = "Chilling Armor", .id = Skill::ChillingArmor}, + {.name = "Fire Mastery", .id = Skill::FireMastery}, + {.name = "Hydra", .id = Skill::Hydra}, + {.name = "Lightning Mastery", .id = Skill::LightningMastery}, + {.name = "Frozen Orb", .id = Skill::FrozenOrb}, + {.name = "Cold Mastery", .id = Skill::ColdMastery}, + {.name = "Amplify Damage", .id = Skill::AmplifyDamage}, + {.name = "Teeth", .id = Skill::Teeth}, + {.name = "Bone Armor", .id = Skill::BoneArmor}, + {.name = "Skeleton Mastery", .id = Skill::SkeletonMastery}, + {.name = "Raise Skeleton", .id = Skill::RaiseSkeleton}, + {.name = "Dim Vision", .id = Skill::DimVision}, + {.name = "Weaken", .id = Skill::Weaken}, + {.name = "Poison Dagger", .id = Skill::PoisonDagger}, + {.name = "Corpse Explosion", .id = Skill::CorpseExplosion}, + {.name = "Clay Golem", .id = Skill::ClayGolem}, + {.name = "Iron Maiden", .id = Skill::IronMaiden}, + {.name = "Terror", .id = Skill::Terror}, + {.name = "Bone Wall", .id = Skill::BoneWall}, + {.name = "Golem Mastery", .id = Skill::GolemMastery}, + {.name = "Raise Skeletal Mage", .id = Skill::RaiseSkeletalMage}, + {.name = "Confuse", .id = Skill::Confuse}, + {.name = "Life Tap", .id = Skill::LifeTap}, + {.name = "Poison Explosion", .id = Skill::PoisonExplosion}, + {.name = "Bone Spear", .id = Skill::BoneSpear}, + {.name = "Blood Golem", .id = Skill::BloodGolem}, + {.name = "Attract", .id = Skill::Attract}, + {.name = "Decrepify", .id = Skill::Decrepify}, + {.name = "Bone Prison", .id = Skill::BonePrison}, + {.name = "Summon Resist", .id = Skill::SummonResist}, + {.name = "Iron Golem", .id = Skill::IronGolem}, + {.name = "Lower Resist", .id = Skill::LowerResist}, + {.name = "Poison Nova", .id = Skill::PoisonNova}, + {.name = "Bone Spirit", .id = Skill::BoneSpirit}, + {.name = "Fire Golem", .id = Skill::FireGolem}, + {.name = "Revive", .id = Skill::Revive}, + {.name = "Sacrifice", .id = Skill::Sacrifice}, + {.name = "Smite", .id = Skill::Smite}, + {.name = "Might", .id = Skill::Might}, + {.name = "Prayer", .id = Skill::Prayer}, + {.name = "Resist Fire", .id = Skill::ResistFire}, + {.name = "Holy Bolt", .id = Skill::HolyBolt}, + {.name = "Holy Fire", .id = Skill::HolyFire}, + {.name = "Thorns", .id = Skill::Thorns}, + {.name = "Defiance", .id = Skill::Defiance}, + {.name = "Resist Cold", .id = Skill::ResistCold}, + {.name = "Zeal", .id = Skill::Zeal}, + {.name = "Charge", .id = Skill::Charge}, + {.name = "Blessed Aim", .id = Skill::BlessedAim}, + {.name = "Cleansing", .id = Skill::Cleansing}, + {.name = "Resist Lightning", .id = Skill::ResistLightning}, + {.name = "Vengeance", .id = Skill::Vengeance}, + {.name = "Blessed Hammer", .id = Skill::BlessedHammer}, + {.name = "Concentration", .id = Skill::Concentration}, + {.name = "Holy Freeze", .id = Skill::HolyFreeze}, + {.name = "Vigor", .id = Skill::Vigor}, + {.name = "Conversion", .id = Skill::Conversion}, + {.name = "Holy Shield", .id = Skill::HolyShield}, + {.name = "Holy Shock", .id = Skill::HolyShock}, + {.name = "Sanctuary", .id = Skill::Sanctuary}, + {.name = "Meditation", .id = Skill::Meditation}, + {.name = "Fist of the Heavens", .id = Skill::FistOfTheHeavens}, + {.name = "Fanaticism", .id = Skill::Fanaticism}, + {.name = "Conviction", .id = Skill::Conviction}, + {.name = "Redemption", .id = Skill::Redemption}, + {.name = "Salvation", .id = Skill::Salvation}, + {.name = "Bash", .id = Skill::Bash}, + {.name = "Sword Mastery", .id = Skill::BladeMastery}, + {.name = "Axe Mastery", .id = Skill::AxeMastery}, + {.name = "Mace Mastery", .id = Skill::MaceMastery}, + {.name = "Howl", .id = Skill::Howl}, + {.name = "Find Potion", .id = Skill::FindPotion}, + {.name = "Leap", .id = Skill::Leap}, + {.name = "Double Swing", .id = Skill::DoubleSwing}, + {.name = "Pole Arm Mastery", .id = Skill::PoleArmMastery}, + {.name = "Throwing Mastery", .id = Skill::ThrowingMastery}, + {.name = "Spear Mastery", .id = Skill::SpearMastery}, + {.name = "Taunt", .id = Skill::Taunt}, + {.name = "Shout", .id = Skill::Shout}, + {.name = "Stun", .id = Skill::Stun}, + {.name = "Double Throw", .id = Skill::DoubleThrow}, + {.name = "Increased Stamina", .id = Skill::IncreasedStamina}, + {.name = "Find Item", .id = Skill::FindItem}, + {.name = "Leap Attack", .id = Skill::LeapAttack}, + {.name = "Concentrate", .id = Skill::Concentrate}, + {.name = "Iron Skin", .id = Skill::IronSkin}, + {.name = "Battle Cry", .id = Skill::BattleCry}, + {.name = "Frenzy", .id = Skill::Frenzy}, + {.name = "Increased Speed", .id = Skill::IncreasedSpeed}, + {.name = "Battle Orders", .id = Skill::BattleOrders}, + {.name = "Grim Ward", .id = Skill::GrimWard}, + {.name = "Whirlwind", .id = Skill::Whirlwind}, + {.name = "Berserk", .id = Skill::Berserk}, + {.name = "Natural Resistance", .id = Skill::NaturalResistance}, + {.name = "War Cry", .id = Skill::WarCry}, + {.name = "Battle Command", .id = Skill::BattleCommand}, + {.name = "Scroll of Townportal", .id = Skill::ScrollOfTownportal}, + {.name = "Book of Townportal", .id = Skill::BookOfTownportal}, + {.name = "Raven", .id = Skill::Raven}, + {.name = "Poison Creeper", .id = Skill::PlaguePoppy}, + {.name = "Werewolf", .id = Skill::Wearwolf}, + {.name = "Shape Shifting", .id = Skill::ShapeShifting}, + {.name = "Firestorm", .id = Skill::Firestorm}, + {.name = "Oak Sage", .id = Skill::OakSage}, + {.name = "Summon Spirit Wolf", .id = Skill::SummonSpiritWolf}, + {.name = "Werebear", .id = Skill::Wearbear}, + {.name = "Molten Boulder", .id = Skill::MoltenBoulder}, + {.name = "Arctic Blast", .id = Skill::ArcticBlast}, + {.name = "Carrion Vine", .id = Skill::CycleOfLife}, + {.name = "Feral Rage", .id = Skill::FeralRage}, + {.name = "Maul", .id = Skill::Maul}, + {.name = "Fissure", .id = Skill::Eruption}, + {.name = "Cyclone Armor", .id = Skill::CycloneArmor}, + {.name = "Heart of Wolverine", .id = Skill::HeartOfWolverine}, + {.name = "Summon Dire Wolf", .id = Skill::SummonFenris}, + {.name = "Rabies", .id = Skill::Rabies}, + {.name = "Fire Claws", .id = Skill::FireClaws}, + {.name = "Twister", .id = Skill::Twister}, + {.name = "Solar Creeper", .id = Skill::Vines}, + {.name = "Hunger", .id = Skill::Hunger}, + {.name = "Shock Wave", .id = Skill::ShockWave}, + {.name = "Volcano", .id = Skill::Volcano}, + {.name = "Tornado", .id = Skill::Tornado}, + {.name = "Spirit of barbs", .id = Skill::SpiritOfBarbs}, + {.name = "Summon Grizzly", .id = Skill::SummonGrizzly}, + {.name = "Fury", .id = Skill::Fury}, + {.name = "Armageddon", .id = Skill::Armageddon}, + {.name = "Hurricane", .id = Skill::Hurricane}, + {.name = "Fire Blast", .id = Skill::FireTrauma}, + {.name = "Claw Mastery", .id = Skill::ClawMastery}, + {.name = "Psychic Hammer", .id = Skill::PsychicHammer}, + {.name = "Tiger Strike", .id = Skill::TigerStrike}, + {.name = "Dragon Talon", .id = Skill::DragonTalon}, + {.name = "Shock Web", .id = Skill::ShockField}, + {.name = "Blade Sentinel", .id = Skill::BladeSentinel}, + {.name = "Burst of Speed", .id = Skill::Quickness}, + {.name = "Fists of Fire", .id = Skill::FistsOfFire}, + {.name = "Dragon Claw", .id = Skill::DragonClaw}, + {.name = "Charged Bolt Sentry", .id = Skill::ChargedBoltSentry}, + {.name = "Wake of Fire", .id = Skill::WakeOfFireSentry}, + {.name = "Weapon Block", .id = Skill::WeaponBlock}, + {.name = "Cloak of Shadows", .id = Skill::CloakOfShadows}, + {.name = "Cobra Strike", .id = Skill::CobraStrike}, + {.name = "Blade Fury", .id = Skill::BladeFury}, + {.name = "Fade", .id = Skill::Fade}, + {.name = "Shadow Warrior", .id = Skill::ShadowWarrior}, + {.name = "Claws of Thunder", .id = Skill::ClawsOfThunder}, + {.name = "Dragon Tail", .id = Skill::DragonTail}, + {.name = "Lightning Sentry", .id = Skill::LightningSentry}, + {.name = "Wake of Inferno", .id = Skill::InfernoSentry}, + {.name = "Mind Blast", .id = Skill::MindBlast}, + {.name = "Blades of Ice", .id = Skill::BladesOfIce}, + {.name = "Dragon Flight", .id = Skill::DragonFlight}, + {.name = "Death Sentry", .id = Skill::DeathSentry}, + {.name = "Blade Shield", .id = Skill::BladeShield}, + {.name = "Venom", .id = Skill::Venom}, + {.name = "Shadow Master", .id = Skill::ShadowMaster}, + {.name = "Phoenix Strike", .id = Skill::RoyalStrike}, }; // NOLINTEND(cppcoreguidelines-avoid-c-arrays,modernize-avoid-c-arrays) -std::optional GetSkillByName(std::string_view name) { +std::optional GetSkillByName(std::string_view name) { auto iter = std::ranges::find_if( SKILLS_1_14D, [&](const SkillEntry& entry) { return utils::EqualsCaseInsensitive(entry.name, name); }); if (iter != std::end(SKILLS_1_14D)) { @@ -1779,13 +1782,13 @@ void* GetHwnd() { // Reference Game.cpp:8-12 SendGold. Drop and stash both write the same global // pair and trigger the gold-dialog action; the difference lives in the // dialog's mode value. -void GoldAction(GoldActionMode mode, int32_t amount) { +void GoldAction(GoldActionMode mode, uint32_t amount) { if (!WaitForGameReady()) { return; } GameThread::Execute([mode, amount]() { *d2client::gnGoldDialogAmount = amount; - *d2client::gnGoldDialogAction = static_cast(mode); + *d2client::gnGoldDialogAction = mode; d2client::UI_PerformGoldDialogAction(); }); } @@ -1818,7 +1821,7 @@ void MoveNPC(uint32_t npcId, Position pos) { // switch the AutomapLayer to the target's layer for the duration and restore // the player's own layer on exit. The layer save/restore is essential: scripts // that reveal a non-current level would otherwise corrupt the player's map. -bool RevealLevel(uint32_t levelNo, bool drawPresets) { +bool RevealLevel(LevelId levelNo, bool drawPresets) { if (!WaitForGameReady()) { return false; } @@ -1832,7 +1835,7 @@ bool RevealLevel(uint32_t levelNo, bool drawPresets) { } D2DrlgLevelStrc* target = nullptr; for (auto* level = act->pDrlg->pLevel; level != nullptr; level = level->pNextLevel) { - if (static_cast(level->nLevelId) == levelNo) { + if (static_cast(level->nLevelId) == levelNo) { target = level; break; } @@ -1854,9 +1857,9 @@ bool RevealLevel(uint32_t levelNo, bool drawPresets) { const auto* playerLevel = (pathRoom != nullptr && pathRoom->pDrlgRoom != nullptr) ? pathRoom->pDrlgRoom->pLevel : nullptr; const uint32_t playerLevelNo = (playerLevel != nullptr) ? static_cast(playerLevel->nLevelId) : 0U; - const bool isOtherLevel = (playerLevelNo != 0U && playerLevelNo != levelNo); + const bool isOtherLevel = (playerLevelNo != 0U && playerLevelNo != std::to_underlying(levelNo)); if (isOtherLevel) { - *d2client::gpAutomapLayer = lod114d::asm_thunks::InitAutomapLayerForLevel(levelNo); + *d2client::gpAutomapLayer = lod114d::asm_thunks::InitAutomapLayerForLevel(std::to_underlying(levelNo)); } for (auto* room = target->pFirstRoomEx; room != nullptr; room = room->pDrlgRoomNext) { @@ -2102,7 +2105,7 @@ Level FindLevelAt(Position gamePos) { const Rect bounds{.origin = {.x = toU(level->nPosX), .y = toU(level->nPosY)}, .size = {.width = toU(level->nWidth), .height = toU(level->nHeight)}}; if (bounds.Contains(subtile)) { - return Level(static_cast(level->nLevelId)); + return Level(static_cast(level->nLevelId)); } } return Level(); diff --git a/src/backends/lod114d/game/Level.cpp b/src/backends/lod114d/game/Level.cpp index cd93df1..a8789ec 100644 --- a/src/backends/lod114d/game/Level.cpp +++ b/src/backends/lod114d/game/Level.cpp @@ -8,19 +8,28 @@ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wmissing-braces" +#include // D2C_Levels #include // D2LevelsTxt #pragma clang diagnostic pop #include #include +#include namespace d2bs::game { using lod114d::imports::extras::D2DrlgLevelStrc; +static_assert(std::to_underlying(LevelId::RogueEncampment) == LEVEL_ROGUEENCAMPMENT); +static_assert(std::to_underlying(LevelId::LutGholein) == LEVEL_LUTGHOLEIN); +static_assert(std::to_underlying(LevelId::ThePandemoniumFortress) == LEVEL_THEPANDEMONIUMFORTRESS); +static_assert(std::to_underlying(LevelId::Harrogath) == LEVEL_HARROGATH); +static_assert(std::to_underlying(LevelId::TheWorldstoneChamber) == LEVEL_THEWORLDSTONECHAMBER); +static_assert(std::to_underlying(LevelId::UberTristram) == LEVEL_PANDEMONIUMFINALE); + void* Level::ResolvePtr() const { assert(IsGameLockHeld() && "resolve under the game lock - use Resolve()"); - if (id_ == 0) { + if (id_ == LevelId::None) { return nullptr; } if (auto* cached = cache_.Get()) { @@ -40,7 +49,7 @@ std::string Level::Name() const { if (!*this) { return {}; } - auto* txt = lod114d::imports::d2common::DATATBLS_GetLevelsTxtRecord(id_); + auto* txt = lod114d::imports::d2common::DATATBLS_GetLevelsTxtRecord(std::to_underlying(id_)); if (txt == nullptr) { return {}; } @@ -70,8 +79,8 @@ Room Level::GetFirstRoom() const { return Room::FromPtr(level->pFirstRoomEx); } -std::optional Level::Get(uint32_t levelNo) { - if (levelNo == 0) { +std::optional Level::Get(LevelId levelNo) { + if (levelNo == LevelId::None) { return std::nullopt; } GameReadLock guard; diff --git a/src/backends/lod114d/game/Menu.cpp b/src/backends/lod114d/game/Menu.cpp index 21fa8eb..46667cb 100644 --- a/src/backends/lod114d/game/Menu.cpp +++ b/src/backends/lod114d/game/Menu.cpp @@ -33,7 +33,7 @@ namespace { // cross-checked against D2MOO's DataTbls/StringIds.h and the string tables; // trailing text is the resolved English. Index space by base offset: string.tbl // 0, patchstring.tbl 10000, expansionstring.tbl 20000. -enum class StringId : int32_t { +enum class StringId : uint16_t { // string.tbl Exit = 5101, // "EXIT" Ok = 5102, // "OK" @@ -634,7 +634,13 @@ bool JoinGame(const std::string& name, const std::string& password) { return GameThread::Execute([]() -> bool { return ClickButtonAt(594, 433, 172, 32); }); } -bool CreateCharacter(const std::string& name, CharacterClass charClass, bool /*isHardcore*/, bool /*isLadder*/) { +bool CreateCharacter(const std::string& name, CharacterClass charClass, bool /*isHardcore*/, bool /*isLadder*/, + GameType gameType) { + // The create flow never touches the expansion checkbox, so it creates what + // the screen defaults to: an expansion character. + if (gameType != GameType::Expansion) { + return false; + } // Each UI interaction runs in its own GameThread::Execute and we sleep / // poll on the calling thread between them. Holding the game write lock // across the multi-step flow would prevent the menu UI from repainting @@ -722,7 +728,7 @@ OutOfGameLocation GetOutOfGameLocation() { // via Control::Find directly because they need the control, not just presence. auto findControl = [](ControlType type, uint32_t x, uint32_t y, uint32_t w, uint32_t h, std::optional label = std::nullopt) { - std::optional localeId; + std::optional localeId; if (label) { localeId = std::to_underlying(*label); } diff --git a/src/backends/lod114d/game/Party.cpp b/src/backends/lod114d/game/Party.cpp index 3faebf7..214bc9b 100644 --- a/src/backends/lod114d/game/Party.cpp +++ b/src/backends/lod114d/game/Party.cpp @@ -71,9 +71,9 @@ Position Party::Pos() const { return {.x = p->dwPosX, .y = p->dwPosY}; } -uint32_t Party::LevelId() const { +LevelId Party::LevelId() const { const auto p = Resolve(); - return p ? p->dwLevelId : 0; + return p ? static_cast(p->dwLevelId) : game::LevelId::None; } uint32_t Party::Id() const { @@ -104,12 +104,12 @@ std::string Party::Name() const { return std::string{static_cast(p->szName)}; } -uint32_t Party::ClassId() const { +CharacterClass Party::ClassId() const { const auto p = Resolve(); - return p ? p->dwClassId : 0; + return p ? static_cast(p->dwClassId) : CharacterClass::Amazon; } -uint32_t Party::CharacterLevel() const { +uint16_t Party::CharacterLevel() const { const auto p = Resolve(); return p ? p->wLevel : 0; } diff --git a/src/backends/lod114d/game/PlugY.cpp b/src/backends/lod114d/game/PlugY.cpp index 1665206..2a0856f 100644 --- a/src/backends/lod114d/game/PlugY.cpp +++ b/src/backends/lod114d/game/PlugY.cpp @@ -13,7 +13,6 @@ #include // D2ItemExtraDataStrc #include // IMODE_STORED -#include // STAT_GOLD #include // D2ItemDataStrc #include // D2PlayerDataStrc #include // D2UnitStrc @@ -598,7 +597,7 @@ bool MoveSharedGold(GoldActionMode mode) { if (ext == nullptr || !ext->HasSharedStash()) { return false; } - const auto carried = Unit::Player().GetStat(STAT_GOLD); + const auto carried = Unit::Player().GetStat(Stat::Gold); if (mode == GoldActionMode::Deposit && (carried <= 0 || ext->sharedGold == UINT32_MAX)) { return false; } diff --git a/src/backends/lod114d/game/Room.cpp b/src/backends/lod114d/game/Room.cpp index 8d72666..b098e61 100644 --- a/src/backends/lod114d/game/Room.cpp +++ b/src/backends/lod114d/game/Room.cpp @@ -71,7 +71,7 @@ inline D2DrlgRoomStrc* AsDrlgRoom(void* p) noexcept { void* Room::ResolvePtr() const { assert(IsGameLockHeld() && "resolve under the game lock - use Resolve()"); - if (level_ == 0) + if (level_ == game::LevelId::None) return nullptr; if (auto* cached = cache_.Get()) return cached; @@ -92,7 +92,7 @@ Room Room::FromPtr(void* p) { auto* drlgRoom = AsDrlgRoom(p); if (drlgRoom->pLevel == nullptr) return Room(); - Room handle(static_cast(drlgRoom->pLevel->nLevelId), + Room handle(static_cast(drlgRoom->pLevel->nLevelId), {.x = static_cast(drlgRoom->nTileXPos), .y = static_cast(drlgRoom->nTileYPos)}); handle.cache_.Set(p); return handle; @@ -133,11 +133,11 @@ Rect Room::Bounds() const { }; } -uint32_t Room::CorrectTomb() const { +LevelId Room::CorrectTomb() const { const auto drlgRoom = Resolve(); if (!drlgRoom || drlgRoom->pLevel == nullptr || drlgRoom->pLevel->pDrlg == nullptr) - return 0; - return static_cast(drlgRoom->pLevel->pDrlg->nStaffTombLevel); + return game::LevelId::None; + return static_cast(drlgRoom->pLevel->pDrlg->nStaffTombLevel); } std::vector> Room::GetCollision() const { @@ -246,7 +246,7 @@ Level Room::GetLevel() const { const auto drlgRoom = Resolve(); if (!drlgRoom || drlgRoom->pLevel == nullptr) return Level(); - return Level(static_cast(drlgRoom->pLevel->nLevelId)); + return Level(static_cast(drlgRoom->pLevel->nLevelId)); } std::optional Room::GetFirstUnit() const { @@ -270,16 +270,16 @@ std::vector Room::GetPresetUnits(std::optional type, s // level). Walked here so framework callers see the destination as a // populated field on PresetUnitInfo without needing a second boundary // call. Reference: D2Helpers.cpp::GetTileLevelNo. - auto resolveTileTarget = [&drlgRoom](uint32_t presetTileId) -> uint32_t { + auto resolveTileTarget = [&drlgRoom](uint32_t presetTileId) -> game::LevelId { for (auto* warp = drlgRoom->pRoomTiles; warp != nullptr; warp = warp->pNext) { if (warp->pPresetTileId == nullptr || warp->pDrlgRoom == nullptr || warp->pDrlgRoom->pLevel == nullptr) { continue; } if (*warp->pPresetTileId == presetTileId) { - return static_cast(warp->pDrlgRoom->pLevel->nLevelId); + return static_cast(warp->pDrlgRoom->pLevel->nLevelId); } } - return 0; + return game::LevelId::None; }; std::vector out; @@ -295,8 +295,8 @@ std::vector Room::GetPresetUnits(std::optional type, s .roomPos = roomPos, .posInRoom = {.x = static_cast(preset->nXpos), .y = static_cast(preset->nYpos)}, .id = presetIndex, - .level = 0, - .tileTargetLevelId = presetType == UnitType::Tile ? resolveTileTarget(presetIndex) : 0, + .level = game::LevelId::None, + .tileTargetLevelId = presetType == UnitType::Tile ? resolveTileTarget(presetIndex) : game::LevelId::None, }); } return out; diff --git a/src/backends/lod114d/game/Stash.cpp b/src/backends/lod114d/game/Stash.cpp index a27626a..341be29 100644 --- a/src/backends/lod114d/game/Stash.cpp +++ b/src/backends/lod114d/game/Stash.cpp @@ -5,9 +5,6 @@ #include "game/GameLock.h" #include "game/Unit.h" -#include // STAT_GOLDBANK - -#include #include #include #include @@ -42,7 +39,7 @@ uint32_t StashTab::Gold() const { if (kind_ == StashTabKind::Shared) { return plugy::IsActive() && plugy::HasStashTabs() ? plugy::SharedGold() : 0U; } - const auto gold = Unit::Player().GetStat(STAT_GOLDBANK); + const auto gold = Unit::Player().GetStat(Stat::GoldBank); return gold > 0 ? static_cast(gold) : 0U; } @@ -82,7 +79,7 @@ bool StashTab::MoveGold(GoldActionMode mode, uint32_t amount) const { if (index_ != 0 || amount == 0 || !*this) { return false; } - GoldAction(mode, static_cast(std::min(amount, INT32_MAX))); + GoldAction(mode, amount); return true; } diff --git a/src/backends/lod114d/game/Unit.cpp b/src/backends/lod114d/game/Unit.cpp index 2a8a371..b5f752b 100644 --- a/src/backends/lod114d/game/Unit.cpp +++ b/src/backends/lod114d/game/Unit.cpp @@ -24,9 +24,11 @@ #include // D2C_MonTypeFlags #include // D2GSPacketClt3C #include // D2SkillStrc, D2SkillListStrc -#include // D2StatStrc, D2StatListExStrc, STAT_* +#include // D2StatStrc, D2StatListExStrc, D2C_ItemStats +#include // D2C_States #include // D2ItemsTxt #include // D2ObjectsTxt +#include // D2C_Skills #include // D2SkillsTxt #include // D2DynamicPathStrc, D2StaticPathStrc #include // D2ItemDataStrc @@ -68,12 +70,36 @@ inline D2UnitStrc* AsUnit(void* p) noexcept { // Reference parity: D2 unit hash tables - 6 type buckets x 128 hash entries. // Constants and struct now live in imports/extras/D2UnitHashTables.h. +using lod114d::imports::d2common::STATLIST_UnitGetStatValue; using lod114d::imports::extras::D2UnitHashTable; using lod114d::imports::extras::D2UnitHashTables; using lod114d::imports::extras::UNIT_HASH_BUCKETS; using lod114d::imports::extras::UNIT_HASH_TYPE_COUNT; -static_assert(STAT_FIXED_POINT_FIRST == STAT_HITPOINTS && STAT_FIXED_POINT_LAST == STAT_MAXSTAMINA); +// The contract stat / state / skill ids this backend relies on, against D2MOO's 1.14d +// tables. PassiveMagicPierce, UberMinion and ImpFireMissileEx are the last ids 1.14d +// defines; D2MOO's SKILL_FIXEDSIEGEBEASTSTOMP after it has no 1.14d skills.txt row (a later +// version puts Interact at 357). +static_assert(Stat::StatPoints == static_cast(STAT_STATPTS)); +static_assert(Stat::NewSkills == static_cast(STAT_SKILLPTS)); +static_assert(Stat::HitPoints == static_cast(STAT_HITPOINTS)); +static_assert(Stat::MaxHitPoints == static_cast(STAT_MAXHP)); +static_assert(Stat::Mana == static_cast(STAT_MANA)); +static_assert(Stat::MaxMana == static_cast(STAT_MAXMANA)); +static_assert(Stat::Stamina == static_cast(STAT_STAMINA)); +static_assert(Stat::MaxStamina == static_cast(STAT_MAXSTAMINA)); +static_assert(Stat::Level == static_cast(STAT_LEVEL)); +static_assert(Stat::Experience == static_cast(STAT_EXPERIENCE)); +static_assert(Stat::Gold == static_cast(STAT_GOLD)); +static_assert(Stat::GoldBank == static_cast(STAT_GOLDBANK)); +static_assert(Stat::LastExperience == static_cast(STAT_LASTEXP)); +static_assert(Stat::NextExperience == static_cast(STAT_NEXTEXP)); +static_assert(Stat::ItemLevelReq == static_cast(STAT_ITEM_LEVELREQ)); +static_assert(Stat::PassiveMagicPierce == static_cast(STAT_PASSIVE_MAG_PIERCE)); +static_assert(State::None == static_cast(STATE_NONE)); +static_assert(State::UberMinion == static_cast(STATE_UBERMINION)); +static_assert(Skill::Attack == static_cast(SKILL_ATTACK)); +static_assert(Skill::ImpFireMissileEx == static_cast(SKILL_IMPFIREMISSILEEX)); static_assert(ENCHANT_SLOT_COUNT <= std::extent_v); // Reference parity: GetItemPrice's mode argument 0/1 = buy/sell, 3 = repair @@ -248,16 +274,16 @@ Position Unit::TargetPos() const { } } -uint32_t Unit::Area() const { +LevelId Unit::Area() const { const auto u = Resolve(); if (!u) { - return 0U; + return LevelId::None; } auto* room = lod114d::imports::d2common::UNITS_GetRoom(u); if (room == nullptr || room->pDrlgRoom == nullptr || room->pDrlgRoom->pLevel == nullptr) { - return 0U; + return LevelId::None; } - return static_cast(room->pDrlgRoom->pLevel->nLevelId); + return static_cast(room->pDrlgRoom->pLevel->nLevelId); } // === Stats === @@ -282,15 +308,13 @@ uint32_t Unit::Hp() const { } if (ownerUnit != nullptr) { const auto percent = lod114d::imports::d2common::UNITS_GetCurrentLifePercentage(u->dwUnitId); - const auto maxHp = - static_cast(lod114d::imports::d2common::STATLIST_UnitGetStatValue(u, STAT_MAXHP, 0)) >> - STAT_FIXED_POINT_SHIFT; + const auto maxHp = static_cast(STATLIST_UnitGetStatValue(u, Stat::MaxHitPoints, 0)) >> + STAT_FIXED_POINT_SHIFT; return (percent * maxHp) / 100U; } } } - return static_cast(lod114d::imports::d2common::STATLIST_UnitGetStatValue(u, STAT_HITPOINTS, 0)) >> - STAT_FIXED_POINT_SHIFT; + return static_cast(STATLIST_UnitGetStatValue(u, Stat::HitPoints, 0)) >> STAT_FIXED_POINT_SHIFT; } uint32_t Unit::HpMax() const { @@ -298,8 +322,7 @@ uint32_t Unit::HpMax() const { if (!u) { return 0U; } - return static_cast(lod114d::imports::d2common::STATLIST_UnitGetStatValue(u, STAT_MAXHP, 0)) >> - STAT_FIXED_POINT_SHIFT; + return static_cast(STATLIST_UnitGetStatValue(u, Stat::MaxHitPoints, 0)) >> STAT_FIXED_POINT_SHIFT; } uint32_t Unit::Mp() const { @@ -307,8 +330,7 @@ uint32_t Unit::Mp() const { if (!u) { return 0U; } - return static_cast(lod114d::imports::d2common::STATLIST_UnitGetStatValue(u, STAT_MANA, 0)) >> - STAT_FIXED_POINT_SHIFT; + return static_cast(STATLIST_UnitGetStatValue(u, Stat::Mana, 0)) >> STAT_FIXED_POINT_SHIFT; } uint32_t Unit::MpMax() const { @@ -316,8 +338,7 @@ uint32_t Unit::MpMax() const { if (!u) { return 0U; } - return static_cast(lod114d::imports::d2common::STATLIST_UnitGetStatValue(u, STAT_MAXMANA, 0)) >> - STAT_FIXED_POINT_SHIFT; + return static_cast(STATLIST_UnitGetStatValue(u, Stat::MaxMana, 0)) >> STAT_FIXED_POINT_SHIFT; } uint32_t Unit::Stamina() const { @@ -325,8 +346,7 @@ uint32_t Unit::Stamina() const { if (!u) { return 0U; } - return static_cast(lod114d::imports::d2common::STATLIST_UnitGetStatValue(u, STAT_STAMINA, 0)) >> - STAT_FIXED_POINT_SHIFT; + return static_cast(STATLIST_UnitGetStatValue(u, Stat::Stamina, 0)) >> STAT_FIXED_POINT_SHIFT; } uint32_t Unit::StaminaMax() const { @@ -334,8 +354,7 @@ uint32_t Unit::StaminaMax() const { if (!u) { return 0U; } - return static_cast(lod114d::imports::d2common::STATLIST_UnitGetStatValue(u, STAT_MAXSTAMINA, 0)) >> - STAT_FIXED_POINT_SHIFT; + return static_cast(STATLIST_UnitGetStatValue(u, Stat::MaxStamina, 0)) >> STAT_FIXED_POINT_SHIFT; } uint32_t Unit::CharLevel() const { @@ -343,30 +362,30 @@ uint32_t Unit::CharLevel() const { if (!u) { return 0U; } - return static_cast(lod114d::imports::d2common::STATLIST_UnitGetStatValue(u, STAT_LEVEL, 0)); + return static_cast(STATLIST_UnitGetStatValue(u, Stat::Level, 0)); } -int32_t Unit::GetStat(uint32_t stat, uint32_t sub) const { +int32_t Unit::GetStat(Stat stat, uint16_t sub) const { const auto u = Resolve(); if (!u) { return 0; } - int32_t value = lod114d::imports::d2common::STATLIST_UnitGetStatValue(u, stat, sub); + int32_t value = STATLIST_UnitGetStatValue(u, stat, sub); // Preset stat fallback: if the regular getter returned 0, search the // item-level (preset) stat list. Reference parity: JSUnit.cpp:980-993 // copies the full preset stat list and linear-scans it for the requested // (stat, sub) pair - no per-stat preset getter exists. if (value == 0) { - if (auto* preset = - lod114d::imports::d2common::STATLIST_GetStatListFromUnitStateAndFlag(u, 0U, STAT_LIST_PRESET_FLAG)) { + if (auto* preset = lod114d::imports::d2common::STATLIST_GetStatListFromUnitStateAndFlag(u, State::None, + StatListFlags::Magic)) { std::array buf{}; const auto count = lod114d::imports::d2common::STATLIST_CopyStats(preset, buf.data(), buf.size()); // NOLINTBEGIN(cppcoreguidelines-pro-bounds-constant-array-index) - bounded by count<=256 for (uint32_t i = 0; i < count; ++i) { const auto& s = buf[i]; - if (s.nStat == stat && s.nLayer == sub) { + if (static_cast(s.nStat) == stat && s.nLayer == sub) { value = s.nValue; break; } @@ -375,18 +394,18 @@ int32_t Unit::GetStat(uint32_t stat, uint32_t sub) const { } } - if (stat >= STAT_FIXED_POINT_FIRST && stat <= STAT_FIXED_POINT_LAST) { + if (IsFixedPointStat(stat)) { return value >> STAT_FIXED_POINT_SHIFT; } return value; } -bool Unit::HasState(uint32_t stateId) const { +bool Unit::HasState(State state) const { const auto u = Resolve(); if (!u) { return false; } - return lod114d::imports::d2common::STATES_CheckState(u, stateId) != 0; + return lod114d::imports::d2common::STATES_CheckState(u, state) != 0; } namespace { @@ -401,7 +420,7 @@ void AppendStatsRaw(const D2StatStrc* stats, uint32_t count, std::vector(s.nStat), .subIndex = s.nLayer, .value = s.nValue, }); @@ -418,11 +437,11 @@ void AppendStatsShifted(const D2StatStrc* stats, uint32_t count, std::vector= STAT_FIXED_POINT_FIRST && s.nStat <= STAT_FIXED_POINT_LAST) { + if (IsFixedPointStat(static_cast(s.nStat))) { value >>= STAT_FIXED_POINT_SHIFT; } out.push_back(StatEntry{ - .statId = s.nStat, + .statId = static_cast(s.nStat), .subIndex = s.nLayer, .value = value, }); @@ -449,8 +468,8 @@ std::vector Unit::GetAllStats() const { // Start with the preset stat list (D2COMMON_GetStatList(unit, nullptr, 0x40) // followed by D2COMMON_CopyStatList). - if (auto* preset = - lod114d::imports::d2common::STATLIST_GetStatListFromUnitStateAndFlag(u, 0U, STAT_LIST_PRESET_FLAG)) { + if (auto* preset = lod114d::imports::d2common::STATLIST_GetStatListFromUnitStateAndFlag(u, State::None, + StatListFlags::Magic)) { std::array buf{}; const auto count = lod114d::imports::d2common::STATLIST_CopyStats(preset, buf.data(), buf.size()); AppendStatsRaw(buf.data(), count, out); @@ -464,7 +483,7 @@ std::vector Unit::GetAllStats() const { // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) - bounded by nStatCount const auto& s = vec.pStat[i]; const StatEntry candidate{ - .statId = s.nStat, + .statId = static_cast(s.nStat), .subIndex = s.nLayer, .value = s.nValue, }; @@ -505,7 +524,8 @@ std::vector Unit::GetDetailedStats() const { }; appendList(u->pStatListEx); - appendList(lod114d::imports::d2common::STATLIST_GetStatListFromUnitStateAndFlag(u, 0U, STAT_LIST_PRESET_FLAG)); + appendList( + lod114d::imports::d2common::STATLIST_GetStatListFromUnitStateAndFlag(u, State::None, StatListFlags::Magic)); return out; } @@ -524,7 +544,9 @@ std::vector Unit::GetStatLists() const { if (list->Stats.pStat == nullptr || list->Stats.nStatCount == 0) { return; } - StatListEntry entry{.flags = list->dwFlags, .stateNo = list->dwStateNo, .stats = {}}; + StatListEntry entry{.flags = static_cast(list->dwFlags), + .stateNo = static_cast(list->dwStateNo), + .stats = {}}; AppendStatsShifted(list->Stats.pStat, list->Stats.nStatCount, entry.stats); out.push_back(std::move(entry)); }; @@ -611,7 +633,7 @@ uint32_t Unit::Direction() const { return u->pDynamicPath->nDirection; } -std::optional Unit::SuperUniqueId() const { +std::optional Unit::SuperUniqueId() const { const auto u = Resolve(); if (!u || u->dwUnitType != UNIT_MONSTER || u->pMonsterData == nullptr) { return std::nullopt; @@ -1003,10 +1025,10 @@ bool Unit::IsLocked() const { // === Player-specific === -uint32_t Unit::RunWalk() const { +MoveMode Unit::RunWalk() const { const auto u = Resolve(); if (!u || u != lod114d::imports::d2client::UNITS_GetPlayerUnit()) { - return 0U; + return MoveMode::Walk; } return *lod114d::imports::d2client::gbAlwaysRun; } @@ -1137,29 +1159,35 @@ const D2SkillStrc* PickEquippedSkill(D2UnitStrc* u, Hand hand) { return hand == Hand::Right ? u->pSkills->pRightSkill : u->pSkills->pLeftSkill; } +// The id of a skill-list entry with a skills.txt row. D2MOO types the id as int16_t; +// the game's -1 reads as Skill::Invalid. +Skill SkillOf(const D2SkillStrc* skill) { + return static_cast(skill->pSkillsTxt->nSkillId); +} + } // namespace std::string Unit::GetSkillName(Hand hand) const { - uint16_t skillId = 0; + Skill skillId{}; { const auto u = Resolve(); const auto* skill = PickEquippedSkill(u, hand); if (skill == nullptr || skill->pSkillsTxt == nullptr) { return {}; } - skillId = static_cast(skill->pSkillsTxt->nSkillId); + skillId = SkillOf(skill); } const auto stringId = SkillNameStringId(skillId); return stringId ? GetLocaleString(*stringId) : std::string{}; } -uint16_t Unit::GetSkillId(Hand hand) const { +Skill Unit::GetSkillId(Hand hand) const { const auto u = Resolve(); const auto* skill = PickEquippedSkill(u, hand); if (skill == nullptr || skill->pSkillsTxt == nullptr) { - return 0U; + return Skill{}; } - return static_cast(skill->pSkillsTxt->nSkillId); + return SkillOf(skill); } std::vector Unit::GetAllSkills() const { @@ -1174,7 +1202,7 @@ std::vector Unit::GetAllSkills() const { } // D2MOO's `nSkillLevel` is reference's `dwSkillLevel` (offset 0x28). out.push_back(SkillInfo{ - .skillId = static_cast(skill->pSkillsTxt->nSkillId), + .skillId = SkillOf(skill), .baseLevel = static_cast(skill->nSkillLevel), .totalLevel = lod114d::imports::d2common::SKILLS_GetSkillLevel(u, skill, true), }); @@ -1182,8 +1210,7 @@ std::vector Unit::GetAllSkills() const { return out; } -std::optional Unit::GetSkillLevel(uint16_t skillId, bool includeExtraLevels, - std::optional charge) const { +std::optional Unit::GetSkillLevel(Skill skill, bool includeExtraLevels, std::optional charge) const { const auto u = Resolve(); if (!u || u->pSkills == nullptr) { return std::nullopt; @@ -1192,14 +1219,14 @@ std::optional Unit::GetSkillLevel(uint16_t skillId, bool includeExtraL // false -> non-charge skills only. // nullopt -> no filter (any skill). // true -> charge skills only. - for (auto* skill = u->pSkills->pFirstSkill; skill != nullptr; skill = skill->pNextSkill) { - if (skill->pSkillsTxt == nullptr || static_cast(skill->pSkillsTxt->nSkillId) != skillId) { + for (auto* entry = u->pSkills->pFirstSkill; entry != nullptr; entry = entry->pNextSkill) { + if (entry->pSkillsTxt == nullptr || SkillOf(entry) != skill) { continue; } - if (charge.has_value() && IsChargeSkill(skill) != charge.value()) { + if (charge.has_value() && IsChargeSkill(entry) != charge.value()) { continue; } - return lod114d::imports::d2common::SKILLS_GetSkillLevel(u, skill, includeExtraLevels); + return lod114d::imports::d2common::SKILLS_GetSkillLevel(u, entry, includeExtraLevels); } return std::nullopt; } @@ -1276,8 +1303,8 @@ bool Unit::Interact() const { return ClickMapAt(0U, false, *this); } -bool Unit::TakeWaypoint(uint32_t waypointId) const { - if (!IsWaypointLevel(waypointId)) { +bool Unit::TakeWaypoint(LevelId level) const { + if (!IsWaypointLevel(level)) { return false; } @@ -1287,9 +1314,8 @@ bool Unit::TakeWaypoint(uint32_t waypointId) const { } // Reference JSUnit.cpp:868 calls `D2CLIENT_TakeWaypoint(pUnit->dwUnitId, // nWaypointID)` where the first arg is the waypoint object's id and the - // second is the destination area. The framework's `waypointId` param is - // the destination-area value (matches reference's nWaypointID). - lod114d::asm_thunks::TakeWaypoint(u->dwUnitId, waypointId); + // second is the destination area (reference's nWaypointID). + lod114d::asm_thunks::TakeWaypoint(u->dwUnitId, std::to_underlying(level)); // Reference JSUnit.cpp:869-870: when the in-game UI didn't pop back open // (e.g. because the destination is the same act and the menu state stayed @@ -1351,7 +1377,7 @@ ClickResult Unit::EquipItem() const { }); } -bool Unit::UseMenu(uint32_t menuId) const { +bool Unit::UseMenu(uint16_t menuId) const { const auto u = Resolve(); if (!u) { return false; @@ -1467,7 +1493,7 @@ bool Unit::Shop(ShopMode mode) const { return true; } -bool Unit::SetSkill(uint16_t skillId, Hand hand, std::optional itemId) const { +bool Unit::SetSkill(Skill skill, Hand hand, std::optional itemId) const { // Hold the read lock across the pSkills linked-list walk below - without // it the chain could be mutated by the game thread between iterations. GameReadLock guard; @@ -1485,11 +1511,11 @@ bool Unit::SetSkill(uint16_t skillId, Hand hand, std::optional itemId) // while the +to-skills entry returns the real total. Stop on the first // entry that yields a non-zero level. uint32_t skillLevel = 0; - for (auto* skill = player->pSkills->pFirstSkill; skill != nullptr; skill = skill->pNextSkill) { - if (skill->pSkillsTxt == nullptr || static_cast(skill->pSkillsTxt->nSkillId) != skillId) { + for (auto* entry = player->pSkills->pFirstSkill; entry != nullptr; entry = entry->pNextSkill) { + if (entry->pSkillsTxt == nullptr || SkillOf(entry) != skill) { continue; } - skillLevel = lod114d::imports::d2common::SKILLS_GetSkillLevel(player, skill, true); + skillLevel = lod114d::imports::d2common::SKILLS_GetSkillLevel(player, entry, true); if (skillLevel != 0) { break; } @@ -1504,7 +1530,7 @@ bool Unit::SetSkill(uint16_t skillId, Hand hand, std::optional itemId) // Fire-and-forget: the V8 binding polls GetSkillId() for the confirmation. D2GSPacketClt3C packet{}; packet.nHeader = 0x3CU; - packet.nSkill = skillId; + packet.nSkill = std::to_underlying(skill); packet.nMode = (hand == Hand::Left) ? 0x8000U : 0U; packet.dwFlags = itemId.value_or(D2UnitInvalidGUID); lod114d::imports::d2net::CLIENT_Send(sizeof(packet), 1U, reinterpret_cast(&packet)); diff --git a/src/backends/lod114d/imports/D2Client.h b/src/backends/lod114d/imports/D2Client.h index adac2ad..82a4364 100644 --- a/src/backends/lod114d/imports/D2Client.h +++ b/src/backends/lod114d/imports/D2Client.h @@ -171,11 +171,10 @@ inline GameVar gnMercReviveCost{0x3C0DD0}; inline GameVar gServerSideUnitHashTables{0x3A5E70}; inline GameVar gClientSideUnitHashTables{0x3A5270}; -// Signed: framework `GoldAction(GoldActionMode, int32_t amount)` takes int32 -// amount. The IDA decompile types this as signed; the game only ever writes -// non-negative values so the bit pattern matches reference's DWORD VARPTR. -inline GameVar gnGoldDialogAction{0x3A279C}; -inline GameVar gnGoldDialogAmount{0x3A2A68}; +// DWORDs, as reference D2Ptrs.h declares them; the action holds the dialog's +// GoldActionMode code. +inline GameVar gnGoldDialogAction{0x3A279C}; +inline GameVar gnGoldDialogAmount{0x3A2A68}; inline GameVar gNPCMenu{0x326C48}; inline GameVar gnNPCMenuAmount{0x325A74}; @@ -192,7 +191,7 @@ inline GameVar gnShopCursorType{0x3BCBF0}; inline GameVar gnPing{0x3A04A4}; inline GameVar gnFPS{0x3BB390}; -inline GameVar gnLang{0x3BB5DC}; +inline GameVar gnLang{0x3BB5DC}; inline GameVar gnOverheadTrigger{0x3BF20E}; @@ -237,7 +236,7 @@ inline GameVar> gwszRecentTradeName{0x3C0E84}; inline GameVar gbExpCharFlag{0x3A04F4}; inline GameVar gnMapId{0x3A0638}; -inline GameVar gbAlwaysRun{0x3A0660}; +inline GameVar gbAlwaysRun{0x3A0660}; inline GameVar gbNoPickUp{0x3A6A90}; inline GameVar> gwszChatMsg{0x3BB638}; diff --git a/src/backends/lod114d/imports/D2Common.h b/src/backends/lod114d/imports/D2Common.h index 002f885..34bb874 100644 --- a/src/backends/lod114d/imports/D2Common.h +++ b/src/backends/lod114d/imports/D2Common.h @@ -61,16 +61,16 @@ inline FastcallFunc DRLG_GetLayer{0x21E470}; inline FastcallFunc DRLG_GetLevel{0x242AE0}; // MOO: STATLIST_GetStatListFromUnitStateAndFlag(D2UnitStrc* pUnit, int nState, int nFlag). -inline StdcallFunc +inline StdcallFunc STATLIST_GetStatListFromUnitStateAndFlag{0x2257D0}; inline StdcallFunc STATLIST_CopyStats{0x225C90}; // MOO: STATLIST_UnitGetStatValue(const D2UnitStrc* pUnit, int nStatId, uint16_t dwLayer). // On 1.14d the body reads `dwLayer` but doesn't act on it; the stdcall slot is // still required to match the ABI. -inline StdcallFunc STATLIST_UnitGetStatValue{ - 0x225480}; -inline StdcallFunc STATES_CheckState{0x239DF0}; +inline StdcallFunc + STATLIST_UnitGetStatValue{0x225480}; +inline StdcallFunc STATES_CheckState{0x239DF0}; inline StdcallFunc UNITS_TestCollisionWithUnit{0x222AA0}; @@ -138,7 +138,7 @@ inline StdcallFunc +inline StdcallFunc QUESTRECORD_GetQuestFlag{0x25C310}; // Coordinate transforms: the in/out args are two contiguous int32_t cells (X diff --git a/src/contract/ContractEnumNames.cpp b/src/contract/ContractEnumNames.cpp index b1b6f23..9a43f0f 100644 --- a/src/contract/ContractEnumNames.cpp +++ b/src/contract/ContractEnumNames.cpp @@ -433,6 +433,1074 @@ std::string EnumName(LoginStatus value) { return utils::NameEnumValue("LoginStatus", ENTRIES, std::to_underlying(value)); } +std::string EnumName(Stat value) { + static constexpr auto ENTRIES = std::to_array({ + {.bits = utils::EnumBits(Stat::Strength), .name = "Strength"}, + {.bits = utils::EnumBits(Stat::Energy), .name = "Energy"}, + {.bits = utils::EnumBits(Stat::Dexterity), .name = "Dexterity"}, + {.bits = utils::EnumBits(Stat::Vitality), .name = "Vitality"}, + {.bits = utils::EnumBits(Stat::StatPoints), .name = "StatPoints"}, + {.bits = utils::EnumBits(Stat::NewSkills), .name = "NewSkills"}, + {.bits = utils::EnumBits(Stat::HitPoints), .name = "HitPoints"}, + {.bits = utils::EnumBits(Stat::MaxHitPoints), .name = "MaxHitPoints"}, + {.bits = utils::EnumBits(Stat::Mana), .name = "Mana"}, + {.bits = utils::EnumBits(Stat::MaxMana), .name = "MaxMana"}, + {.bits = utils::EnumBits(Stat::Stamina), .name = "Stamina"}, + {.bits = utils::EnumBits(Stat::MaxStamina), .name = "MaxStamina"}, + {.bits = utils::EnumBits(Stat::Level), .name = "Level"}, + {.bits = utils::EnumBits(Stat::Experience), .name = "Experience"}, + {.bits = utils::EnumBits(Stat::Gold), .name = "Gold"}, + {.bits = utils::EnumBits(Stat::GoldBank), .name = "GoldBank"}, + {.bits = utils::EnumBits(Stat::ItemArmorPercent), .name = "ItemArmorPercent"}, + {.bits = utils::EnumBits(Stat::ItemMaxDamagePercent), .name = "ItemMaxDamagePercent"}, + {.bits = utils::EnumBits(Stat::ItemMinDamagePercent), .name = "ItemMinDamagePercent"}, + {.bits = utils::EnumBits(Stat::ToHit), .name = "ToHit"}, + {.bits = utils::EnumBits(Stat::ToBlock), .name = "ToBlock"}, + {.bits = utils::EnumBits(Stat::MinDamage), .name = "MinDamage"}, + {.bits = utils::EnumBits(Stat::MaxDamage), .name = "MaxDamage"}, + {.bits = utils::EnumBits(Stat::SecondaryMinDamage), .name = "SecondaryMinDamage"}, + {.bits = utils::EnumBits(Stat::SecondaryMaxDamage), .name = "SecondaryMaxDamage"}, + {.bits = utils::EnumBits(Stat::DamagePercent), .name = "DamagePercent"}, + {.bits = utils::EnumBits(Stat::ManaRecovery), .name = "ManaRecovery"}, + {.bits = utils::EnumBits(Stat::ManaRecoveryBonus), .name = "ManaRecoveryBonus"}, + {.bits = utils::EnumBits(Stat::StaminaRecoveryBonus), .name = "StaminaRecoveryBonus"}, + {.bits = utils::EnumBits(Stat::LastExperience), .name = "LastExperience"}, + {.bits = utils::EnumBits(Stat::NextExperience), .name = "NextExperience"}, + {.bits = utils::EnumBits(Stat::ArmorClass), .name = "ArmorClass"}, + {.bits = utils::EnumBits(Stat::ArmorClassVsMissile), .name = "ArmorClassVsMissile"}, + {.bits = utils::EnumBits(Stat::ArmorClassVsHandToHand), .name = "ArmorClassVsHandToHand"}, + {.bits = utils::EnumBits(Stat::NormalDamageReduction), .name = "NormalDamageReduction"}, + {.bits = utils::EnumBits(Stat::MagicDamageReduction), .name = "MagicDamageReduction"}, + {.bits = utils::EnumBits(Stat::DamageResist), .name = "DamageResist"}, + {.bits = utils::EnumBits(Stat::MagicResist), .name = "MagicResist"}, + {.bits = utils::EnumBits(Stat::MaxMagicResist), .name = "MaxMagicResist"}, + {.bits = utils::EnumBits(Stat::FireResist), .name = "FireResist"}, + {.bits = utils::EnumBits(Stat::MaxFireResist), .name = "MaxFireResist"}, + {.bits = utils::EnumBits(Stat::LightningResist), .name = "LightningResist"}, + {.bits = utils::EnumBits(Stat::MaxLightningResist), .name = "MaxLightningResist"}, + {.bits = utils::EnumBits(Stat::ColdResist), .name = "ColdResist"}, + {.bits = utils::EnumBits(Stat::MaxColdResist), .name = "MaxColdResist"}, + {.bits = utils::EnumBits(Stat::PoisonResist), .name = "PoisonResist"}, + {.bits = utils::EnumBits(Stat::MaxPoisonResist), .name = "MaxPoisonResist"}, + {.bits = utils::EnumBits(Stat::DamageAura), .name = "DamageAura"}, + {.bits = utils::EnumBits(Stat::FireMinDamage), .name = "FireMinDamage"}, + {.bits = utils::EnumBits(Stat::FireMaxDamage), .name = "FireMaxDamage"}, + {.bits = utils::EnumBits(Stat::LightningMinDamage), .name = "LightningMinDamage"}, + {.bits = utils::EnumBits(Stat::LightningMaxDamage), .name = "LightningMaxDamage"}, + {.bits = utils::EnumBits(Stat::MagicMinDamage), .name = "MagicMinDamage"}, + {.bits = utils::EnumBits(Stat::MagicMaxDamage), .name = "MagicMaxDamage"}, + {.bits = utils::EnumBits(Stat::ColdMinDamage), .name = "ColdMinDamage"}, + {.bits = utils::EnumBits(Stat::ColdMaxDamage), .name = "ColdMaxDamage"}, + {.bits = utils::EnumBits(Stat::ColdLength), .name = "ColdLength"}, + {.bits = utils::EnumBits(Stat::PoisonMinDamage), .name = "PoisonMinDamage"}, + {.bits = utils::EnumBits(Stat::PoisonMaxDamage), .name = "PoisonMaxDamage"}, + {.bits = utils::EnumBits(Stat::PoisonLength), .name = "PoisonLength"}, + {.bits = utils::EnumBits(Stat::LifeDrainMinDamage), .name = "LifeDrainMinDamage"}, + {.bits = utils::EnumBits(Stat::LifeDrainMaxDamage), .name = "LifeDrainMaxDamage"}, + {.bits = utils::EnumBits(Stat::ManaDrainMinDamage), .name = "ManaDrainMinDamage"}, + {.bits = utils::EnumBits(Stat::ManaDrainMaxDamage), .name = "ManaDrainMaxDamage"}, + {.bits = utils::EnumBits(Stat::StaminaDrainMinDamage), .name = "StaminaDrainMinDamage"}, + {.bits = utils::EnumBits(Stat::StaminaDrainMaxDamage), .name = "StaminaDrainMaxDamage"}, + {.bits = utils::EnumBits(Stat::StunLength), .name = "StunLength"}, + {.bits = utils::EnumBits(Stat::VelocityPercent), .name = "VelocityPercent"}, + {.bits = utils::EnumBits(Stat::AttackRate), .name = "AttackRate"}, + {.bits = utils::EnumBits(Stat::OtherAnimRate), .name = "OtherAnimRate"}, + {.bits = utils::EnumBits(Stat::Quantity), .name = "Quantity"}, + {.bits = utils::EnumBits(Stat::Value), .name = "Value"}, + {.bits = utils::EnumBits(Stat::Durability), .name = "Durability"}, + {.bits = utils::EnumBits(Stat::MaxDurability), .name = "MaxDurability"}, + {.bits = utils::EnumBits(Stat::HpRegen), .name = "HpRegen"}, + {.bits = utils::EnumBits(Stat::ItemMaxDurabilityPercent), .name = "ItemMaxDurabilityPercent"}, + {.bits = utils::EnumBits(Stat::ItemMaxHpPercent), .name = "ItemMaxHpPercent"}, + {.bits = utils::EnumBits(Stat::ItemMaxManaPercent), .name = "ItemMaxManaPercent"}, + {.bits = utils::EnumBits(Stat::ItemAttackerTakesDamage), .name = "ItemAttackerTakesDamage"}, + {.bits = utils::EnumBits(Stat::ItemGoldBonus), .name = "ItemGoldBonus"}, + {.bits = utils::EnumBits(Stat::ItemMagicBonus), .name = "ItemMagicBonus"}, + {.bits = utils::EnumBits(Stat::ItemKnockback), .name = "ItemKnockback"}, + {.bits = utils::EnumBits(Stat::ItemTimeDuration), .name = "ItemTimeDuration"}, + {.bits = utils::EnumBits(Stat::ItemAddClassSkills), .name = "ItemAddClassSkills"}, + {.bits = utils::EnumBits(Stat::UnsentParam1), .name = "UnsentParam1"}, + {.bits = utils::EnumBits(Stat::ItemAddExperience), .name = "ItemAddExperience"}, + {.bits = utils::EnumBits(Stat::ItemHealAfterKill), .name = "ItemHealAfterKill"}, + {.bits = utils::EnumBits(Stat::ItemReducedPrices), .name = "ItemReducedPrices"}, + {.bits = utils::EnumBits(Stat::ItemDoubleHerbDuration), .name = "ItemDoubleHerbDuration"}, + {.bits = utils::EnumBits(Stat::ItemLightRadius), .name = "ItemLightRadius"}, + {.bits = utils::EnumBits(Stat::ItemLightColor), .name = "ItemLightColor"}, + {.bits = utils::EnumBits(Stat::ItemReqPercent), .name = "ItemReqPercent"}, + {.bits = utils::EnumBits(Stat::ItemLevelReq), .name = "ItemLevelReq"}, + {.bits = utils::EnumBits(Stat::ItemFasterAttackRate), .name = "ItemFasterAttackRate"}, + {.bits = utils::EnumBits(Stat::ItemLevelReqPercent), .name = "ItemLevelReqPercent"}, + {.bits = utils::EnumBits(Stat::LastBlockFrame), .name = "LastBlockFrame"}, + {.bits = utils::EnumBits(Stat::ItemFasterMoveVelocity), .name = "ItemFasterMoveVelocity"}, + {.bits = utils::EnumBits(Stat::ItemNonClassSkill), .name = "ItemNonClassSkill"}, + {.bits = utils::EnumBits(Stat::State), .name = "State"}, + {.bits = utils::EnumBits(Stat::ItemFasterGetHitRate), .name = "ItemFasterGetHitRate"}, + {.bits = utils::EnumBits(Stat::MonsterPlayerCount), .name = "MonsterPlayerCount"}, + {.bits = utils::EnumBits(Stat::SkillPoisonOverrideLength), .name = "SkillPoisonOverrideLength"}, + {.bits = utils::EnumBits(Stat::ItemFasterBlockRate), .name = "ItemFasterBlockRate"}, + {.bits = utils::EnumBits(Stat::SkillBypassUndead), .name = "SkillBypassUndead"}, + {.bits = utils::EnumBits(Stat::SkillBypassDemons), .name = "SkillBypassDemons"}, + {.bits = utils::EnumBits(Stat::ItemFasterCastRate), .name = "ItemFasterCastRate"}, + {.bits = utils::EnumBits(Stat::SkillBypassBeasts), .name = "SkillBypassBeasts"}, + {.bits = utils::EnumBits(Stat::ItemSingleSkill), .name = "ItemSingleSkill"}, + {.bits = utils::EnumBits(Stat::ItemRestInPeace), .name = "ItemRestInPeace"}, + {.bits = utils::EnumBits(Stat::CurseResistance), .name = "CurseResistance"}, + {.bits = utils::EnumBits(Stat::ItemPoisonLengthResist), .name = "ItemPoisonLengthResist"}, + {.bits = utils::EnumBits(Stat::ItemNormalDamage), .name = "ItemNormalDamage"}, + {.bits = utils::EnumBits(Stat::ItemHowl), .name = "ItemHowl"}, + {.bits = utils::EnumBits(Stat::ItemStupidity), .name = "ItemStupidity"}, + {.bits = utils::EnumBits(Stat::ItemDamageToMana), .name = "ItemDamageToMana"}, + {.bits = utils::EnumBits(Stat::ItemIgnoreTargetAc), .name = "ItemIgnoreTargetAc"}, + {.bits = utils::EnumBits(Stat::ItemFractionalTargetAc), .name = "ItemFractionalTargetAc"}, + {.bits = utils::EnumBits(Stat::ItemPreventHeal), .name = "ItemPreventHeal"}, + {.bits = utils::EnumBits(Stat::ItemHalfFreezeDuration), .name = "ItemHalfFreezeDuration"}, + {.bits = utils::EnumBits(Stat::ItemToHitPercent), .name = "ItemToHitPercent"}, + {.bits = utils::EnumBits(Stat::ItemDamageTargetAc), .name = "ItemDamageTargetAc"}, + {.bits = utils::EnumBits(Stat::ItemDemonDamagePercent), .name = "ItemDemonDamagePercent"}, + {.bits = utils::EnumBits(Stat::ItemUndeadDamagePercent), .name = "ItemUndeadDamagePercent"}, + {.bits = utils::EnumBits(Stat::ItemDemonToHit), .name = "ItemDemonToHit"}, + {.bits = utils::EnumBits(Stat::ItemUndeadToHit), .name = "ItemUndeadToHit"}, + {.bits = utils::EnumBits(Stat::ItemThrowable), .name = "ItemThrowable"}, + {.bits = utils::EnumBits(Stat::ItemElemSkill), .name = "ItemElemSkill"}, + {.bits = utils::EnumBits(Stat::ItemAllSkills), .name = "ItemAllSkills"}, + {.bits = utils::EnumBits(Stat::ItemAttackerTakesLightDamage), .name = "ItemAttackerTakesLightDamage"}, + {.bits = utils::EnumBits(Stat::IronMaidenLevel), .name = "IronMaidenLevel"}, + {.bits = utils::EnumBits(Stat::LifeTapLevel), .name = "LifeTapLevel"}, + {.bits = utils::EnumBits(Stat::ThornsPercent), .name = "ThornsPercent"}, + {.bits = utils::EnumBits(Stat::BoneArmor), .name = "BoneArmor"}, + {.bits = utils::EnumBits(Stat::BoneArmorMax), .name = "BoneArmorMax"}, + {.bits = utils::EnumBits(Stat::ItemFreeze), .name = "ItemFreeze"}, + {.bits = utils::EnumBits(Stat::ItemOpenWounds), .name = "ItemOpenWounds"}, + {.bits = utils::EnumBits(Stat::ItemCrushingBlow), .name = "ItemCrushingBlow"}, + {.bits = utils::EnumBits(Stat::ItemKickDamage), .name = "ItemKickDamage"}, + {.bits = utils::EnumBits(Stat::ItemManaAfterKill), .name = "ItemManaAfterKill"}, + {.bits = utils::EnumBits(Stat::ItemHealAfterDemonKill), .name = "ItemHealAfterDemonKill"}, + {.bits = utils::EnumBits(Stat::ItemExtraBlood), .name = "ItemExtraBlood"}, + {.bits = utils::EnumBits(Stat::ItemDeadlyStrike), .name = "ItemDeadlyStrike"}, + {.bits = utils::EnumBits(Stat::ItemAbsorbFirePercent), .name = "ItemAbsorbFirePercent"}, + {.bits = utils::EnumBits(Stat::ItemAbsorbFire), .name = "ItemAbsorbFire"}, + {.bits = utils::EnumBits(Stat::ItemAbsorbLightningPercent), .name = "ItemAbsorbLightningPercent"}, + {.bits = utils::EnumBits(Stat::ItemAbsorbLightning), .name = "ItemAbsorbLightning"}, + {.bits = utils::EnumBits(Stat::ItemAbsorbMagicPercent), .name = "ItemAbsorbMagicPercent"}, + {.bits = utils::EnumBits(Stat::ItemAbsorbMagic), .name = "ItemAbsorbMagic"}, + {.bits = utils::EnumBits(Stat::ItemAbsorbColdPercent), .name = "ItemAbsorbColdPercent"}, + {.bits = utils::EnumBits(Stat::ItemAbsorbCold), .name = "ItemAbsorbCold"}, + {.bits = utils::EnumBits(Stat::ItemSlow), .name = "ItemSlow"}, + {.bits = utils::EnumBits(Stat::ItemAura), .name = "ItemAura"}, + {.bits = utils::EnumBits(Stat::ItemIndestructible), .name = "ItemIndestructible"}, + {.bits = utils::EnumBits(Stat::ItemCannotBeFrozen), .name = "ItemCannotBeFrozen"}, + {.bits = utils::EnumBits(Stat::ItemStaminaDrainPercent), .name = "ItemStaminaDrainPercent"}, + {.bits = utils::EnumBits(Stat::ItemReanimate), .name = "ItemReanimate"}, + {.bits = utils::EnumBits(Stat::ItemPierce), .name = "ItemPierce"}, + {.bits = utils::EnumBits(Stat::ItemMagicArrow), .name = "ItemMagicArrow"}, + {.bits = utils::EnumBits(Stat::ItemExplosiveArrow), .name = "ItemExplosiveArrow"}, + {.bits = utils::EnumBits(Stat::ItemThrowMinDamage), .name = "ItemThrowMinDamage"}, + {.bits = utils::EnumBits(Stat::ItemThrowMaxDamage), .name = "ItemThrowMaxDamage"}, + {.bits = utils::EnumBits(Stat::SkillHandOfAthena), .name = "SkillHandOfAthena"}, + {.bits = utils::EnumBits(Stat::SkillStaminaPercent), .name = "SkillStaminaPercent"}, + {.bits = utils::EnumBits(Stat::SkillPassiveStaminaPercent), .name = "SkillPassiveStaminaPercent"}, + {.bits = utils::EnumBits(Stat::SkillConcentration), .name = "SkillConcentration"}, + {.bits = utils::EnumBits(Stat::SkillEnchant), .name = "SkillEnchant"}, + {.bits = utils::EnumBits(Stat::SkillPierce), .name = "SkillPierce"}, + {.bits = utils::EnumBits(Stat::SkillConviction), .name = "SkillConviction"}, + {.bits = utils::EnumBits(Stat::SkillChillingArmor), .name = "SkillChillingArmor"}, + {.bits = utils::EnumBits(Stat::SkillFrenzy), .name = "SkillFrenzy"}, + {.bits = utils::EnumBits(Stat::SkillDecrepify), .name = "SkillDecrepify"}, + {.bits = utils::EnumBits(Stat::SkillArmorPercent), .name = "SkillArmorPercent"}, + {.bits = utils::EnumBits(Stat::Alignment), .name = "Alignment"}, + {.bits = utils::EnumBits(Stat::Target0), .name = "Target0"}, + {.bits = utils::EnumBits(Stat::Target1), .name = "Target1"}, + {.bits = utils::EnumBits(Stat::GoldLost), .name = "GoldLost"}, + {.bits = utils::EnumBits(Stat::ConversionLevel), .name = "ConversionLevel"}, + {.bits = utils::EnumBits(Stat::ConversionMaxHp), .name = "ConversionMaxHp"}, + {.bits = utils::EnumBits(Stat::UnitDoOverlay), .name = "UnitDoOverlay"}, + {.bits = utils::EnumBits(Stat::AttackVsMonsterType), .name = "AttackVsMonsterType"}, + {.bits = utils::EnumBits(Stat::DamageVsMonsterType), .name = "DamageVsMonsterType"}, + {.bits = utils::EnumBits(Stat::Fade), .name = "Fade"}, + {.bits = utils::EnumBits(Stat::ArmorOverridePercent), .name = "ArmorOverridePercent"}, + {.bits = utils::EnumBits(Stat::LastHitReactFrame), .name = "LastHitReactFrame"}, + {.bits = utils::EnumBits(Stat::CreateSeason), .name = "CreateSeason"}, + {.bits = utils::EnumBits(Stat::BonusMinDamage), .name = "BonusMinDamage"}, + {.bits = utils::EnumBits(Stat::BonusMaxDamage), .name = "BonusMaxDamage"}, + {.bits = utils::EnumBits(Stat::ItemPierceColdImmunity), .name = "ItemPierceColdImmunity"}, + {.bits = utils::EnumBits(Stat::ItemAddSkillTab), .name = "ItemAddSkillTab"}, + {.bits = utils::EnumBits(Stat::ItemPierceFireImmunity), .name = "ItemPierceFireImmunity"}, + {.bits = utils::EnumBits(Stat::ItemPierceLightningImmunity), .name = "ItemPierceLightningImmunity"}, + {.bits = utils::EnumBits(Stat::ItemPiercePoisonImmunity), .name = "ItemPiercePoisonImmunity"}, + {.bits = utils::EnumBits(Stat::ItemPierceDamageImmunity), .name = "ItemPierceDamageImmunity"}, + {.bits = utils::EnumBits(Stat::ItemPierceMagicImmunity), .name = "ItemPierceMagicImmunity"}, + {.bits = utils::EnumBits(Stat::ItemNumSockets), .name = "ItemNumSockets"}, + {.bits = utils::EnumBits(Stat::ItemSkillOnAttack), .name = "ItemSkillOnAttack"}, + {.bits = utils::EnumBits(Stat::ItemSkillOnKill), .name = "ItemSkillOnKill"}, + {.bits = utils::EnumBits(Stat::ItemSkillOnDeath), .name = "ItemSkillOnDeath"}, + {.bits = utils::EnumBits(Stat::ItemSkillOnHit), .name = "ItemSkillOnHit"}, + {.bits = utils::EnumBits(Stat::ItemSkillOnLevelUp), .name = "ItemSkillOnLevelUp"}, + {.bits = utils::EnumBits(Stat::ItemChargeNoConsume), .name = "ItemChargeNoConsume"}, + {.bits = utils::EnumBits(Stat::ItemSkillOnGetHit), .name = "ItemSkillOnGetHit"}, + {.bits = utils::EnumBits(Stat::ModifierListCastId), .name = "ModifierListCastId"}, + {.bits = utils::EnumBits(Stat::PassiveMasteryItemReqPercent), .name = "PassiveMasteryItemReqPercent"}, + {.bits = utils::EnumBits(Stat::ItemChargedSkill), .name = "ItemChargedSkill"}, + {.bits = utils::EnumBits(Stat::ItemNoConsume), .name = "ItemNoConsume"}, + {.bits = utils::EnumBits(Stat::PassiveMasteryNoConsume), .name = "PassiveMasteryNoConsume"}, + {.bits = utils::EnumBits(Stat::PassiveMasteryReplenishOnCrit), .name = "PassiveMasteryReplenishOnCrit"}, + {.bits = utils::EnumBits(Stat::MissileThornsPercent), .name = "MissileThornsPercent"}, + {.bits = utils::EnumBits(Stat::PassiveMasteryItemLevelReqPercent), .name = "PassiveMasteryItemLevelReqPercent"}, + {.bits = utils::EnumBits(Stat::UberAncientsEscalation), .name = "UberAncientsEscalation"}, + {.bits = utils::EnumBits(Stat::UberAncientsDefeated), .name = "UberAncientsDefeated"}, + {.bits = utils::EnumBits(Stat::PassiveMasteryGetHitRate), .name = "PassiveMasteryGetHitRate"}, + {.bits = utils::EnumBits(Stat::PassiveMasteryAttackSpeed), .name = "PassiveMasteryAttackSpeed"}, + {.bits = utils::EnumBits(Stat::ItemArmorPerLevel), .name = "ItemArmorPerLevel"}, + {.bits = utils::EnumBits(Stat::ItemArmorPercentPerLevel), .name = "ItemArmorPercentPerLevel"}, + {.bits = utils::EnumBits(Stat::ItemHpPerLevel), .name = "ItemHpPerLevel"}, + {.bits = utils::EnumBits(Stat::ItemManaPerLevel), .name = "ItemManaPerLevel"}, + {.bits = utils::EnumBits(Stat::ItemMaxDamagePerLevel), .name = "ItemMaxDamagePerLevel"}, + {.bits = utils::EnumBits(Stat::ItemMaxDamagePercentPerLevel), .name = "ItemMaxDamagePercentPerLevel"}, + {.bits = utils::EnumBits(Stat::ItemStrengthPerLevel), .name = "ItemStrengthPerLevel"}, + {.bits = utils::EnumBits(Stat::ItemDexterityPerLevel), .name = "ItemDexterityPerLevel"}, + {.bits = utils::EnumBits(Stat::ItemEnergyPerLevel), .name = "ItemEnergyPerLevel"}, + {.bits = utils::EnumBits(Stat::ItemVitalityPerLevel), .name = "ItemVitalityPerLevel"}, + {.bits = utils::EnumBits(Stat::ItemToHitPerLevel), .name = "ItemToHitPerLevel"}, + {.bits = utils::EnumBits(Stat::ItemToHitPercentPerLevel), .name = "ItemToHitPercentPerLevel"}, + {.bits = utils::EnumBits(Stat::ItemColdDamageMaxPerLevel), .name = "ItemColdDamageMaxPerLevel"}, + {.bits = utils::EnumBits(Stat::ItemFireDamageMaxPerLevel), .name = "ItemFireDamageMaxPerLevel"}, + {.bits = utils::EnumBits(Stat::ItemLightningDamageMaxPerLevel), .name = "ItemLightningDamageMaxPerLevel"}, + {.bits = utils::EnumBits(Stat::ItemPoisonDamageMaxPerLevel), .name = "ItemPoisonDamageMaxPerLevel"}, + {.bits = utils::EnumBits(Stat::ItemResistColdPerLevel), .name = "ItemResistColdPerLevel"}, + {.bits = utils::EnumBits(Stat::ItemResistFirePerLevel), .name = "ItemResistFirePerLevel"}, + {.bits = utils::EnumBits(Stat::ItemResistLightningPerLevel), .name = "ItemResistLightningPerLevel"}, + {.bits = utils::EnumBits(Stat::ItemResistPoisonPerLevel), .name = "ItemResistPoisonPerLevel"}, + {.bits = utils::EnumBits(Stat::ItemAbsorbColdPerLevel), .name = "ItemAbsorbColdPerLevel"}, + {.bits = utils::EnumBits(Stat::ItemAbsorbFirePerLevel), .name = "ItemAbsorbFirePerLevel"}, + {.bits = utils::EnumBits(Stat::ItemAbsorbLightningPerLevel), .name = "ItemAbsorbLightningPerLevel"}, + {.bits = utils::EnumBits(Stat::ItemAbsorbPoisonPerLevel), .name = "ItemAbsorbPoisonPerLevel"}, + {.bits = utils::EnumBits(Stat::ItemThornsPerLevel), .name = "ItemThornsPerLevel"}, + {.bits = utils::EnumBits(Stat::ItemFindGoldPerLevel), .name = "ItemFindGoldPerLevel"}, + {.bits = utils::EnumBits(Stat::ItemFindMagicPerLevel), .name = "ItemFindMagicPerLevel"}, + {.bits = utils::EnumBits(Stat::ItemRegenStaminaPerLevel), .name = "ItemRegenStaminaPerLevel"}, + {.bits = utils::EnumBits(Stat::ItemStaminaPerLevel), .name = "ItemStaminaPerLevel"}, + {.bits = utils::EnumBits(Stat::ItemDamageDemonPerLevel), .name = "ItemDamageDemonPerLevel"}, + {.bits = utils::EnumBits(Stat::ItemDamageUndeadPerLevel), .name = "ItemDamageUndeadPerLevel"}, + {.bits = utils::EnumBits(Stat::ItemToHitDemonPerLevel), .name = "ItemToHitDemonPerLevel"}, + {.bits = utils::EnumBits(Stat::ItemToHitUndeadPerLevel), .name = "ItemToHitUndeadPerLevel"}, + {.bits = utils::EnumBits(Stat::ItemCrushingBlowPerLevel), .name = "ItemCrushingBlowPerLevel"}, + {.bits = utils::EnumBits(Stat::ItemOpenWoundsPerLevel), .name = "ItemOpenWoundsPerLevel"}, + {.bits = utils::EnumBits(Stat::ItemKickDamagePerLevel), .name = "ItemKickDamagePerLevel"}, + {.bits = utils::EnumBits(Stat::ItemDeadlyStrikePerLevel), .name = "ItemDeadlyStrikePerLevel"}, + {.bits = utils::EnumBits(Stat::ItemFindGemsPerLevel), .name = "ItemFindGemsPerLevel"}, + {.bits = utils::EnumBits(Stat::ItemReplenishDurability), .name = "ItemReplenishDurability"}, + {.bits = utils::EnumBits(Stat::ItemReplenishQuantity), .name = "ItemReplenishQuantity"}, + {.bits = utils::EnumBits(Stat::ItemExtraStack), .name = "ItemExtraStack"}, + {.bits = utils::EnumBits(Stat::ItemFindItem), .name = "ItemFindItem"}, + {.bits = utils::EnumBits(Stat::ItemSlashDamage), .name = "ItemSlashDamage"}, + {.bits = utils::EnumBits(Stat::ItemSlashDamagePercent), .name = "ItemSlashDamagePercent"}, + {.bits = utils::EnumBits(Stat::ItemCrushDamage), .name = "ItemCrushDamage"}, + {.bits = utils::EnumBits(Stat::ItemCrushDamagePercent), .name = "ItemCrushDamagePercent"}, + {.bits = utils::EnumBits(Stat::ItemThrustDamage), .name = "ItemThrustDamage"}, + {.bits = utils::EnumBits(Stat::ItemThrustDamagePercent), .name = "ItemThrustDamagePercent"}, + {.bits = utils::EnumBits(Stat::ItemAbsorbSlash), .name = "ItemAbsorbSlash"}, + {.bits = utils::EnumBits(Stat::ItemAbsorbCrush), .name = "ItemAbsorbCrush"}, + {.bits = utils::EnumBits(Stat::ItemAbsorbThrust), .name = "ItemAbsorbThrust"}, + {.bits = utils::EnumBits(Stat::ItemAbsorbSlashPercent), .name = "ItemAbsorbSlashPercent"}, + {.bits = utils::EnumBits(Stat::ItemAbsorbCrushPercent), .name = "ItemAbsorbCrushPercent"}, + {.bits = utils::EnumBits(Stat::ItemAbsorbThrustPercent), .name = "ItemAbsorbThrustPercent"}, + {.bits = utils::EnumBits(Stat::ItemArmorByTime), .name = "ItemArmorByTime"}, + {.bits = utils::EnumBits(Stat::ItemArmorPercentByTime), .name = "ItemArmorPercentByTime"}, + {.bits = utils::EnumBits(Stat::ItemHpByTime), .name = "ItemHpByTime"}, + {.bits = utils::EnumBits(Stat::ItemManaByTime), .name = "ItemManaByTime"}, + {.bits = utils::EnumBits(Stat::ItemMaxDamageByTime), .name = "ItemMaxDamageByTime"}, + {.bits = utils::EnumBits(Stat::ItemMaxDamagePercentByTime), .name = "ItemMaxDamagePercentByTime"}, + {.bits = utils::EnumBits(Stat::ItemStrengthByTime), .name = "ItemStrengthByTime"}, + {.bits = utils::EnumBits(Stat::ItemDexterityByTime), .name = "ItemDexterityByTime"}, + {.bits = utils::EnumBits(Stat::ItemEnergyByTime), .name = "ItemEnergyByTime"}, + {.bits = utils::EnumBits(Stat::ItemVitalityByTime), .name = "ItemVitalityByTime"}, + {.bits = utils::EnumBits(Stat::ItemToHitByTime), .name = "ItemToHitByTime"}, + {.bits = utils::EnumBits(Stat::ItemToHitPercentByTime), .name = "ItemToHitPercentByTime"}, + {.bits = utils::EnumBits(Stat::ItemColdDamageMaxByTime), .name = "ItemColdDamageMaxByTime"}, + {.bits = utils::EnumBits(Stat::ItemFireDamageMaxByTime), .name = "ItemFireDamageMaxByTime"}, + {.bits = utils::EnumBits(Stat::ItemLightningDamageMaxByTime), .name = "ItemLightningDamageMaxByTime"}, + {.bits = utils::EnumBits(Stat::ItemPoisonDamageMaxByTime), .name = "ItemPoisonDamageMaxByTime"}, + {.bits = utils::EnumBits(Stat::ItemResistColdByTime), .name = "ItemResistColdByTime"}, + {.bits = utils::EnumBits(Stat::ItemResistFireByTime), .name = "ItemResistFireByTime"}, + {.bits = utils::EnumBits(Stat::ItemResistLightningByTime), .name = "ItemResistLightningByTime"}, + {.bits = utils::EnumBits(Stat::ItemResistPoisonByTime), .name = "ItemResistPoisonByTime"}, + {.bits = utils::EnumBits(Stat::ItemAbsorbColdByTime), .name = "ItemAbsorbColdByTime"}, + {.bits = utils::EnumBits(Stat::ItemAbsorbFireByTime), .name = "ItemAbsorbFireByTime"}, + {.bits = utils::EnumBits(Stat::ItemAbsorbLightningByTime), .name = "ItemAbsorbLightningByTime"}, + {.bits = utils::EnumBits(Stat::ItemAbsorbPoisonByTime), .name = "ItemAbsorbPoisonByTime"}, + {.bits = utils::EnumBits(Stat::ItemFindGoldByTime), .name = "ItemFindGoldByTime"}, + {.bits = utils::EnumBits(Stat::ItemFindMagicByTime), .name = "ItemFindMagicByTime"}, + {.bits = utils::EnumBits(Stat::ItemRegenStaminaByTime), .name = "ItemRegenStaminaByTime"}, + {.bits = utils::EnumBits(Stat::ItemStaminaByTime), .name = "ItemStaminaByTime"}, + {.bits = utils::EnumBits(Stat::ItemDamageDemonByTime), .name = "ItemDamageDemonByTime"}, + {.bits = utils::EnumBits(Stat::ItemDamageUndeadByTime), .name = "ItemDamageUndeadByTime"}, + {.bits = utils::EnumBits(Stat::ItemToHitDemonByTime), .name = "ItemToHitDemonByTime"}, + {.bits = utils::EnumBits(Stat::ItemToHitUndeadByTime), .name = "ItemToHitUndeadByTime"}, + {.bits = utils::EnumBits(Stat::ItemCrushingBlowByTime), .name = "ItemCrushingBlowByTime"}, + {.bits = utils::EnumBits(Stat::ItemOpenWoundsByTime), .name = "ItemOpenWoundsByTime"}, + {.bits = utils::EnumBits(Stat::ItemKickDamageByTime), .name = "ItemKickDamageByTime"}, + {.bits = utils::EnumBits(Stat::ItemDeadlyStrikeByTime), .name = "ItemDeadlyStrikeByTime"}, + {.bits = utils::EnumBits(Stat::ItemFindGemsByTime), .name = "ItemFindGemsByTime"}, + {.bits = utils::EnumBits(Stat::ItemPierceCold), .name = "ItemPierceCold"}, + {.bits = utils::EnumBits(Stat::ItemPierceFire), .name = "ItemPierceFire"}, + {.bits = utils::EnumBits(Stat::ItemPierceLightning), .name = "ItemPierceLightning"}, + {.bits = utils::EnumBits(Stat::ItemPiercePoison), .name = "ItemPiercePoison"}, + {.bits = utils::EnumBits(Stat::ItemDamageVsMonster), .name = "ItemDamageVsMonster"}, + {.bits = utils::EnumBits(Stat::ItemDamagePercentVsMonster), .name = "ItemDamagePercentVsMonster"}, + {.bits = utils::EnumBits(Stat::ItemToHitVsMonster), .name = "ItemToHitVsMonster"}, + {.bits = utils::EnumBits(Stat::ItemToHitPercentVsMonster), .name = "ItemToHitPercentVsMonster"}, + {.bits = utils::EnumBits(Stat::ItemAcVsMonster), .name = "ItemAcVsMonster"}, + {.bits = utils::EnumBits(Stat::ItemAcPercentVsMonster), .name = "ItemAcPercentVsMonster"}, + {.bits = utils::EnumBits(Stat::FireLength), .name = "FireLength"}, + {.bits = utils::EnumBits(Stat::BurningMin), .name = "BurningMin"}, + {.bits = utils::EnumBits(Stat::BurningMax), .name = "BurningMax"}, + {.bits = utils::EnumBits(Stat::ProgressiveDamage), .name = "ProgressiveDamage"}, + {.bits = utils::EnumBits(Stat::ProgressiveSteal), .name = "ProgressiveSteal"}, + {.bits = utils::EnumBits(Stat::ProgressiveOther), .name = "ProgressiveOther"}, + {.bits = utils::EnumBits(Stat::ProgressiveFire), .name = "ProgressiveFire"}, + {.bits = utils::EnumBits(Stat::ProgressiveCold), .name = "ProgressiveCold"}, + {.bits = utils::EnumBits(Stat::ProgressiveLightning), .name = "ProgressiveLightning"}, + {.bits = utils::EnumBits(Stat::ItemExtraCharges), .name = "ItemExtraCharges"}, + {.bits = utils::EnumBits(Stat::ProgressiveToHit), .name = "ProgressiveToHit"}, + {.bits = utils::EnumBits(Stat::PoisonCount), .name = "PoisonCount"}, + {.bits = utils::EnumBits(Stat::DamageFrameRate), .name = "DamageFrameRate"}, + {.bits = utils::EnumBits(Stat::PierceIndex), .name = "PierceIndex"}, + {.bits = utils::EnumBits(Stat::PassiveFireMastery), .name = "PassiveFireMastery"}, + {.bits = utils::EnumBits(Stat::PassiveLightningMastery), .name = "PassiveLightningMastery"}, + {.bits = utils::EnumBits(Stat::PassiveColdMastery), .name = "PassiveColdMastery"}, + {.bits = utils::EnumBits(Stat::PassivePoisonMastery), .name = "PassivePoisonMastery"}, + {.bits = utils::EnumBits(Stat::PassiveFirePierce), .name = "PassiveFirePierce"}, + {.bits = utils::EnumBits(Stat::PassiveLightningPierce), .name = "PassiveLightningPierce"}, + {.bits = utils::EnumBits(Stat::PassiveColdPierce), .name = "PassiveColdPierce"}, + {.bits = utils::EnumBits(Stat::PassivePoisonPierce), .name = "PassivePoisonPierce"}, + {.bits = utils::EnumBits(Stat::PassiveCriticalStrike), .name = "PassiveCriticalStrike"}, + {.bits = utils::EnumBits(Stat::PassiveDodge), .name = "PassiveDodge"}, + {.bits = utils::EnumBits(Stat::PassiveAvoid), .name = "PassiveAvoid"}, + {.bits = utils::EnumBits(Stat::PassiveEvade), .name = "PassiveEvade"}, + {.bits = utils::EnumBits(Stat::PassiveWarmth), .name = "PassiveWarmth"}, + {.bits = utils::EnumBits(Stat::PassiveMasteryMeleeToHit), .name = "PassiveMasteryMeleeToHit"}, + {.bits = utils::EnumBits(Stat::PassiveMasteryMeleeDamage), .name = "PassiveMasteryMeleeDamage"}, + {.bits = utils::EnumBits(Stat::PassiveMasteryMeleeCrit), .name = "PassiveMasteryMeleeCrit"}, + {.bits = utils::EnumBits(Stat::PassiveMasteryThrowToHit), .name = "PassiveMasteryThrowToHit"}, + {.bits = utils::EnumBits(Stat::PassiveMasteryThrowDamage), .name = "PassiveMasteryThrowDamage"}, + {.bits = utils::EnumBits(Stat::PassiveMasteryThrowCrit), .name = "PassiveMasteryThrowCrit"}, + {.bits = utils::EnumBits(Stat::PassiveWeaponBlock), .name = "PassiveWeaponBlock"}, + {.bits = utils::EnumBits(Stat::PassiveSummonResist), .name = "PassiveSummonResist"}, + {.bits = utils::EnumBits(Stat::ModifierListSkill), .name = "ModifierListSkill"}, + {.bits = utils::EnumBits(Stat::ModifierListLevel), .name = "ModifierListLevel"}, + {.bits = utils::EnumBits(Stat::LastSentHitPointPercent), .name = "LastSentHitPointPercent"}, + {.bits = utils::EnumBits(Stat::SourceUnitType), .name = "SourceUnitType"}, + {.bits = utils::EnumBits(Stat::SourceUnitId), .name = "SourceUnitId"}, + {.bits = utils::EnumBits(Stat::ShortParam1), .name = "ShortParam1"}, + {.bits = utils::EnumBits(Stat::QuestItemDifficulty), .name = "QuestItemDifficulty"}, + {.bits = utils::EnumBits(Stat::PassiveMagicMastery), .name = "PassiveMagicMastery"}, + {.bits = utils::EnumBits(Stat::PassiveMagicPierce), .name = "PassiveMagicPierce"}, + {.bits = utils::EnumBits(Stat::SkillCooldown), .name = "SkillCooldown"}, + {.bits = utils::EnumBits(Stat::SkillMissileDamageScale), .name = "SkillMissileDamageScale"}, + {.bits = utils::EnumBits(Stat::PsychicWard), .name = "PsychicWard"}, + {.bits = utils::EnumBits(Stat::PsychicWardMax), .name = "PsychicWardMax"}, + {.bits = utils::EnumBits(Stat::SkillChannelingTick), .name = "SkillChannelingTick"}, + {.bits = utils::EnumBits(Stat::CustomizationIndex), .name = "CustomizationIndex"}, + {.bits = utils::EnumBits(Stat::ItemMagicDamageMaxPerLevel), .name = "ItemMagicDamageMaxPerLevel"}, + {.bits = utils::EnumBits(Stat::PassiveDamagePierce), .name = "PassiveDamagePierce"}, + {.bits = utils::EnumBits(Stat::HeraldTier), .name = "HeraldTier"}, + }); + return utils::NameEnumValue("Stat", ENTRIES, std::to_underlying(value)); +} + +std::string EnumName(StatListFlags value) { + static constexpr auto ENTRIES = std::to_array({ + {.bits = utils::EnumBits(StatListFlags::Base), .name = "Base"}, + {.bits = utils::EnumBits(StatListFlags::Basic), .name = "Basic"}, + {.bits = utils::EnumBits(StatListFlags::NewLength), .name = "NewLength"}, + {.bits = utils::EnumBits(StatListFlags::TempOnly), .name = "TempOnly"}, + {.bits = utils::EnumBits(StatListFlags::Buff), .name = "Buff"}, + {.bits = utils::EnumBits(StatListFlags::Curse), .name = "Curse"}, + {.bits = utils::EnumBits(StatListFlags::Magic), .name = "Magic"}, + {.bits = utils::EnumBits(StatListFlags::Overlay), .name = "Overlay"}, + {.bits = utils::EnumBits(StatListFlags::Unk0x100), .name = "Unk0x100"}, + {.bits = utils::EnumBits(StatListFlags::Toggle), .name = "Toggle"}, + {.bits = utils::EnumBits(StatListFlags::Convert), .name = "Convert"}, + {.bits = utils::EnumBits(StatListFlags::Set), .name = "Set"}, + {.bits = utils::EnumBits(StatListFlags::ItemEx), .name = "ItemEx"}, + {.bits = utils::EnumBits(StatListFlags::Permanent), .name = "Permanent"}, + {.bits = utils::EnumBits(StatListFlags::Dynamic), .name = "Dynamic"}, + {.bits = utils::EnumBits(StatListFlags::Extended), .name = "Extended"}, + }); + return utils::NameEnumValue("StatListFlags", ENTRIES, std::to_underlying(value)); +} + +std::string EnumName(State value) { + static constexpr auto ENTRIES = std::to_array({ + {.bits = utils::EnumBits(State::None), .name = "None"}, + {.bits = utils::EnumBits(State::Freeze), .name = "Freeze"}, + {.bits = utils::EnumBits(State::Poison), .name = "Poison"}, + {.bits = utils::EnumBits(State::ResistFire), .name = "ResistFire"}, + {.bits = utils::EnumBits(State::ResistCold), .name = "ResistCold"}, + {.bits = utils::EnumBits(State::ResistLight), .name = "ResistLight"}, + {.bits = utils::EnumBits(State::ResistMagic), .name = "ResistMagic"}, + {.bits = utils::EnumBits(State::PlayerBody), .name = "PlayerBody"}, + {.bits = utils::EnumBits(State::ResistAll), .name = "ResistAll"}, + {.bits = utils::EnumBits(State::AmplifyDamage), .name = "AmplifyDamage"}, + {.bits = utils::EnumBits(State::FrozenArmor), .name = "FrozenArmor"}, + {.bits = utils::EnumBits(State::Cold), .name = "Cold"}, + {.bits = utils::EnumBits(State::Inferno), .name = "Inferno"}, + {.bits = utils::EnumBits(State::Blaze), .name = "Blaze"}, + {.bits = utils::EnumBits(State::BoneArmor), .name = "BoneArmor"}, + {.bits = utils::EnumBits(State::Concentrate), .name = "Concentrate"}, + {.bits = utils::EnumBits(State::Enchant), .name = "Enchant"}, + {.bits = utils::EnumBits(State::InnerSight), .name = "InnerSight"}, + {.bits = utils::EnumBits(State::SkillMove), .name = "SkillMove"}, + {.bits = utils::EnumBits(State::Weaken), .name = "Weaken"}, + {.bits = utils::EnumBits(State::ChillingArmor), .name = "ChillingArmor"}, + {.bits = utils::EnumBits(State::Stunned), .name = "Stunned"}, + {.bits = utils::EnumBits(State::Spiderlay), .name = "Spiderlay"}, + {.bits = utils::EnumBits(State::DimVision), .name = "DimVision"}, + {.bits = utils::EnumBits(State::Slowed), .name = "Slowed"}, + {.bits = utils::EnumBits(State::FetishAura), .name = "FetishAura"}, + {.bits = utils::EnumBits(State::Shout), .name = "Shout"}, + {.bits = utils::EnumBits(State::Taunt), .name = "Taunt"}, + {.bits = utils::EnumBits(State::Conviction), .name = "Conviction"}, + {.bits = utils::EnumBits(State::Convicted), .name = "Convicted"}, + {.bits = utils::EnumBits(State::EnergyShield), .name = "EnergyShield"}, + {.bits = utils::EnumBits(State::VenomClaws), .name = "VenomClaws"}, + {.bits = utils::EnumBits(State::BattleOrders), .name = "BattleOrders"}, + {.bits = utils::EnumBits(State::Might), .name = "Might"}, + {.bits = utils::EnumBits(State::Prayer), .name = "Prayer"}, + {.bits = utils::EnumBits(State::HolyFire), .name = "HolyFire"}, + {.bits = utils::EnumBits(State::Thorns), .name = "Thorns"}, + {.bits = utils::EnumBits(State::Defiance), .name = "Defiance"}, + {.bits = utils::EnumBits(State::Thunderstorm), .name = "Thunderstorm"}, + {.bits = utils::EnumBits(State::LightningBolt), .name = "LightningBolt"}, + {.bits = utils::EnumBits(State::BlessedAim), .name = "BlessedAim"}, + {.bits = utils::EnumBits(State::Stamina), .name = "Stamina"}, + {.bits = utils::EnumBits(State::Concentration), .name = "Concentration"}, + {.bits = utils::EnumBits(State::HolyWind), .name = "HolyWind"}, + {.bits = utils::EnumBits(State::HolyWindCold), .name = "HolyWindCold"}, + {.bits = utils::EnumBits(State::Cleansing), .name = "Cleansing"}, + {.bits = utils::EnumBits(State::HolyShock), .name = "HolyShock"}, + {.bits = utils::EnumBits(State::Sanctuary), .name = "Sanctuary"}, + {.bits = utils::EnumBits(State::Meditation), .name = "Meditation"}, + {.bits = utils::EnumBits(State::Fanaticism), .name = "Fanaticism"}, + {.bits = utils::EnumBits(State::Redemption), .name = "Redemption"}, + {.bits = utils::EnumBits(State::BattleCommand), .name = "BattleCommand"}, + {.bits = utils::EnumBits(State::PreventHeal), .name = "PreventHeal"}, + {.bits = utils::EnumBits(State::Conversion), .name = "Conversion"}, + {.bits = utils::EnumBits(State::Uninterruptable), .name = "Uninterruptable"}, + {.bits = utils::EnumBits(State::IronMaiden), .name = "IronMaiden"}, + {.bits = utils::EnumBits(State::Terror), .name = "Terror"}, + {.bits = utils::EnumBits(State::Attract), .name = "Attract"}, + {.bits = utils::EnumBits(State::LifeTap), .name = "LifeTap"}, + {.bits = utils::EnumBits(State::Confuse), .name = "Confuse"}, + {.bits = utils::EnumBits(State::Decrepify), .name = "Decrepify"}, + {.bits = utils::EnumBits(State::LowerResist), .name = "LowerResist"}, + {.bits = utils::EnumBits(State::OpenWounds), .name = "OpenWounds"}, + {.bits = utils::EnumBits(State::Dopplezon), .name = "Dopplezon"}, + {.bits = utils::EnumBits(State::CriticalStrike), .name = "CriticalStrike"}, + {.bits = utils::EnumBits(State::Dodge), .name = "Dodge"}, + {.bits = utils::EnumBits(State::Avoid), .name = "Avoid"}, + {.bits = utils::EnumBits(State::Penetrate), .name = "Penetrate"}, + {.bits = utils::EnumBits(State::Evade), .name = "Evade"}, + {.bits = utils::EnumBits(State::Pierce), .name = "Pierce"}, + {.bits = utils::EnumBits(State::Warmth), .name = "Warmth"}, + {.bits = utils::EnumBits(State::FireMastery), .name = "FireMastery"}, + {.bits = utils::EnumBits(State::LightningMastery), .name = "LightningMastery"}, + {.bits = utils::EnumBits(State::ColdMastery), .name = "ColdMastery"}, + {.bits = utils::EnumBits(State::BladeMastery), .name = "BladeMastery"}, + {.bits = utils::EnumBits(State::AxeMastery), .name = "AxeMastery"}, + {.bits = utils::EnumBits(State::MaceMastery), .name = "MaceMastery"}, + {.bits = utils::EnumBits(State::PolearmMastery), .name = "PolearmMastery"}, + {.bits = utils::EnumBits(State::ThrowingMastery), .name = "ThrowingMastery"}, + {.bits = utils::EnumBits(State::SpearMastery), .name = "SpearMastery"}, + {.bits = utils::EnumBits(State::IncreasedStamina), .name = "IncreasedStamina"}, + {.bits = utils::EnumBits(State::IronSkin), .name = "IronSkin"}, + {.bits = utils::EnumBits(State::IncreasedSpeed), .name = "IncreasedSpeed"}, + {.bits = utils::EnumBits(State::NaturalResistance), .name = "NaturalResistance"}, + {.bits = utils::EnumBits(State::FingerMageCurse), .name = "FingerMageCurse"}, + {.bits = utils::EnumBits(State::NoManaRegen), .name = "NoManaRegen"}, + {.bits = utils::EnumBits(State::JustHit), .name = "JustHit"}, + {.bits = utils::EnumBits(State::SlowMissiles), .name = "SlowMissiles"}, + {.bits = utils::EnumBits(State::ShiverArmor), .name = "ShiverArmor"}, + {.bits = utils::EnumBits(State::BattleCry), .name = "BattleCry"}, + {.bits = utils::EnumBits(State::Blue), .name = "Blue"}, + {.bits = utils::EnumBits(State::Red), .name = "Red"}, + {.bits = utils::EnumBits(State::DeathDelay), .name = "DeathDelay"}, + {.bits = utils::EnumBits(State::Valkyrie), .name = "Valkyrie"}, + {.bits = utils::EnumBits(State::Frenzy), .name = "Frenzy"}, + {.bits = utils::EnumBits(State::Berserk), .name = "Berserk"}, + {.bits = utils::EnumBits(State::Revive), .name = "Revive"}, + {.bits = utils::EnumBits(State::SkeletalMastery), .name = "SkeletalMastery"}, + {.bits = utils::EnumBits(State::SourceUnit), .name = "SourceUnit"}, + {.bits = utils::EnumBits(State::Redeemed), .name = "Redeemed"}, + {.bits = utils::EnumBits(State::HealthPot), .name = "HealthPot"}, + {.bits = utils::EnumBits(State::HolyShield), .name = "HolyShield"}, + {.bits = utils::EnumBits(State::JustPortaled), .name = "JustPortaled"}, + {.bits = utils::EnumBits(State::MonFrenzy), .name = "MonFrenzy"}, + {.bits = utils::EnumBits(State::CorpseNoDraw), .name = "CorpseNoDraw"}, + {.bits = utils::EnumBits(State::Alignment), .name = "Alignment"}, + {.bits = utils::EnumBits(State::ManaPot), .name = "ManaPot"}, + {.bits = utils::EnumBits(State::Shatter), .name = "Shatter"}, + {.bits = utils::EnumBits(State::SyncWarped), .name = "SyncWarped"}, + {.bits = utils::EnumBits(State::ConversionSave), .name = "ConversionSave"}, + {.bits = utils::EnumBits(State::Pregnant), .name = "Pregnant"}, + {.bits = utils::EnumBits(State::GolemMastery), .name = "GolemMastery"}, + {.bits = utils::EnumBits(State::Rabies), .name = "Rabies"}, + {.bits = utils::EnumBits(State::DefenseCurse), .name = "DefenseCurse"}, + {.bits = utils::EnumBits(State::BloodMana), .name = "BloodMana"}, + {.bits = utils::EnumBits(State::Burning), .name = "Burning"}, + {.bits = utils::EnumBits(State::DragonFlight), .name = "DragonFlight"}, + {.bits = utils::EnumBits(State::Maul), .name = "Maul"}, + {.bits = utils::EnumBits(State::CorpseNoSelect), .name = "CorpseNoSelect"}, + {.bits = utils::EnumBits(State::ShadowWarrior), .name = "ShadowWarrior"}, + {.bits = utils::EnumBits(State::FeralRage), .name = "FeralRage"}, + {.bits = utils::EnumBits(State::SkillDelay), .name = "SkillDelay"}, + {.bits = utils::EnumBits(State::ProgressiveDamage), .name = "ProgressiveDamage"}, + {.bits = utils::EnumBits(State::ProgressiveSteal), .name = "ProgressiveSteal"}, + {.bits = utils::EnumBits(State::ProgressiveOther), .name = "ProgressiveOther"}, + {.bits = utils::EnumBits(State::ProgressiveFire), .name = "ProgressiveFire"}, + {.bits = utils::EnumBits(State::ProgressiveCold), .name = "ProgressiveCold"}, + {.bits = utils::EnumBits(State::ProgressiveLightning), .name = "ProgressiveLightning"}, + {.bits = utils::EnumBits(State::ShrineArmor), .name = "ShrineArmor"}, + {.bits = utils::EnumBits(State::ShrineCombat), .name = "ShrineCombat"}, + {.bits = utils::EnumBits(State::ShrineResistLightning), .name = "ShrineResistLightning"}, + {.bits = utils::EnumBits(State::ShrineResistFire), .name = "ShrineResistFire"}, + {.bits = utils::EnumBits(State::ShrineResistCold), .name = "ShrineResistCold"}, + {.bits = utils::EnumBits(State::ShrineResistPoison), .name = "ShrineResistPoison"}, + {.bits = utils::EnumBits(State::ShrineSkill), .name = "ShrineSkill"}, + {.bits = utils::EnumBits(State::ShrineManaRegen), .name = "ShrineManaRegen"}, + {.bits = utils::EnumBits(State::ShrineStamina), .name = "ShrineStamina"}, + {.bits = utils::EnumBits(State::ShrineExperience), .name = "ShrineExperience"}, + {.bits = utils::EnumBits(State::FenrisRage), .name = "FenrisRage"}, + {.bits = utils::EnumBits(State::Wolf), .name = "Wolf"}, + {.bits = utils::EnumBits(State::Bear), .name = "Bear"}, + {.bits = utils::EnumBits(State::BloodLust), .name = "BloodLust"}, + {.bits = utils::EnumBits(State::ChangeClass), .name = "ChangeClass"}, + {.bits = utils::EnumBits(State::Attached), .name = "Attached"}, + {.bits = utils::EnumBits(State::Hurricane), .name = "Hurricane"}, + {.bits = utils::EnumBits(State::Armageddon), .name = "Armageddon"}, + {.bits = utils::EnumBits(State::Invis), .name = "Invis"}, + {.bits = utils::EnumBits(State::Barbs), .name = "Barbs"}, + {.bits = utils::EnumBits(State::Wolverine), .name = "Wolverine"}, + {.bits = utils::EnumBits(State::Oaksage), .name = "Oaksage"}, + {.bits = utils::EnumBits(State::VineBeast), .name = "VineBeast"}, + {.bits = utils::EnumBits(State::CycloneArmor), .name = "CycloneArmor"}, + {.bits = utils::EnumBits(State::ClawMastery), .name = "ClawMastery"}, + {.bits = utils::EnumBits(State::CloakOfShadows), .name = "CloakOfShadows"}, + {.bits = utils::EnumBits(State::Recycled), .name = "Recycled"}, + {.bits = utils::EnumBits(State::WeaponBlock), .name = "WeaponBlock"}, + {.bits = utils::EnumBits(State::Cloaked), .name = "Cloaked"}, + {.bits = utils::EnumBits(State::Quickness), .name = "Quickness"}, + {.bits = utils::EnumBits(State::BladeShield), .name = "BladeShield"}, + {.bits = utils::EnumBits(State::Fade), .name = "Fade"}, + {.bits = utils::EnumBits(State::SummonResist), .name = "SummonResist"}, + {.bits = utils::EnumBits(State::OakSageControl), .name = "OakSageControl"}, + {.bits = utils::EnumBits(State::WolverineControl), .name = "WolverineControl"}, + {.bits = utils::EnumBits(State::BarbsControl), .name = "BarbsControl"}, + {.bits = utils::EnumBits(State::DebugControl), .name = "DebugControl"}, + {.bits = utils::EnumBits(State::Itemset1), .name = "Itemset1"}, + {.bits = utils::EnumBits(State::Itemset2), .name = "Itemset2"}, + {.bits = utils::EnumBits(State::Itemset3), .name = "Itemset3"}, + {.bits = utils::EnumBits(State::Itemset4), .name = "Itemset4"}, + {.bits = utils::EnumBits(State::Itemset5), .name = "Itemset5"}, + {.bits = utils::EnumBits(State::Itemset6), .name = "Itemset6"}, + {.bits = utils::EnumBits(State::RuneWord), .name = "RuneWord"}, + {.bits = utils::EnumBits(State::RestInPeace), .name = "RestInPeace"}, + {.bits = utils::EnumBits(State::CorpseExp), .name = "CorpseExp"}, + {.bits = utils::EnumBits(State::Whirlwind), .name = "Whirlwind"}, + {.bits = utils::EnumBits(State::FullSetGeneric), .name = "FullSetGeneric"}, + {.bits = utils::EnumBits(State::MonsterSet), .name = "MonsterSet"}, + {.bits = utils::EnumBits(State::Delerium), .name = "Delerium"}, + {.bits = utils::EnumBits(State::Antidote), .name = "Antidote"}, + {.bits = utils::EnumBits(State::Thawing), .name = "Thawing"}, + {.bits = utils::EnumBits(State::StaminaPot), .name = "StaminaPot"}, + {.bits = utils::EnumBits(State::PassiveResistFire), .name = "PassiveResistFire"}, + {.bits = utils::EnumBits(State::PassiveResistCold), .name = "PassiveResistCold"}, + {.bits = utils::EnumBits(State::PassiveResistLtng), .name = "PassiveResistLtng"}, + {.bits = utils::EnumBits(State::UberMinion), .name = "UberMinion"}, + {.bits = utils::EnumBits(State::Cooldown), .name = "Cooldown"}, + {.bits = utils::EnumBits(State::SharedStash), .name = "SharedStash"}, + {.bits = utils::EnumBits(State::HideDead), .name = "HideDead"}, + {.bits = utils::EnumBits(State::Impale), .name = "Impale"}, + {.bits = utils::EnumBits(State::Desecrated), .name = "Desecrated"}, + {.bits = utils::EnumBits(State::MarkBear), .name = "MarkBear"}, + {.bits = utils::EnumBits(State::MarkWolf), .name = "MarkWolf"}, + {.bits = utils::EnumBits(State::SigilLethargy), .name = "SigilLethargy"}, + {.bits = utils::EnumBits(State::SigilRancor), .name = "SigilRancor"}, + {.bits = utils::EnumBits(State::SigilDeath), .name = "SigilDeath"}, + {.bits = utils::EnumBits(State::BloodOath), .name = "BloodOath"}, + {.bits = utils::EnumBits(State::DemonicMastery), .name = "DemonicMastery"}, + {.bits = utils::EnumBits(State::LevitateMastery), .name = "LevitateMastery"}, + {.bits = utils::EnumBits(State::HexBane), .name = "HexBane"}, + {.bits = utils::EnumBits(State::HexBaneDebuff), .name = "HexBaneDebuff"}, + {.bits = utils::EnumBits(State::HexSiphon), .name = "HexSiphon"}, + {.bits = utils::EnumBits(State::HexSiphonDebuff), .name = "HexSiphonDebuff"}, + {.bits = utils::EnumBits(State::HexPurge), .name = "HexPurge"}, + {.bits = utils::EnumBits(State::MindBarrier), .name = "MindBarrier"}, + {.bits = utils::EnumBits(State::ShadowMastery), .name = "ShadowMastery"}, + {.bits = utils::EnumBits(State::Sigil), .name = "Sigil"}, + {.bits = utils::EnumBits(State::PsychicWard), .name = "PsychicWard"}, + {.bits = utils::EnumBits(State::BindDemon), .name = "BindDemon"}, + {.bits = utils::EnumBits(State::Consume), .name = "Consume"}, + {.bits = utils::EnumBits(State::AdvancedStash), .name = "AdvancedStash"}, + {.bits = utils::EnumBits(State::HealthLink), .name = "HealthLink"}, + {.bits = utils::EnumBits(State::Engorge), .name = "Engorge"}, + {.bits = utils::EnumBits(State::DeathMark), .name = "DeathMark"}, + {.bits = utils::EnumBits(State::HealthLinkCaster), .name = "HealthLinkCaster"}, + {.bits = utils::EnumBits(State::WarlockDemonGround), .name = "WarlockDemonGround"}, + {.bits = utils::EnumBits(State::WarlockDemonTorso), .name = "WarlockDemonTorso"}, + {.bits = utils::EnumBits(State::SkillChanneling), .name = "SkillChanneling"}, + {.bits = utils::EnumBits(State::HexPurgeDebuff), .name = "HexPurgeDebuff"}, + {.bits = utils::EnumBits(State::ChronicleBacklight), .name = "ChronicleBacklight"}, + {.bits = utils::EnumBits(State::ChronicleFootprints), .name = "ChronicleFootprints"}, + {.bits = utils::EnumBits(State::ChroniclePortal), .name = "ChroniclePortal"}, + {.bits = utils::EnumBits(State::EldritchBlastPeriodic), .name = "EldritchBlastPeriodic"}, + {.bits = utils::EnumBits(State::LightningEnchant), .name = "LightningEnchant"}, + {.bits = utils::EnumBits(State::ColdEnchant), .name = "ColdEnchant"}, + {.bits = utils::EnumBits(State::TalicFirePierce), .name = "TalicFirePierce"}, + {.bits = utils::EnumBits(State::MadawcLightPierce), .name = "MadawcLightPierce"}, + {.bits = utils::EnumBits(State::KorlicColdPierce), .name = "KorlicColdPierce"}, + {.bits = utils::EnumBits(State::BindDemonUnderling), .name = "BindDemonUnderling"}, + {.bits = utils::EnumBits(State::Herald), .name = "Herald"}, + {.bits = utils::EnumBits(State::Apocalypse), .name = "Apocalypse"}, + }); + return utils::NameEnumValue("State", ENTRIES, std::to_underlying(value)); +} + +std::string EnumName(Skill value) { + static constexpr auto ENTRIES = std::to_array({ + {.bits = utils::EnumBits(Skill::Attack), .name = "Attack"}, + {.bits = utils::EnumBits(Skill::Kick), .name = "Kick"}, + {.bits = utils::EnumBits(Skill::Throw), .name = "Throw"}, + {.bits = utils::EnumBits(Skill::Unsummon), .name = "Unsummon"}, + {.bits = utils::EnumBits(Skill::LeftHandThrow), .name = "LeftHandThrow"}, + {.bits = utils::EnumBits(Skill::LeftHandSwing), .name = "LeftHandSwing"}, + {.bits = utils::EnumBits(Skill::MagicArrow), .name = "MagicArrow"}, + {.bits = utils::EnumBits(Skill::FireArrow), .name = "FireArrow"}, + {.bits = utils::EnumBits(Skill::InnerSight), .name = "InnerSight"}, + {.bits = utils::EnumBits(Skill::CriticalStrike), .name = "CriticalStrike"}, + {.bits = utils::EnumBits(Skill::Jab), .name = "Jab"}, + {.bits = utils::EnumBits(Skill::ColdArrow), .name = "ColdArrow"}, + {.bits = utils::EnumBits(Skill::MultipleShot), .name = "MultipleShot"}, + {.bits = utils::EnumBits(Skill::Dodge), .name = "Dodge"}, + {.bits = utils::EnumBits(Skill::PowerStrike), .name = "PowerStrike"}, + {.bits = utils::EnumBits(Skill::PoisonJavelin), .name = "PoisonJavelin"}, + {.bits = utils::EnumBits(Skill::ExplodingArrow), .name = "ExplodingArrow"}, + {.bits = utils::EnumBits(Skill::SlowMissiles), .name = "SlowMissiles"}, + {.bits = utils::EnumBits(Skill::Avoid), .name = "Avoid"}, + {.bits = utils::EnumBits(Skill::Impale), .name = "Impale"}, + {.bits = utils::EnumBits(Skill::LightningBolt), .name = "LightningBolt"}, + {.bits = utils::EnumBits(Skill::IceArrow), .name = "IceArrow"}, + {.bits = utils::EnumBits(Skill::GuidedArrow), .name = "GuidedArrow"}, + {.bits = utils::EnumBits(Skill::Penetrate), .name = "Penetrate"}, + {.bits = utils::EnumBits(Skill::ChargedStrike), .name = "ChargedStrike"}, + {.bits = utils::EnumBits(Skill::PlagueJavelin), .name = "PlagueJavelin"}, + {.bits = utils::EnumBits(Skill::Strafe), .name = "Strafe"}, + {.bits = utils::EnumBits(Skill::ImmolationArrow), .name = "ImmolationArrow"}, + {.bits = utils::EnumBits(Skill::Dopplezon), .name = "Dopplezon"}, + {.bits = utils::EnumBits(Skill::Evade), .name = "Evade"}, + {.bits = utils::EnumBits(Skill::Fend), .name = "Fend"}, + {.bits = utils::EnumBits(Skill::FreezingArrow), .name = "FreezingArrow"}, + {.bits = utils::EnumBits(Skill::Valkyrie), .name = "Valkyrie"}, + {.bits = utils::EnumBits(Skill::Pierce), .name = "Pierce"}, + {.bits = utils::EnumBits(Skill::LightningStrike), .name = "LightningStrike"}, + {.bits = utils::EnumBits(Skill::LightningFury), .name = "LightningFury"}, + {.bits = utils::EnumBits(Skill::FireBolt), .name = "FireBolt"}, + {.bits = utils::EnumBits(Skill::Warmth), .name = "Warmth"}, + {.bits = utils::EnumBits(Skill::ChargedBolt), .name = "ChargedBolt"}, + {.bits = utils::EnumBits(Skill::IceBolt), .name = "IceBolt"}, + {.bits = utils::EnumBits(Skill::FrozenArmor), .name = "FrozenArmor"}, + {.bits = utils::EnumBits(Skill::Inferno), .name = "Inferno"}, + {.bits = utils::EnumBits(Skill::StaticField), .name = "StaticField"}, + {.bits = utils::EnumBits(Skill::Telekinesis), .name = "Telekinesis"}, + {.bits = utils::EnumBits(Skill::FrostNova), .name = "FrostNova"}, + {.bits = utils::EnumBits(Skill::IceBlast), .name = "IceBlast"}, + {.bits = utils::EnumBits(Skill::Blaze), .name = "Blaze"}, + {.bits = utils::EnumBits(Skill::FireBall), .name = "FireBall"}, + {.bits = utils::EnumBits(Skill::Nova), .name = "Nova"}, + {.bits = utils::EnumBits(Skill::Lightning), .name = "Lightning"}, + {.bits = utils::EnumBits(Skill::ShiverArmor), .name = "ShiverArmor"}, + {.bits = utils::EnumBits(Skill::FireWall), .name = "FireWall"}, + {.bits = utils::EnumBits(Skill::Enchant), .name = "Enchant"}, + {.bits = utils::EnumBits(Skill::ChainLightning), .name = "ChainLightning"}, + {.bits = utils::EnumBits(Skill::Teleport), .name = "Teleport"}, + {.bits = utils::EnumBits(Skill::GlacialSpike), .name = "GlacialSpike"}, + {.bits = utils::EnumBits(Skill::Meteor), .name = "Meteor"}, + {.bits = utils::EnumBits(Skill::ThunderStorm), .name = "ThunderStorm"}, + {.bits = utils::EnumBits(Skill::EnergyShield), .name = "EnergyShield"}, + {.bits = utils::EnumBits(Skill::Blizzard), .name = "Blizzard"}, + {.bits = utils::EnumBits(Skill::ChillingArmor), .name = "ChillingArmor"}, + {.bits = utils::EnumBits(Skill::FireMastery), .name = "FireMastery"}, + {.bits = utils::EnumBits(Skill::Hydra), .name = "Hydra"}, + {.bits = utils::EnumBits(Skill::LightningMastery), .name = "LightningMastery"}, + {.bits = utils::EnumBits(Skill::FrozenOrb), .name = "FrozenOrb"}, + {.bits = utils::EnumBits(Skill::ColdMastery), .name = "ColdMastery"}, + {.bits = utils::EnumBits(Skill::AmplifyDamage), .name = "AmplifyDamage"}, + {.bits = utils::EnumBits(Skill::Teeth), .name = "Teeth"}, + {.bits = utils::EnumBits(Skill::BoneArmor), .name = "BoneArmor"}, + {.bits = utils::EnumBits(Skill::SkeletonMastery), .name = "SkeletonMastery"}, + {.bits = utils::EnumBits(Skill::RaiseSkeleton), .name = "RaiseSkeleton"}, + {.bits = utils::EnumBits(Skill::DimVision), .name = "DimVision"}, + {.bits = utils::EnumBits(Skill::Weaken), .name = "Weaken"}, + {.bits = utils::EnumBits(Skill::PoisonDagger), .name = "PoisonDagger"}, + {.bits = utils::EnumBits(Skill::CorpseExplosion), .name = "CorpseExplosion"}, + {.bits = utils::EnumBits(Skill::ClayGolem), .name = "ClayGolem"}, + {.bits = utils::EnumBits(Skill::IronMaiden), .name = "IronMaiden"}, + {.bits = utils::EnumBits(Skill::Terror), .name = "Terror"}, + {.bits = utils::EnumBits(Skill::BoneWall), .name = "BoneWall"}, + {.bits = utils::EnumBits(Skill::GolemMastery), .name = "GolemMastery"}, + {.bits = utils::EnumBits(Skill::RaiseSkeletalMage), .name = "RaiseSkeletalMage"}, + {.bits = utils::EnumBits(Skill::Confuse), .name = "Confuse"}, + {.bits = utils::EnumBits(Skill::LifeTap), .name = "LifeTap"}, + {.bits = utils::EnumBits(Skill::PoisonExplosion), .name = "PoisonExplosion"}, + {.bits = utils::EnumBits(Skill::BoneSpear), .name = "BoneSpear"}, + {.bits = utils::EnumBits(Skill::BloodGolem), .name = "BloodGolem"}, + {.bits = utils::EnumBits(Skill::Attract), .name = "Attract"}, + {.bits = utils::EnumBits(Skill::Decrepify), .name = "Decrepify"}, + {.bits = utils::EnumBits(Skill::BonePrison), .name = "BonePrison"}, + {.bits = utils::EnumBits(Skill::SummonResist), .name = "SummonResist"}, + {.bits = utils::EnumBits(Skill::IronGolem), .name = "IronGolem"}, + {.bits = utils::EnumBits(Skill::LowerResist), .name = "LowerResist"}, + {.bits = utils::EnumBits(Skill::PoisonNova), .name = "PoisonNova"}, + {.bits = utils::EnumBits(Skill::BoneSpirit), .name = "BoneSpirit"}, + {.bits = utils::EnumBits(Skill::FireGolem), .name = "FireGolem"}, + {.bits = utils::EnumBits(Skill::Revive), .name = "Revive"}, + {.bits = utils::EnumBits(Skill::Sacrifice), .name = "Sacrifice"}, + {.bits = utils::EnumBits(Skill::Smite), .name = "Smite"}, + {.bits = utils::EnumBits(Skill::Might), .name = "Might"}, + {.bits = utils::EnumBits(Skill::Prayer), .name = "Prayer"}, + {.bits = utils::EnumBits(Skill::ResistFire), .name = "ResistFire"}, + {.bits = utils::EnumBits(Skill::HolyBolt), .name = "HolyBolt"}, + {.bits = utils::EnumBits(Skill::HolyFire), .name = "HolyFire"}, + {.bits = utils::EnumBits(Skill::Thorns), .name = "Thorns"}, + {.bits = utils::EnumBits(Skill::Defiance), .name = "Defiance"}, + {.bits = utils::EnumBits(Skill::ResistCold), .name = "ResistCold"}, + {.bits = utils::EnumBits(Skill::Zeal), .name = "Zeal"}, + {.bits = utils::EnumBits(Skill::Charge), .name = "Charge"}, + {.bits = utils::EnumBits(Skill::BlessedAim), .name = "BlessedAim"}, + {.bits = utils::EnumBits(Skill::Cleansing), .name = "Cleansing"}, + {.bits = utils::EnumBits(Skill::ResistLightning), .name = "ResistLightning"}, + {.bits = utils::EnumBits(Skill::Vengeance), .name = "Vengeance"}, + {.bits = utils::EnumBits(Skill::BlessedHammer), .name = "BlessedHammer"}, + {.bits = utils::EnumBits(Skill::Concentration), .name = "Concentration"}, + {.bits = utils::EnumBits(Skill::HolyFreeze), .name = "HolyFreeze"}, + {.bits = utils::EnumBits(Skill::Vigor), .name = "Vigor"}, + {.bits = utils::EnumBits(Skill::Conversion), .name = "Conversion"}, + {.bits = utils::EnumBits(Skill::HolyShield), .name = "HolyShield"}, + {.bits = utils::EnumBits(Skill::HolyShock), .name = "HolyShock"}, + {.bits = utils::EnumBits(Skill::Sanctuary), .name = "Sanctuary"}, + {.bits = utils::EnumBits(Skill::Meditation), .name = "Meditation"}, + {.bits = utils::EnumBits(Skill::FistOfTheHeavens), .name = "FistOfTheHeavens"}, + {.bits = utils::EnumBits(Skill::Fanaticism), .name = "Fanaticism"}, + {.bits = utils::EnumBits(Skill::Conviction), .name = "Conviction"}, + {.bits = utils::EnumBits(Skill::Redemption), .name = "Redemption"}, + {.bits = utils::EnumBits(Skill::Salvation), .name = "Salvation"}, + {.bits = utils::EnumBits(Skill::Bash), .name = "Bash"}, + {.bits = utils::EnumBits(Skill::BladeMastery), .name = "BladeMastery"}, + {.bits = utils::EnumBits(Skill::AxeMastery), .name = "AxeMastery"}, + {.bits = utils::EnumBits(Skill::MaceMastery), .name = "MaceMastery"}, + {.bits = utils::EnumBits(Skill::Howl), .name = "Howl"}, + {.bits = utils::EnumBits(Skill::FindPotion), .name = "FindPotion"}, + {.bits = utils::EnumBits(Skill::Leap), .name = "Leap"}, + {.bits = utils::EnumBits(Skill::DoubleSwing), .name = "DoubleSwing"}, + {.bits = utils::EnumBits(Skill::PoleArmMastery), .name = "PoleArmMastery"}, + {.bits = utils::EnumBits(Skill::ThrowingMastery), .name = "ThrowingMastery"}, + {.bits = utils::EnumBits(Skill::SpearMastery), .name = "SpearMastery"}, + {.bits = utils::EnumBits(Skill::Taunt), .name = "Taunt"}, + {.bits = utils::EnumBits(Skill::Shout), .name = "Shout"}, + {.bits = utils::EnumBits(Skill::Stun), .name = "Stun"}, + {.bits = utils::EnumBits(Skill::DoubleThrow), .name = "DoubleThrow"}, + {.bits = utils::EnumBits(Skill::IncreasedStamina), .name = "IncreasedStamina"}, + {.bits = utils::EnumBits(Skill::FindItem), .name = "FindItem"}, + {.bits = utils::EnumBits(Skill::LeapAttack), .name = "LeapAttack"}, + {.bits = utils::EnumBits(Skill::Concentrate), .name = "Concentrate"}, + {.bits = utils::EnumBits(Skill::IronSkin), .name = "IronSkin"}, + {.bits = utils::EnumBits(Skill::BattleCry), .name = "BattleCry"}, + {.bits = utils::EnumBits(Skill::Frenzy), .name = "Frenzy"}, + {.bits = utils::EnumBits(Skill::IncreasedSpeed), .name = "IncreasedSpeed"}, + {.bits = utils::EnumBits(Skill::BattleOrders), .name = "BattleOrders"}, + {.bits = utils::EnumBits(Skill::GrimWard), .name = "GrimWard"}, + {.bits = utils::EnumBits(Skill::Whirlwind), .name = "Whirlwind"}, + {.bits = utils::EnumBits(Skill::Berserk), .name = "Berserk"}, + {.bits = utils::EnumBits(Skill::NaturalResistance), .name = "NaturalResistance"}, + {.bits = utils::EnumBits(Skill::WarCry), .name = "WarCry"}, + {.bits = utils::EnumBits(Skill::BattleCommand), .name = "BattleCommand"}, + {.bits = utils::EnumBits(Skill::FireHit), .name = "FireHit"}, + {.bits = utils::EnumBits(Skill::UnHolyBolt), .name = "UnHolyBolt"}, + {.bits = utils::EnumBits(Skill::SkeletonRaise), .name = "SkeletonRaise"}, + {.bits = utils::EnumBits(Skill::MaggotEgg), .name = "MaggotEgg"}, + {.bits = utils::EnumBits(Skill::ShamanFire), .name = "ShamanFire"}, + {.bits = utils::EnumBits(Skill::MagottUp), .name = "MagottUp"}, + {.bits = utils::EnumBits(Skill::MagottDown), .name = "MagottDown"}, + {.bits = utils::EnumBits(Skill::MagottLay), .name = "MagottLay"}, + {.bits = utils::EnumBits(Skill::AndrialSpray), .name = "AndrialSpray"}, + {.bits = utils::EnumBits(Skill::Jump), .name = "Jump"}, + {.bits = utils::EnumBits(Skill::SwarmMove), .name = "SwarmMove"}, + {.bits = utils::EnumBits(Skill::Nest), .name = "Nest"}, + {.bits = utils::EnumBits(Skill::QuickStrike), .name = "QuickStrike"}, + {.bits = utils::EnumBits(Skill::VampireFireball), .name = "VampireFireball"}, + {.bits = utils::EnumBits(Skill::VampireFirewall), .name = "VampireFirewall"}, + {.bits = utils::EnumBits(Skill::VampireMeteor), .name = "VampireMeteor"}, + {.bits = utils::EnumBits(Skill::GargoyleTrap), .name = "GargoyleTrap"}, + {.bits = utils::EnumBits(Skill::SpiderLay), .name = "SpiderLay"}, + {.bits = utils::EnumBits(Skill::VampireHeal), .name = "VampireHeal"}, + {.bits = utils::EnumBits(Skill::VampireRaise), .name = "VampireRaise"}, + {.bits = utils::EnumBits(Skill::Submerge), .name = "Submerge"}, + {.bits = utils::EnumBits(Skill::FetishAura), .name = "FetishAura"}, + {.bits = utils::EnumBits(Skill::FetishInferno), .name = "FetishInferno"}, + {.bits = utils::EnumBits(Skill::ZakarumHeal), .name = "ZakarumHeal"}, + {.bits = utils::EnumBits(Skill::Emerge), .name = "Emerge"}, + {.bits = utils::EnumBits(Skill::Resurrect), .name = "Resurrect"}, + {.bits = utils::EnumBits(Skill::Bestow), .name = "Bestow"}, + {.bits = utils::EnumBits(Skill::MissileSkill1), .name = "MissileSkill1"}, + {.bits = utils::EnumBits(Skill::MonTeleport), .name = "MonTeleport"}, + {.bits = utils::EnumBits(Skill::PrimeLightning), .name = "PrimeLightning"}, + {.bits = utils::EnumBits(Skill::PrimeBolt), .name = "PrimeBolt"}, + {.bits = utils::EnumBits(Skill::PrimeBlaze), .name = "PrimeBlaze"}, + {.bits = utils::EnumBits(Skill::PrimeFirewall), .name = "PrimeFirewall"}, + {.bits = utils::EnumBits(Skill::PrimeSpike), .name = "PrimeSpike"}, + {.bits = utils::EnumBits(Skill::PrimeIceNova), .name = "PrimeIceNova"}, + {.bits = utils::EnumBits(Skill::PrimePoisonball), .name = "PrimePoisonball"}, + {.bits = utils::EnumBits(Skill::PrimePoisonNova), .name = "PrimePoisonNova"}, + {.bits = utils::EnumBits(Skill::DiabLight), .name = "DiabLight"}, + {.bits = utils::EnumBits(Skill::DiabCold), .name = "DiabCold"}, + {.bits = utils::EnumBits(Skill::DiabFire), .name = "DiabFire"}, + {.bits = utils::EnumBits(Skill::FingerMageSpider), .name = "FingerMageSpider"}, + {.bits = utils::EnumBits(Skill::DiabWall), .name = "DiabWall"}, + {.bits = utils::EnumBits(Skill::DiabRun), .name = "DiabRun"}, + {.bits = utils::EnumBits(Skill::DiabPrison), .name = "DiabPrison"}, + {.bits = utils::EnumBits(Skill::PoisonBallTrap), .name = "PoisonBallTrap"}, + {.bits = utils::EnumBits(Skill::AndyPoisonBolt), .name = "AndyPoisonBolt"}, + {.bits = utils::EnumBits(Skill::HireableMissile), .name = "HireableMissile"}, + {.bits = utils::EnumBits(Skill::DesertTurret), .name = "DesertTurret"}, + {.bits = utils::EnumBits(Skill::ArcaneTower), .name = "ArcaneTower"}, + {.bits = utils::EnumBits(Skill::MonBlizzard), .name = "MonBlizzard"}, + {.bits = utils::EnumBits(Skill::Mosquito), .name = "Mosquito"}, + {.bits = utils::EnumBits(Skill::CursedBallTrapRight), .name = "CursedBallTrapRight"}, + {.bits = utils::EnumBits(Skill::CursedBallTrapLeft), .name = "CursedBallTrapLeft"}, + {.bits = utils::EnumBits(Skill::MonFrozenArmor), .name = "MonFrozenArmor"}, + {.bits = utils::EnumBits(Skill::MonBoneArmor), .name = "MonBoneArmor"}, + {.bits = utils::EnumBits(Skill::MonBoneSpirit), .name = "MonBoneSpirit"}, + {.bits = utils::EnumBits(Skill::MonCurseCast), .name = "MonCurseCast"}, + {.bits = utils::EnumBits(Skill::HellMeteor), .name = "HellMeteor"}, + {.bits = utils::EnumBits(Skill::RegurgitatorEat), .name = "RegurgitatorEat"}, + {.bits = utils::EnumBits(Skill::MonFrenzy), .name = "MonFrenzy"}, + {.bits = utils::EnumBits(Skill::QueenDeath), .name = "QueenDeath"}, + {.bits = utils::EnumBits(Skill::ScrollOfIdentify), .name = "ScrollOfIdentify"}, + {.bits = utils::EnumBits(Skill::BookOfIdentify), .name = "BookOfIdentify"}, + {.bits = utils::EnumBits(Skill::ScrollOfTownportal), .name = "ScrollOfTownportal"}, + {.bits = utils::EnumBits(Skill::BookOfTownportal), .name = "BookOfTownportal"}, + {.bits = utils::EnumBits(Skill::Raven), .name = "Raven"}, + {.bits = utils::EnumBits(Skill::PlaguePoppy), .name = "PlaguePoppy"}, + {.bits = utils::EnumBits(Skill::Wearwolf), .name = "Wearwolf"}, + {.bits = utils::EnumBits(Skill::ShapeShifting), .name = "ShapeShifting"}, + {.bits = utils::EnumBits(Skill::Firestorm), .name = "Firestorm"}, + {.bits = utils::EnumBits(Skill::OakSage), .name = "OakSage"}, + {.bits = utils::EnumBits(Skill::SummonSpiritWolf), .name = "SummonSpiritWolf"}, + {.bits = utils::EnumBits(Skill::Wearbear), .name = "Wearbear"}, + {.bits = utils::EnumBits(Skill::MoltenBoulder), .name = "MoltenBoulder"}, + {.bits = utils::EnumBits(Skill::ArcticBlast), .name = "ArcticBlast"}, + {.bits = utils::EnumBits(Skill::CycleOfLife), .name = "CycleOfLife"}, + {.bits = utils::EnumBits(Skill::FeralRage), .name = "FeralRage"}, + {.bits = utils::EnumBits(Skill::Maul), .name = "Maul"}, + {.bits = utils::EnumBits(Skill::Eruption), .name = "Eruption"}, + {.bits = utils::EnumBits(Skill::CycloneArmor), .name = "CycloneArmor"}, + {.bits = utils::EnumBits(Skill::HeartOfWolverine), .name = "HeartOfWolverine"}, + {.bits = utils::EnumBits(Skill::SummonFenris), .name = "SummonFenris"}, + {.bits = utils::EnumBits(Skill::Rabies), .name = "Rabies"}, + {.bits = utils::EnumBits(Skill::FireClaws), .name = "FireClaws"}, + {.bits = utils::EnumBits(Skill::Twister), .name = "Twister"}, + {.bits = utils::EnumBits(Skill::Vines), .name = "Vines"}, + {.bits = utils::EnumBits(Skill::Hunger), .name = "Hunger"}, + {.bits = utils::EnumBits(Skill::ShockWave), .name = "ShockWave"}, + {.bits = utils::EnumBits(Skill::Volcano), .name = "Volcano"}, + {.bits = utils::EnumBits(Skill::Tornado), .name = "Tornado"}, + {.bits = utils::EnumBits(Skill::SpiritOfBarbs), .name = "SpiritOfBarbs"}, + {.bits = utils::EnumBits(Skill::SummonGrizzly), .name = "SummonGrizzly"}, + {.bits = utils::EnumBits(Skill::Fury), .name = "Fury"}, + {.bits = utils::EnumBits(Skill::Armageddon), .name = "Armageddon"}, + {.bits = utils::EnumBits(Skill::Hurricane), .name = "Hurricane"}, + {.bits = utils::EnumBits(Skill::FireTrauma), .name = "FireTrauma"}, + {.bits = utils::EnumBits(Skill::ClawMastery), .name = "ClawMastery"}, + {.bits = utils::EnumBits(Skill::PsychicHammer), .name = "PsychicHammer"}, + {.bits = utils::EnumBits(Skill::TigerStrike), .name = "TigerStrike"}, + {.bits = utils::EnumBits(Skill::DragonTalon), .name = "DragonTalon"}, + {.bits = utils::EnumBits(Skill::ShockField), .name = "ShockField"}, + {.bits = utils::EnumBits(Skill::BladeSentinel), .name = "BladeSentinel"}, + {.bits = utils::EnumBits(Skill::Quickness), .name = "Quickness"}, + {.bits = utils::EnumBits(Skill::FistsOfFire), .name = "FistsOfFire"}, + {.bits = utils::EnumBits(Skill::DragonClaw), .name = "DragonClaw"}, + {.bits = utils::EnumBits(Skill::ChargedBoltSentry), .name = "ChargedBoltSentry"}, + {.bits = utils::EnumBits(Skill::WakeOfFireSentry), .name = "WakeOfFireSentry"}, + {.bits = utils::EnumBits(Skill::WeaponBlock), .name = "WeaponBlock"}, + {.bits = utils::EnumBits(Skill::CloakOfShadows), .name = "CloakOfShadows"}, + {.bits = utils::EnumBits(Skill::CobraStrike), .name = "CobraStrike"}, + {.bits = utils::EnumBits(Skill::BladeFury), .name = "BladeFury"}, + {.bits = utils::EnumBits(Skill::Fade), .name = "Fade"}, + {.bits = utils::EnumBits(Skill::ShadowWarrior), .name = "ShadowWarrior"}, + {.bits = utils::EnumBits(Skill::ClawsOfThunder), .name = "ClawsOfThunder"}, + {.bits = utils::EnumBits(Skill::DragonTail), .name = "DragonTail"}, + {.bits = utils::EnumBits(Skill::LightningSentry), .name = "LightningSentry"}, + {.bits = utils::EnumBits(Skill::InfernoSentry), .name = "InfernoSentry"}, + {.bits = utils::EnumBits(Skill::MindBlast), .name = "MindBlast"}, + {.bits = utils::EnumBits(Skill::BladesOfIce), .name = "BladesOfIce"}, + {.bits = utils::EnumBits(Skill::DragonFlight), .name = "DragonFlight"}, + {.bits = utils::EnumBits(Skill::DeathSentry), .name = "DeathSentry"}, + {.bits = utils::EnumBits(Skill::BladeShield), .name = "BladeShield"}, + {.bits = utils::EnumBits(Skill::Venom), .name = "Venom"}, + {.bits = utils::EnumBits(Skill::ShadowMaster), .name = "ShadowMaster"}, + {.bits = utils::EnumBits(Skill::RoyalStrike), .name = "RoyalStrike"}, + {.bits = utils::EnumBits(Skill::WakeOfDestructionSentry), .name = "WakeOfDestructionSentry"}, + {.bits = utils::EnumBits(Skill::ImpInferno), .name = "ImpInferno"}, + {.bits = utils::EnumBits(Skill::ImpFireball), .name = "ImpFireball"}, + {.bits = utils::EnumBits(Skill::BaalTaunt), .name = "BaalTaunt"}, + {.bits = utils::EnumBits(Skill::BaalCorpseExplode), .name = "BaalCorpseExplode"}, + {.bits = utils::EnumBits(Skill::BaalMonsterSpawn), .name = "BaalMonsterSpawn"}, + {.bits = utils::EnumBits(Skill::CatapultChargedBall), .name = "CatapultChargedBall"}, + {.bits = utils::EnumBits(Skill::CatapultSpikeBall), .name = "CatapultSpikeBall"}, + {.bits = utils::EnumBits(Skill::SuckBlood), .name = "SuckBlood"}, + {.bits = utils::EnumBits(Skill::CryHelp), .name = "CryHelp"}, + {.bits = utils::EnumBits(Skill::HealingVortex), .name = "HealingVortex"}, + {.bits = utils::EnumBits(Skill::Teleport2), .name = "Teleport2"}, + {.bits = utils::EnumBits(Skill::SelfResurrect), .name = "SelfResurrect"}, + {.bits = utils::EnumBits(Skill::VineAttack), .name = "VineAttack"}, + {.bits = utils::EnumBits(Skill::OverseerWhip), .name = "OverseerWhip"}, + {.bits = utils::EnumBits(Skill::BarbsAura), .name = "BarbsAura"}, + {.bits = utils::EnumBits(Skill::WolverineAura), .name = "WolverineAura"}, + {.bits = utils::EnumBits(Skill::OakSageAura), .name = "OakSageAura"}, + {.bits = utils::EnumBits(Skill::ImpFireMissile), .name = "ImpFireMissile"}, + {.bits = utils::EnumBits(Skill::Impregnate), .name = "Impregnate"}, + {.bits = utils::EnumBits(Skill::SiegeBeastStomp), .name = "SiegeBeastStomp"}, + {.bits = utils::EnumBits(Skill::MinionSpawner), .name = "MinionSpawner"}, + {.bits = utils::EnumBits(Skill::CatapultBlizzard), .name = "CatapultBlizzard"}, + {.bits = utils::EnumBits(Skill::CatapultPlague), .name = "CatapultPlague"}, + {.bits = utils::EnumBits(Skill::CatapultMeteor), .name = "CatapultMeteor"}, + {.bits = utils::EnumBits(Skill::BoltSentry), .name = "BoltSentry"}, + {.bits = utils::EnumBits(Skill::CorpseCycler), .name = "CorpseCycler"}, + {.bits = utils::EnumBits(Skill::DeathMaul), .name = "DeathMaul"}, + {.bits = utils::EnumBits(Skill::DefenseCurse), .name = "DefenseCurse"}, + {.bits = utils::EnumBits(Skill::BloodMana), .name = "BloodMana"}, + {.bits = utils::EnumBits(Skill::MonInfernoSentry), .name = "MonInfernoSentry"}, + {.bits = utils::EnumBits(Skill::MonDeathSentry), .name = "MonDeathSentry"}, + {.bits = utils::EnumBits(Skill::SentryLightning), .name = "SentryLightning"}, + {.bits = utils::EnumBits(Skill::FenrisRage), .name = "FenrisRage"}, + {.bits = utils::EnumBits(Skill::BaalTentacle), .name = "BaalTentacle"}, + {.bits = utils::EnumBits(Skill::BaalNova), .name = "BaalNova"}, + {.bits = utils::EnumBits(Skill::BaalInferno), .name = "BaalInferno"}, + {.bits = utils::EnumBits(Skill::BaalColdMissiles), .name = "BaalColdMissiles"}, + {.bits = utils::EnumBits(Skill::MegademonInferno), .name = "MegademonInferno"}, + {.bits = utils::EnumBits(Skill::EvilHutSpawner), .name = "EvilHutSpawner"}, + {.bits = utils::EnumBits(Skill::CountessFirewall), .name = "CountessFirewall"}, + {.bits = utils::EnumBits(Skill::ImpBolt), .name = "ImpBolt"}, + {.bits = utils::EnumBits(Skill::HorrorArcticBlast), .name = "HorrorArcticBlast"}, + {.bits = utils::EnumBits(Skill::DeathSentryLtng), .name = "DeathSentryLtng"}, + {.bits = utils::EnumBits(Skill::VineCycler), .name = "VineCycler"}, + {.bits = utils::EnumBits(Skill::BearSmite), .name = "BearSmite"}, + {.bits = utils::EnumBits(Skill::Resurrect2), .name = "Resurrect2"}, + {.bits = utils::EnumBits(Skill::BloodLordFrenzy), .name = "BloodLordFrenzy"}, + {.bits = utils::EnumBits(Skill::BaalTeleport), .name = "BaalTeleport"}, + {.bits = utils::EnumBits(Skill::ImpTeleport), .name = "ImpTeleport"}, + {.bits = utils::EnumBits(Skill::BaalCloneTeleport), .name = "BaalCloneTeleport"}, + {.bits = utils::EnumBits(Skill::ZakarumLightning), .name = "ZakarumLightning"}, + {.bits = utils::EnumBits(Skill::VampireMissile), .name = "VampireMissile"}, + {.bits = utils::EnumBits(Skill::MephistoMissile), .name = "MephistoMissile"}, + {.bits = utils::EnumBits(Skill::DoomKnightMissile), .name = "DoomKnightMissile"}, + {.bits = utils::EnumBits(Skill::RogueMissile), .name = "RogueMissile"}, + {.bits = utils::EnumBits(Skill::HydraMissile), .name = "HydraMissile"}, + {.bits = utils::EnumBits(Skill::NecromageMissile), .name = "NecromageMissile"}, + {.bits = utils::EnumBits(Skill::MonBow), .name = "MonBow"}, + {.bits = utils::EnumBits(Skill::MonFireArrow), .name = "MonFireArrow"}, + {.bits = utils::EnumBits(Skill::MonColdArrow), .name = "MonColdArrow"}, + {.bits = utils::EnumBits(Skill::MonExplodingArrow), .name = "MonExplodingArrow"}, + {.bits = utils::EnumBits(Skill::MonFreezingArrow), .name = "MonFreezingArrow"}, + {.bits = utils::EnumBits(Skill::MonPowerStrike), .name = "MonPowerStrike"}, + {.bits = utils::EnumBits(Skill::SuccubusBolt), .name = "SuccubusBolt"}, + {.bits = utils::EnumBits(Skill::MephFrostNova), .name = "MephFrostNova"}, + {.bits = utils::EnumBits(Skill::MonIceSpear), .name = "MonIceSpear"}, + {.bits = utils::EnumBits(Skill::ShamanIce), .name = "ShamanIce"}, + {.bits = utils::EnumBits(Skill::Diablogeddon), .name = "Diablogeddon"}, + {.bits = utils::EnumBits(Skill::DeleriumChange), .name = "DeleriumChange"}, + {.bits = utils::EnumBits(Skill::NihlathakCorpseExplosion), .name = "NihlathakCorpseExplosion"}, + {.bits = utils::EnumBits(Skill::SerpentCharge), .name = "SerpentCharge"}, + {.bits = utils::EnumBits(Skill::TrapNova), .name = "TrapNova"}, + {.bits = utils::EnumBits(Skill::UnHolyBoltEx), .name = "UnHolyBoltEx"}, + {.bits = utils::EnumBits(Skill::ShamanFireEx), .name = "ShamanFireEx"}, + {.bits = utils::EnumBits(Skill::ImpFireMissileEx), .name = "ImpFireMissileEx"}, + {.bits = utils::EnumBits(Skill::Interact), .name = "Interact"}, + {.bits = utils::EnumBits(Skill::Loot), .name = "Loot"}, + {.bits = utils::EnumBits(Skill::TownPortal), .name = "TownPortal"}, + {.bits = utils::EnumBits(Skill::EmoteWheel), .name = "EmoteWheel"}, + {.bits = utils::EnumBits(Skill::SwapWeapons), .name = "SwapWeapons"}, + {.bits = utils::EnumBits(Skill::Map), .name = "Map"}, + {.bits = utils::EnumBits(Skill::ShowItems), .name = "ShowItems"}, + {.bits = utils::EnumBits(Skill::RunToggle), .name = "RunToggle"}, + {.bits = utils::EnumBits(Skill::MonHolyFreeze), .name = "MonHolyFreeze"}, + {.bits = utils::EnumBits(Skill::MonLeap), .name = "MonLeap"}, + {.bits = utils::EnumBits(Skill::MonLeapAttack), .name = "MonLeapAttack"}, + {.bits = utils::EnumBits(Skill::MonHolyFire), .name = "MonHolyFire"}, + {.bits = utils::EnumBits(Skill::MonHolyShock), .name = "MonHolyShock"}, + {.bits = utils::EnumBits(Skill::CubeLoot), .name = "CubeLoot"}, + {.bits = utils::EnumBits(Skill::MarkOfTheBear), .name = "MarkOfTheBear"}, + {.bits = utils::EnumBits(Skill::MarkOfTheWolf), .name = "MarkOfTheWolf"}, + {.bits = utils::EnumBits(Skill::SummonGoatman), .name = "SummonGoatman"}, + {.bits = utils::EnumBits(Skill::DemonicMastery), .name = "DemonicMastery"}, + {.bits = utils::EnumBits(Skill::DeathMark), .name = "DeathMark"}, + {.bits = utils::EnumBits(Skill::SummonTainted), .name = "SummonTainted"}, + {.bits = utils::EnumBits(Skill::SummonDefiler), .name = "SummonDefiler"}, + {.bits = utils::EnumBits(Skill::BloodOath), .name = "BloodOath"}, + {.bits = utils::EnumBits(Skill::Engorge), .name = "Engorge"}, + {.bits = utils::EnumBits(Skill::BloodBoil), .name = "BloodBoil"}, + {.bits = utils::EnumBits(Skill::Consume), .name = "Consume"}, + {.bits = utils::EnumBits(Skill::BindDemon), .name = "BindDemon"}, + {.bits = utils::EnumBits(Skill::Levitate), .name = "Levitate"}, + {.bits = utils::EnumBits(Skill::EldritchBlast), .name = "EldritchBlast"}, + {.bits = utils::EnumBits(Skill::HexBane), .name = "HexBane"}, + {.bits = utils::EnumBits(Skill::HexSiphon), .name = "HexSiphon"}, + {.bits = utils::EnumBits(Skill::PsychicWard), .name = "PsychicWard"}, + {.bits = utils::EnumBits(Skill::EchoingStrike), .name = "EchoingStrike"}, + {.bits = utils::EnumBits(Skill::HexPurge), .name = "HexPurge"}, + {.bits = utils::EnumBits(Skill::BladeWarp), .name = "BladeWarp"}, + {.bits = utils::EnumBits(Skill::Cleave), .name = "Cleave"}, + {.bits = utils::EnumBits(Skill::MirroredBlades), .name = "MirroredBlades"}, + {.bits = utils::EnumBits(Skill::SigilLethargy), .name = "SigilLethargy"}, + {.bits = utils::EnumBits(Skill::RingOfFire), .name = "RingOfFire"}, + {.bits = utils::EnumBits(Skill::MiasmaBolt), .name = "MiasmaBolt"}, + {.bits = utils::EnumBits(Skill::SigilRancor), .name = "SigilRancor"}, + {.bits = utils::EnumBits(Skill::EnhancedEntropy), .name = "EnhancedEntropy"}, + {.bits = utils::EnumBits(Skill::FlameWave), .name = "FlameWave"}, + {.bits = utils::EnumBits(Skill::MiasmaChains), .name = "MiasmaChains"}, + {.bits = utils::EnumBits(Skill::SigilDeath), .name = "SigilDeath"}, + {.bits = utils::EnumBits(Skill::Apocalypse), .name = "Apocalypse"}, + {.bits = utils::EnumBits(Skill::Abyss), .name = "Abyss"}, + {.bits = utils::EnumBits(Skill::SigilDeathExplosion), .name = "SigilDeathExplosion"}, + {.bits = utils::EnumBits(Skill::HexPurgeExplosion), .name = "HexPurgeExplosion"}, + {.bits = utils::EnumBits(Skill::HealthLink), .name = "HealthLink"}, + {.bits = utils::EnumBits(Skill::ColdFissure), .name = "ColdFissure"}, + {.bits = utils::EnumBits(Skill::KorlicsLeapAttack), .name = "KorlicsLeapAttack"}, + {.bits = utils::EnumBits(Skill::ColdEnchant), .name = "ColdEnchant"}, + {.bits = utils::EnumBits(Skill::LightningEnchant), .name = "LightningEnchant"}, + {.bits = utils::EnumBits(Skill::TalicsWhirlwind), .name = "TalicsWhirlwind"}, + {.bits = utils::EnumBits(Skill::TownportalOSkill), .name = "TownportalOSkill"}, + {.bits = utils::EnumBits(Skill::FireTwisters), .name = "FireTwisters"}, + {.bits = utils::EnumBits(Skill::ColossalVolcano), .name = "ColossalVolcano"}, + {.bits = utils::EnumBits(Skill::ColossalThunderStorm), .name = "ColossalThunderStorm"}, + {.bits = utils::EnumBits(Skill::UberAncientsHeal), .name = "UberAncientsHeal"}, + {.bits = utils::EnumBits(Skill::GoatmanStun), .name = "GoatmanStun"}, + {.bits = utils::EnumBits(Skill::GoatmanFrenzy), .name = "GoatmanFrenzy"}, + {.bits = utils::EnumBits(Skill::GoatmanBerserk), .name = "GoatmanBerserk"}, + {.bits = utils::EnumBits(Skill::GoatmanCleave), .name = "GoatmanCleave"}, + {.bits = utils::EnumBits(Skill::TaintedResistFire), .name = "TaintedResistFire"}, + {.bits = utils::EnumBits(Skill::TaintedFireBolt), .name = "TaintedFireBolt"}, + {.bits = utils::EnumBits(Skill::TaintedFireBall), .name = "TaintedFireBall"}, + {.bits = utils::EnumBits(Skill::TalicsFirePierce), .name = "TalicsFirePierce"}, + {.bits = utils::EnumBits(Skill::MadawcsLightningPierce), .name = "MadawcsLightningPierce"}, + {.bits = utils::EnumBits(Skill::KorlicsColdPierce), .name = "KorlicsColdPierce"}, + {.bits = utils::EnumBits(Skill::ChargedBoltDisk), .name = "ChargedBoltDisk"}, + {.bits = utils::EnumBits(Skill::KorlicsBash), .name = "KorlicsBash"}, + {.bits = utils::EnumBits(Skill::HeraldThorns), .name = "HeraldThorns"}, + {.bits = utils::EnumBits(Skill::Invalid), .name = "Invalid"}, + }); + return utils::NameEnumValue("Skill", ENTRIES, std::to_underlying(value)); +} + std::string EnumName(UnitType value) { static constexpr auto ENTRIES = std::to_array({ {.bits = utils::EnumBits(UnitType::Player), .name = "Player"}, @@ -620,6 +1688,154 @@ std::string EnumName(Act value) { return utils::NameEnumValue("Act", ENTRIES, std::to_underlying(value)); } +std::string EnumName(LevelId value) { + static constexpr auto ENTRIES = std::to_array({ + {.bits = utils::EnumBits(LevelId::None), .name = "None"}, + {.bits = utils::EnumBits(LevelId::RogueEncampment), .name = "RogueEncampment"}, + {.bits = utils::EnumBits(LevelId::BloodMoor), .name = "BloodMoor"}, + {.bits = utils::EnumBits(LevelId::ColdPlains), .name = "ColdPlains"}, + {.bits = utils::EnumBits(LevelId::StonyField), .name = "StonyField"}, + {.bits = utils::EnumBits(LevelId::DarkWood), .name = "DarkWood"}, + {.bits = utils::EnumBits(LevelId::BlackMarsh), .name = "BlackMarsh"}, + {.bits = utils::EnumBits(LevelId::TamoeHighland), .name = "TamoeHighland"}, + {.bits = utils::EnumBits(LevelId::DenOfEvil), .name = "DenOfEvil"}, + {.bits = utils::EnumBits(LevelId::CaveLevel1), .name = "CaveLevel1"}, + {.bits = utils::EnumBits(LevelId::UndergroundPassageLevel1), .name = "UndergroundPassageLevel1"}, + {.bits = utils::EnumBits(LevelId::HoleLevel1), .name = "HoleLevel1"}, + {.bits = utils::EnumBits(LevelId::PitLevel1), .name = "PitLevel1"}, + {.bits = utils::EnumBits(LevelId::CaveLevel2), .name = "CaveLevel2"}, + {.bits = utils::EnumBits(LevelId::UndergroundPassageLevel2), .name = "UndergroundPassageLevel2"}, + {.bits = utils::EnumBits(LevelId::HoleLevel2), .name = "HoleLevel2"}, + {.bits = utils::EnumBits(LevelId::PitLevel2), .name = "PitLevel2"}, + {.bits = utils::EnumBits(LevelId::BurialGrounds), .name = "BurialGrounds"}, + {.bits = utils::EnumBits(LevelId::Crypt), .name = "Crypt"}, + {.bits = utils::EnumBits(LevelId::Mausoleum), .name = "Mausoleum"}, + {.bits = utils::EnumBits(LevelId::ForgottenTower), .name = "ForgottenTower"}, + {.bits = utils::EnumBits(LevelId::TowerCellarLevel1), .name = "TowerCellarLevel1"}, + {.bits = utils::EnumBits(LevelId::TowerCellarLevel2), .name = "TowerCellarLevel2"}, + {.bits = utils::EnumBits(LevelId::TowerCellarLevel3), .name = "TowerCellarLevel3"}, + {.bits = utils::EnumBits(LevelId::TowerCellarLevel4), .name = "TowerCellarLevel4"}, + {.bits = utils::EnumBits(LevelId::TowerCellarLevel5), .name = "TowerCellarLevel5"}, + {.bits = utils::EnumBits(LevelId::MonasteryGate), .name = "MonasteryGate"}, + {.bits = utils::EnumBits(LevelId::OuterCloister), .name = "OuterCloister"}, + {.bits = utils::EnumBits(LevelId::Barracks), .name = "Barracks"}, + {.bits = utils::EnumBits(LevelId::JailLevel1), .name = "JailLevel1"}, + {.bits = utils::EnumBits(LevelId::JailLevel2), .name = "JailLevel2"}, + {.bits = utils::EnumBits(LevelId::JailLevel3), .name = "JailLevel3"}, + {.bits = utils::EnumBits(LevelId::InnerCloister), .name = "InnerCloister"}, + {.bits = utils::EnumBits(LevelId::Cathedral), .name = "Cathedral"}, + {.bits = utils::EnumBits(LevelId::CatacombsLevel1), .name = "CatacombsLevel1"}, + {.bits = utils::EnumBits(LevelId::CatacombsLevel2), .name = "CatacombsLevel2"}, + {.bits = utils::EnumBits(LevelId::CatacombsLevel3), .name = "CatacombsLevel3"}, + {.bits = utils::EnumBits(LevelId::CatacombsLevel4), .name = "CatacombsLevel4"}, + {.bits = utils::EnumBits(LevelId::Tristram), .name = "Tristram"}, + {.bits = utils::EnumBits(LevelId::MooMooFarm), .name = "MooMooFarm"}, + {.bits = utils::EnumBits(LevelId::LutGholein), .name = "LutGholein"}, + {.bits = utils::EnumBits(LevelId::RockyWaste), .name = "RockyWaste"}, + {.bits = utils::EnumBits(LevelId::DryHills), .name = "DryHills"}, + {.bits = utils::EnumBits(LevelId::FarOasis), .name = "FarOasis"}, + {.bits = utils::EnumBits(LevelId::LostCity), .name = "LostCity"}, + {.bits = utils::EnumBits(LevelId::ValleyOfSnakes), .name = "ValleyOfSnakes"}, + {.bits = utils::EnumBits(LevelId::CanyonOfTheMagi), .name = "CanyonOfTheMagi"}, + {.bits = utils::EnumBits(LevelId::SewersLevel1Act2), .name = "SewersLevel1Act2"}, + {.bits = utils::EnumBits(LevelId::SewersLevel2Act2), .name = "SewersLevel2Act2"}, + {.bits = utils::EnumBits(LevelId::SewersLevel3Act2), .name = "SewersLevel3Act2"}, + {.bits = utils::EnumBits(LevelId::HaremLevel1), .name = "HaremLevel1"}, + {.bits = utils::EnumBits(LevelId::HaremLevel2), .name = "HaremLevel2"}, + {.bits = utils::EnumBits(LevelId::PalaceCellarLevel1), .name = "PalaceCellarLevel1"}, + {.bits = utils::EnumBits(LevelId::PalaceCellarLevel2), .name = "PalaceCellarLevel2"}, + {.bits = utils::EnumBits(LevelId::PalaceCellarLevel3), .name = "PalaceCellarLevel3"}, + {.bits = utils::EnumBits(LevelId::StonyTombLevel1), .name = "StonyTombLevel1"}, + {.bits = utils::EnumBits(LevelId::HallsOfTheDeadLevel1), .name = "HallsOfTheDeadLevel1"}, + {.bits = utils::EnumBits(LevelId::HallsOfTheDeadLevel2), .name = "HallsOfTheDeadLevel2"}, + {.bits = utils::EnumBits(LevelId::ClawViperTempleLevel1), .name = "ClawViperTempleLevel1"}, + {.bits = utils::EnumBits(LevelId::StonyTombLevel2), .name = "StonyTombLevel2"}, + {.bits = utils::EnumBits(LevelId::HallsOfTheDeadLevel3), .name = "HallsOfTheDeadLevel3"}, + {.bits = utils::EnumBits(LevelId::ClawViperTempleLevel2), .name = "ClawViperTempleLevel2"}, + {.bits = utils::EnumBits(LevelId::MaggotLairLevel1), .name = "MaggotLairLevel1"}, + {.bits = utils::EnumBits(LevelId::MaggotLairLevel2), .name = "MaggotLairLevel2"}, + {.bits = utils::EnumBits(LevelId::MaggotLairLevel3), .name = "MaggotLairLevel3"}, + {.bits = utils::EnumBits(LevelId::AncientTunnels), .name = "AncientTunnels"}, + {.bits = utils::EnumBits(LevelId::TalRashasTomb1), .name = "TalRashasTomb1"}, + {.bits = utils::EnumBits(LevelId::TalRashasTomb2), .name = "TalRashasTomb2"}, + {.bits = utils::EnumBits(LevelId::TalRashasTomb3), .name = "TalRashasTomb3"}, + {.bits = utils::EnumBits(LevelId::TalRashasTomb4), .name = "TalRashasTomb4"}, + {.bits = utils::EnumBits(LevelId::TalRashasTomb5), .name = "TalRashasTomb5"}, + {.bits = utils::EnumBits(LevelId::TalRashasTomb6), .name = "TalRashasTomb6"}, + {.bits = utils::EnumBits(LevelId::TalRashasTomb7), .name = "TalRashasTomb7"}, + {.bits = utils::EnumBits(LevelId::DurielsLair), .name = "DurielsLair"}, + {.bits = utils::EnumBits(LevelId::ArcaneSanctuary), .name = "ArcaneSanctuary"}, + {.bits = utils::EnumBits(LevelId::KurastDocks), .name = "KurastDocks"}, + {.bits = utils::EnumBits(LevelId::SpiderForest), .name = "SpiderForest"}, + {.bits = utils::EnumBits(LevelId::GreatMarsh), .name = "GreatMarsh"}, + {.bits = utils::EnumBits(LevelId::FlayerJungle), .name = "FlayerJungle"}, + {.bits = utils::EnumBits(LevelId::LowerKurast), .name = "LowerKurast"}, + {.bits = utils::EnumBits(LevelId::KurastBazaar), .name = "KurastBazaar"}, + {.bits = utils::EnumBits(LevelId::UpperKurast), .name = "UpperKurast"}, + {.bits = utils::EnumBits(LevelId::KurastCauseway), .name = "KurastCauseway"}, + {.bits = utils::EnumBits(LevelId::Travincal), .name = "Travincal"}, + {.bits = utils::EnumBits(LevelId::SpiderCave), .name = "SpiderCave"}, + {.bits = utils::EnumBits(LevelId::SpiderCavern), .name = "SpiderCavern"}, + {.bits = utils::EnumBits(LevelId::SwampyPitLevel1), .name = "SwampyPitLevel1"}, + {.bits = utils::EnumBits(LevelId::SwampyPitLevel2), .name = "SwampyPitLevel2"}, + {.bits = utils::EnumBits(LevelId::FlayerDungeonLevel1), .name = "FlayerDungeonLevel1"}, + {.bits = utils::EnumBits(LevelId::FlayerDungeonLevel2), .name = "FlayerDungeonLevel2"}, + {.bits = utils::EnumBits(LevelId::SwampyPitLevel3), .name = "SwampyPitLevel3"}, + {.bits = utils::EnumBits(LevelId::FlayerDungeonLevel3), .name = "FlayerDungeonLevel3"}, + {.bits = utils::EnumBits(LevelId::SewersLevel1Act3), .name = "SewersLevel1Act3"}, + {.bits = utils::EnumBits(LevelId::SewersLevel2Act3), .name = "SewersLevel2Act3"}, + {.bits = utils::EnumBits(LevelId::RuinedTemple), .name = "RuinedTemple"}, + {.bits = utils::EnumBits(LevelId::DisusedFane), .name = "DisusedFane"}, + {.bits = utils::EnumBits(LevelId::ForgottenReliquary), .name = "ForgottenReliquary"}, + {.bits = utils::EnumBits(LevelId::ForgottenTemple), .name = "ForgottenTemple"}, + {.bits = utils::EnumBits(LevelId::RuinedFane), .name = "RuinedFane"}, + {.bits = utils::EnumBits(LevelId::DisusedReliquary), .name = "DisusedReliquary"}, + {.bits = utils::EnumBits(LevelId::DuranceOfHateLevel1), .name = "DuranceOfHateLevel1"}, + {.bits = utils::EnumBits(LevelId::DuranceOfHateLevel2), .name = "DuranceOfHateLevel2"}, + {.bits = utils::EnumBits(LevelId::DuranceOfHateLevel3), .name = "DuranceOfHateLevel3"}, + {.bits = utils::EnumBits(LevelId::ThePandemoniumFortress), .name = "ThePandemoniumFortress"}, + {.bits = utils::EnumBits(LevelId::OuterSteppes), .name = "OuterSteppes"}, + {.bits = utils::EnumBits(LevelId::PlainsOfDespair), .name = "PlainsOfDespair"}, + {.bits = utils::EnumBits(LevelId::CityOfTheDamned), .name = "CityOfTheDamned"}, + {.bits = utils::EnumBits(LevelId::RiverOfFlame), .name = "RiverOfFlame"}, + {.bits = utils::EnumBits(LevelId::ChaosSanctuary), .name = "ChaosSanctuary"}, + {.bits = utils::EnumBits(LevelId::Harrogath), .name = "Harrogath"}, + {.bits = utils::EnumBits(LevelId::BloodyFoothills), .name = "BloodyFoothills"}, + {.bits = utils::EnumBits(LevelId::FrigidHighlands), .name = "FrigidHighlands"}, + {.bits = utils::EnumBits(LevelId::ArreatPlateau), .name = "ArreatPlateau"}, + {.bits = utils::EnumBits(LevelId::CrystallinePassage), .name = "CrystallinePassage"}, + {.bits = utils::EnumBits(LevelId::FrozenRiver), .name = "FrozenRiver"}, + {.bits = utils::EnumBits(LevelId::GlacialTrail), .name = "GlacialTrail"}, + {.bits = utils::EnumBits(LevelId::DrifterCavern), .name = "DrifterCavern"}, + {.bits = utils::EnumBits(LevelId::FrozenTundra), .name = "FrozenTundra"}, + {.bits = utils::EnumBits(LevelId::TheAncientsWay), .name = "TheAncientsWay"}, + {.bits = utils::EnumBits(LevelId::IcyCellar), .name = "IcyCellar"}, + {.bits = utils::EnumBits(LevelId::ArreatSummit), .name = "ArreatSummit"}, + {.bits = utils::EnumBits(LevelId::NihlathaksTemple), .name = "NihlathaksTemple"}, + {.bits = utils::EnumBits(LevelId::HallsOfAnguish), .name = "HallsOfAnguish"}, + {.bits = utils::EnumBits(LevelId::HallsOfPain), .name = "HallsOfPain"}, + {.bits = utils::EnumBits(LevelId::HallsOfVaught), .name = "HallsOfVaught"}, + {.bits = utils::EnumBits(LevelId::Abaddon), .name = "Abaddon"}, + {.bits = utils::EnumBits(LevelId::PitOfAcheron), .name = "PitOfAcheron"}, + {.bits = utils::EnumBits(LevelId::InfernalPit), .name = "InfernalPit"}, + {.bits = utils::EnumBits(LevelId::TheWorldStoneKeepLevel1), .name = "TheWorldStoneKeepLevel1"}, + {.bits = utils::EnumBits(LevelId::TheWorldStoneKeepLevel2), .name = "TheWorldStoneKeepLevel2"}, + {.bits = utils::EnumBits(LevelId::TheWorldStoneKeepLevel3), .name = "TheWorldStoneKeepLevel3"}, + {.bits = utils::EnumBits(LevelId::ThroneOfDestruction), .name = "ThroneOfDestruction"}, + {.bits = utils::EnumBits(LevelId::TheWorldstoneChamber), .name = "TheWorldstoneChamber"}, + {.bits = utils::EnumBits(LevelId::MatronsDen), .name = "MatronsDen"}, + {.bits = utils::EnumBits(LevelId::ForgottenSands), .name = "ForgottenSands"}, + {.bits = utils::EnumBits(LevelId::FurnaceOfPain), .name = "FurnaceOfPain"}, + {.bits = utils::EnumBits(LevelId::UberTristram), .name = "UberTristram"}, + {.bits = utils::EnumBits(LevelId::MapsAncientTemple), .name = "MapsAncientTemple"}, + {.bits = utils::EnumBits(LevelId::MapsDesecratedTemple), .name = "MapsDesecratedTemple"}, + {.bits = utils::EnumBits(LevelId::MapsFrigidPlateau), .name = "MapsFrigidPlateau"}, + {.bits = utils::EnumBits(LevelId::MapsInfernalTrial), .name = "MapsInfernalTrial"}, + {.bits = utils::EnumBits(LevelId::MapsRuinedCitadel), .name = "MapsRuinedCitadel"}, + }); + return utils::NameEnumValue("LevelId", ENTRIES, std::to_underlying(value)); +} + std::string EnumName(PlayerMode value) { static constexpr auto ENTRIES = std::to_array({ {.bits = utils::EnumBits(PlayerMode::Death), .name = "Death"}, @@ -712,11 +1928,58 @@ std::string EnumName(UnitFlagEx value) { {.bits = utils::EnumBits(UnitFlagEx::NoDraw), .name = "NoDraw"}, {.bits = utils::EnumBits(UnitFlagEx::IsExpansion), .name = "IsExpansion"}, {.bits = utils::EnumBits(UnitFlagEx::ServerUnit), .name = "ServerUnit"}, - {.bits = utils::EnumBits(UnitFlagEx::SharedStash), .name = "SharedStash"}, + {.bits = utils::EnumBits(UnitFlagEx::AnimEndOfCycle), .name = "AnimEndOfCycle"}, }); return utils::NameEnumValue("UnitFlagEx", ENTRIES, std::to_underlying(value)); } +std::string EnumName(Quest value) { + static constexpr auto ENTRIES = std::to_array({ + {.bits = utils::EnumBits(Quest::Act1Prologue), .name = "Act1Prologue"}, + {.bits = utils::EnumBits(Quest::DenOfEvil), .name = "DenOfEvil"}, + {.bits = utils::EnumBits(Quest::SistersBurialGrounds), .name = "SistersBurialGrounds"}, + {.bits = utils::EnumBits(Quest::ToolsOfTheTrade), .name = "ToolsOfTheTrade"}, + {.bits = utils::EnumBits(Quest::TheSearchForCain), .name = "TheSearchForCain"}, + {.bits = utils::EnumBits(Quest::TheForgottenTower), .name = "TheForgottenTower"}, + {.bits = utils::EnumBits(Quest::SistersToTheSlaughter), .name = "SistersToTheSlaughter"}, + {.bits = utils::EnumBits(Quest::Act1Completed), .name = "Act1Completed"}, + {.bits = utils::EnumBits(Quest::Act2Prologue), .name = "Act2Prologue"}, + {.bits = utils::EnumBits(Quest::RadamentsLair), .name = "RadamentsLair"}, + {.bits = utils::EnumBits(Quest::TheHoradricStaff), .name = "TheHoradricStaff"}, + {.bits = utils::EnumBits(Quest::TaintedSun), .name = "TaintedSun"}, + {.bits = utils::EnumBits(Quest::ArcaneSanctuary), .name = "ArcaneSanctuary"}, + {.bits = utils::EnumBits(Quest::TheSummoner), .name = "TheSummoner"}, + {.bits = utils::EnumBits(Quest::TheSevenTombs), .name = "TheSevenTombs"}, + {.bits = utils::EnumBits(Quest::Act2Completed), .name = "Act2Completed"}, + {.bits = utils::EnumBits(Quest::Act3Prologue), .name = "Act3Prologue"}, + {.bits = utils::EnumBits(Quest::LamEsensTome), .name = "LamEsensTome"}, + {.bits = utils::EnumBits(Quest::KhalimsWill), .name = "KhalimsWill"}, + {.bits = utils::EnumBits(Quest::BladeOfTheOldReligion), .name = "BladeOfTheOldReligion"}, + {.bits = utils::EnumBits(Quest::TheGoldenBird), .name = "TheGoldenBird"}, + {.bits = utils::EnumBits(Quest::TheBlackenedTemple), .name = "TheBlackenedTemple"}, + {.bits = utils::EnumBits(Quest::TheGuardian), .name = "TheGuardian"}, + {.bits = utils::EnumBits(Quest::Act3Completed), .name = "Act3Completed"}, + {.bits = utils::EnumBits(Quest::Act4Prologue), .name = "Act4Prologue"}, + {.bits = utils::EnumBits(Quest::TheFallenAngel), .name = "TheFallenAngel"}, + {.bits = utils::EnumBits(Quest::TerrorsEnd), .name = "TerrorsEnd"}, + {.bits = utils::EnumBits(Quest::HellsForge), .name = "HellsForge"}, + {.bits = utils::EnumBits(Quest::Act4Completed), .name = "Act4Completed"}, + {.bits = utils::EnumBits(Quest::Act1Navi), .name = "Act1Navi"}, + {.bits = utils::EnumBits(Quest::Act2GuardGossip1), .name = "Act2GuardGossip1"}, + {.bits = utils::EnumBits(Quest::Act2GuardGossip2), .name = "Act2GuardGossip2"}, + {.bits = utils::EnumBits(Quest::DarkWanderer), .name = "DarkWanderer"}, + {.bits = utils::EnumBits(Quest::Malachai), .name = "Malachai"}, + {.bits = utils::EnumBits(Quest::SiegeOnHarrogath), .name = "SiegeOnHarrogath"}, + {.bits = utils::EnumBits(Quest::RescueOnMountArreat), .name = "RescueOnMountArreat"}, + {.bits = utils::EnumBits(Quest::PrisonOfIce), .name = "PrisonOfIce"}, + {.bits = utils::EnumBits(Quest::BetrayalOfHarrogath), .name = "BetrayalOfHarrogath"}, + {.bits = utils::EnumBits(Quest::RiteOfPassage), .name = "RiteOfPassage"}, + {.bits = utils::EnumBits(Quest::EveOfDestruction), .name = "EveOfDestruction"}, + {.bits = utils::EnumBits(Quest::Respec), .name = "Respec"}, + }); + return utils::NameEnumValue("Quest", ENTRIES, std::to_underlying(value)); +} + std::string EnumName(QuestFlag value) { static constexpr auto ENTRIES = std::to_array({ {.bits = utils::EnumBits(QuestFlag::RewardGranted), .name = "RewardGranted"}, @@ -743,6 +2006,7 @@ std::string EnumName(GameType value) { static constexpr auto ENTRIES = std::to_array({ {.bits = utils::EnumBits(GameType::Classic), .name = "Classic"}, {.bits = utils::EnumBits(GameType::Expansion), .name = "Expansion"}, + {.bits = utils::EnumBits(GameType::RotW), .name = "RotW"}, }); return utils::NameEnumValue("GameType", ENTRIES, std::to_underlying(value)); } @@ -771,6 +2035,24 @@ std::string EnumName(MoveMode value) { return utils::NameEnumValue("MoveMode", ENTRIES, std::to_underlying(value)); } +std::string EnumName(Language value) { + static constexpr auto ENTRIES = std::to_array({ + {.bits = utils::EnumBits(Language::English), .name = "English"}, + {.bits = utils::EnumBits(Language::Spanish), .name = "Spanish"}, + {.bits = utils::EnumBits(Language::German), .name = "German"}, + {.bits = utils::EnumBits(Language::French), .name = "French"}, + {.bits = utils::EnumBits(Language::Portuguese), .name = "Portuguese"}, + {.bits = utils::EnumBits(Language::Italian), .name = "Italian"}, + {.bits = utils::EnumBits(Language::Japanese), .name = "Japanese"}, + {.bits = utils::EnumBits(Language::Korean), .name = "Korean"}, + {.bits = utils::EnumBits(Language::ChineseSimplified), .name = "ChineseSimplified"}, + {.bits = utils::EnumBits(Language::ChineseTraditional), .name = "ChineseTraditional"}, + {.bits = utils::EnumBits(Language::Polish), .name = "Polish"}, + {.bits = utils::EnumBits(Language::Russian), .name = "Russian"}, + }); + return utils::NameEnumValue("Language", ENTRIES, std::to_underlying(value)); +} + // NOLINTBEGIN(readability-identifier-naming) - fmt's customisation point name std::string format_as(CharFlag value) { @@ -873,6 +2155,22 @@ std::string format_as(LoginStatus value) { return EnumName(value); } +std::string format_as(Stat value) { + return EnumName(value); +} + +std::string format_as(StatListFlags value) { + return EnumName(value); +} + +std::string format_as(State value) { + return EnumName(value); +} + +std::string format_as(Skill value) { + return EnumName(value); +} + std::string format_as(UnitType value) { return EnumName(value); } @@ -921,6 +2219,10 @@ std::string format_as(Act value) { return EnumName(value); } +std::string format_as(LevelId value) { + return EnumName(value); +} + std::string format_as(PlayerMode value) { return EnumName(value); } @@ -941,6 +2243,10 @@ std::string format_as(UnitFlagEx value) { return EnumName(value); } +std::string format_as(Quest value) { + return EnumName(value); +} + std::string format_as(QuestFlag value) { return EnumName(value); } @@ -961,6 +2267,10 @@ std::string format_as(MoveMode value) { return EnumName(value); } +std::string format_as(Language value) { + return EnumName(value); +} + // NOLINTEND(readability-identifier-naming) } // namespace d2bs::game diff --git a/src/contract/ContractEnumNames.h b/src/contract/ContractEnumNames.h index 4e5ca5f..7ab2b34 100644 --- a/src/contract/ContractEnumNames.h +++ b/src/contract/ContractEnumNames.h @@ -71,6 +71,10 @@ enum class Difficulty : uint32_t; enum class GameState : uint32_t; enum class IpcMode : uint32_t; enum class LoginStatus : uint8_t; +enum class Stat : uint32_t; +enum class StatListFlags : uint32_t; +enum class State : uint32_t; +enum class Skill : uint16_t; enum class UnitType : uint32_t; enum class UnitKind : uint32_t; enum class NodePage : uint8_t; @@ -83,16 +87,19 @@ enum class MonsterSpecType : uint32_t; enum class CollisionFlag : uint16_t; enum class ItemFlag : uint32_t; enum class Act : uint8_t; +enum class LevelId : uint32_t; enum class PlayerMode : uint32_t; enum class MonsterMode : uint32_t; enum class ObjectMode : uint32_t; enum class ItemMode : uint32_t; enum class UnitFlagEx : uint32_t; +enum class Quest : uint32_t; enum class QuestFlag : uint32_t; enum class GameType : uint32_t; enum class ScreenSize : uint32_t; enum class WeaponSet : uint32_t; enum class MoveMode : uint32_t; +enum class Language : uint32_t; // A human-readable name for an enum value: the enumerator that has it (the first declared one // for aliases), else the single-bit enumerators that together make it up exactly as "A|B", @@ -122,6 +129,10 @@ enum class MoveMode : uint32_t; [[nodiscard]] std::string EnumName(GameState value); [[nodiscard]] std::string EnumName(IpcMode value); [[nodiscard]] std::string EnumName(LoginStatus value); +[[nodiscard]] std::string EnumName(Stat value); +[[nodiscard]] std::string EnumName(StatListFlags value); +[[nodiscard]] std::string EnumName(State value); +[[nodiscard]] std::string EnumName(Skill value); [[nodiscard]] std::string EnumName(UnitType value); [[nodiscard]] std::string EnumName(UnitKind value); [[nodiscard]] std::string EnumName(NodePage value); @@ -134,16 +145,19 @@ enum class MoveMode : uint32_t; [[nodiscard]] std::string EnumName(CollisionFlag value); [[nodiscard]] std::string EnumName(ItemFlag value); [[nodiscard]] std::string EnumName(Act value); +[[nodiscard]] std::string EnumName(LevelId value); [[nodiscard]] std::string EnumName(PlayerMode value); [[nodiscard]] std::string EnumName(MonsterMode value); [[nodiscard]] std::string EnumName(ObjectMode value); [[nodiscard]] std::string EnumName(ItemMode value); [[nodiscard]] std::string EnumName(UnitFlagEx value); +[[nodiscard]] std::string EnumName(Quest value); [[nodiscard]] std::string EnumName(QuestFlag value); [[nodiscard]] std::string EnumName(GameType value); [[nodiscard]] std::string EnumName(ScreenSize value); [[nodiscard]] std::string EnumName(WeaponSet value); [[nodiscard]] std::string EnumName(MoveMode value); +[[nodiscard]] std::string EnumName(Language value); // NOLINTBEGIN(readability-identifier-naming) - fmt's customisation point name [[nodiscard]] std::string format_as(CharFlag value); @@ -171,6 +185,10 @@ enum class MoveMode : uint32_t; [[nodiscard]] std::string format_as(GameState value); [[nodiscard]] std::string format_as(IpcMode value); [[nodiscard]] std::string format_as(LoginStatus value); +[[nodiscard]] std::string format_as(Stat value); +[[nodiscard]] std::string format_as(StatListFlags value); +[[nodiscard]] std::string format_as(State value); +[[nodiscard]] std::string format_as(Skill value); [[nodiscard]] std::string format_as(UnitType value); [[nodiscard]] std::string format_as(UnitKind value); [[nodiscard]] std::string format_as(NodePage value); @@ -183,16 +201,19 @@ enum class MoveMode : uint32_t; [[nodiscard]] std::string format_as(CollisionFlag value); [[nodiscard]] std::string format_as(ItemFlag value); [[nodiscard]] std::string format_as(Act value); +[[nodiscard]] std::string format_as(LevelId value); [[nodiscard]] std::string format_as(PlayerMode value); [[nodiscard]] std::string format_as(MonsterMode value); [[nodiscard]] std::string format_as(ObjectMode value); [[nodiscard]] std::string format_as(ItemMode value); [[nodiscard]] std::string format_as(UnitFlagEx value); +[[nodiscard]] std::string format_as(Quest value); [[nodiscard]] std::string format_as(QuestFlag value); [[nodiscard]] std::string format_as(GameType value); [[nodiscard]] std::string format_as(ScreenSize value); [[nodiscard]] std::string format_as(WeaponSet value); [[nodiscard]] std::string format_as(MoveMode value); +[[nodiscard]] std::string format_as(Language value); // NOLINTEND(readability-identifier-naming) // Bit operations over the @flags enumerations: a value is an OR of their bits, and stays @@ -254,6 +275,34 @@ enum class MoveMode : uint32_t; return utils::FlagHasAny(value, flag); } +[[nodiscard, gnu::always_inline]] constexpr StatListFlags operator|(StatListFlags lhs, StatListFlags rhs) { + return utils::FlagOr(lhs, rhs); +} +[[nodiscard, gnu::always_inline]] constexpr StatListFlags operator&(StatListFlags lhs, StatListFlags rhs) { + return utils::FlagAnd(lhs, rhs); +} +[[nodiscard, gnu::always_inline]] constexpr StatListFlags operator^(StatListFlags lhs, StatListFlags rhs) { + return utils::FlagXor(lhs, rhs); +} +[[nodiscard, gnu::always_inline]] constexpr StatListFlags operator~(StatListFlags value) { + return utils::FlagNot(value); +} +[[gnu::always_inline]] constexpr StatListFlags& operator|=(StatListFlags& lhs, StatListFlags rhs) { + return lhs = lhs | rhs; +} +[[gnu::always_inline]] constexpr StatListFlags& operator&=(StatListFlags& lhs, StatListFlags rhs) { + return lhs = lhs & rhs; +} +[[gnu::always_inline]] constexpr StatListFlags& operator^=(StatListFlags& lhs, StatListFlags rhs) { + return lhs = lhs ^ rhs; +} +[[nodiscard, gnu::always_inline]] constexpr bool HasFlag(StatListFlags value, StatListFlags flag) { + return utils::FlagHas(value, flag); +} +[[nodiscard, gnu::always_inline]] constexpr bool HasAnyFlag(StatListFlags value, StatListFlags flag) { + return utils::FlagHasAny(value, flag); +} + [[nodiscard, gnu::always_inline]] constexpr MonsterSpecType operator|(MonsterSpecType lhs, MonsterSpecType rhs) { return utils::FlagOr(lhs, rhs); } diff --git a/src/contract/game/Common.cpp b/src/contract/game/Common.cpp index 8c24796..0e12d1a 100644 --- a/src/contract/game/Common.cpp +++ b/src/contract/game/Common.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include "game/Constants.h" @@ -35,15 +36,15 @@ uint32_t PricingNpcClassId(uint32_t npcClassId) { return row == nullptr || *row == 0 ? NPC_CHARSI_CLASS_ID : npcClassId; } -bool IsWaypointLevel(uint32_t levelId) { +bool IsWaypointLevel(LevelId levelId) { constexpr int64_t NO_WAYPOINT = 255; - const auto waypoint = GetTxtValue("levels", levelId, "Waypoint"); + const auto waypoint = GetTxtValue("levels", std::to_underlying(levelId), "Waypoint"); const auto* index = std::get_if(&waypoint); return index == nullptr || *index != NO_WAYPOINT; } -std::optional SkillNameStringId(uint16_t skillId) { - const auto descRow = GetTxtValue("skills", skillId, "skilldesc"); +std::optional SkillNameStringId(Skill skill) { + const auto descRow = GetTxtValue("skills", std::to_underlying(skill), "skilldesc"); const auto* desc = std::get_if(&descRow); if (desc == nullptr) { return std::nullopt; diff --git a/src/contract/game/Common.h b/src/contract/game/Common.h index b338fa8..65a2e4a 100644 --- a/src/contract/game/Common.h +++ b/src/contract/game/Common.h @@ -8,6 +8,8 @@ #include #include +#include "game/Types.h" + namespace d2bs::game { // Reference parity: an NPC class without a monstats inventory prices as Charsi, @@ -17,11 +19,11 @@ uint32_t PricingNpcClassId(uint32_t npcClassId); // Reference parity: levels.txt carries 255 in `Waypoint` for a level no // waypoint reaches, and sending the game there can take it down. A row the // lookup cannot read as a number is not rejected. -bool IsWaypointLevel(uint32_t levelId); +bool IsWaypointLevel(LevelId levelId); // String-table index of a skill's display name, through the skills -> // skilldesc -> `str name` chain the client itself uses. nullopt when a link // of the chain is missing. -std::optional SkillNameStringId(uint16_t skillId); +std::optional SkillNameStringId(Skill skill); } // namespace d2bs::game diff --git a/src/contract/game/Constants.h b/src/contract/game/Constants.h index c0a4d57..77c16a7 100644 --- a/src/contract/game/Constants.h +++ b/src/contract/game/Constants.h @@ -9,18 +9,8 @@ namespace d2bs::game { -// Stat IDs referenced in the API layer. -// Values from reference/d2bs/Constants.h and JSUnit.cpp. -constexpr uint32_t STAT_EXP = 13; -constexpr uint32_t STAT_LASTEXP = 29; -constexpr uint32_t STAT_NEXTEXP = 30; -constexpr uint32_t STAT_ITEMLEVELREQ = 92; +// Fractional bits of the fixed-point stats (IsFixedPointStat). constexpr uint32_t STAT_FIXED_POINT_SHIFT = 8; -// hp/mana/stamina (STAT_HITPOINTS..STAT_MAXSTAMINA) are the contiguous run D2 stores -// in 8.8 fixed point. -constexpr uint32_t STAT_FIXED_POINT_FIRST = 6; -constexpr uint32_t STAT_FIXED_POINT_LAST = 11; -constexpr uint32_t STAT_LIST_PRESET_FLAG = 0x40; // Default NPC class ID for pricing (Charsi). constexpr uint32_t NPC_CHARSI_CLASS_ID = 0x9A; diff --git a/src/contract/game/Control.h b/src/contract/game/Control.h index a95b11e..19f7dc6 100644 --- a/src/contract/game/Control.h +++ b/src/contract/game/Control.h @@ -51,7 +51,7 @@ class Control { // Reference-parity locale match against the resolved locale string: Buttons // exact-match their text; TextBoxes substring-match pFirstText. False for // control types with no locale text source. - bool HasLocaleText(int32_t localeId) const; + bool HasLocaleText(uint16_t localeId) const; // Methods Control GetNext() const; @@ -79,7 +79,7 @@ class Control { std::optional y = std::nullopt, std::optional xsize = std::nullopt, std::optional ysize = std::nullopt, - std::optional localeId = std::nullopt); + std::optional localeId = std::nullopt); }; } // namespace d2bs::game diff --git a/src/contract/game/Finders.h b/src/contract/game/Finders.h index ad5ad1e..374ad25 100644 --- a/src/contract/game/Finders.h +++ b/src/contract/game/Finders.h @@ -228,7 +228,7 @@ inline std::optional Level::FindRoomAt(Position pos) const { inline std::vector Level::GetPresetUnits(std::optional type, std::optional classId) const { std::vector out; - const uint32_t levelId = Id(); + const LevelId levelId = Id(); for (auto r = GetFirstRoom(); r; r = r.GetNext()) { auto roomPresets = r.GetPresetUnits(type, classId); for (auto& pu : roomPresets) { @@ -241,7 +241,7 @@ inline std::vector Level::GetPresetUnits(std::optional inline std::optional Level::FindFirstPresetUnit(std::optional type, std::optional classId) const { - const uint32_t levelId = Id(); + const LevelId levelId = Id(); for (auto r = GetFirstRoom(); r; r = r.GetNext()) { auto roomPresets = r.GetPresetUnits(type, classId); if (!roomPresets.empty()) { @@ -289,7 +289,7 @@ inline std::optional Party::FindByName(const std::string& name) { // Text / Disabled constraints from reference are not exposed via our JS API. inline std::optional Control::Find(std::optional type, std::optional x, std::optional y, std::optional xsize, - std::optional ysize, std::optional localeId) { + std::optional ysize, std::optional localeId) { // One read lock for the whole walk: inner accessor locks collapse to // free recursive re-entries, and the control list can't shift mid-iteration. GameReadLock guard; diff --git a/src/contract/game/GameHelpers.h b/src/contract/game/GameHelpers.h index 6471603..ea9e13b 100644 --- a/src/contract/game/GameHelpers.h +++ b/src/contract/game/GameHelpers.h @@ -43,9 +43,9 @@ void SetAlwaysRun(bool value); bool GetNoPickUp(); void SetNoPickUp(bool value); uint32_t GetWeaponSwitch(); -uint32_t GetGameType(); +GameType GetGameType(); uint32_t GetMercReviveCost(); -uint32_t GetLocale(); +Language GetLocale(); // === BnetData Queries === std::string GetAccountName(); @@ -113,7 +113,7 @@ bool SubmitItem(const Unit& item); void Transmute(); bool TestPvpFlag(const Unit& a, const Unit& b, RosterFlag flag); bool HasWaypoint(uint32_t waypointId); -bool IsTownByLevelNo(uint32_t levelNo); +bool IsTownByLevelNo(LevelId levelNo); std::string GetLocaleString(uint16_t localeId); // Result of a cell lookup in a game data .txt table (skills / monstats / itemstatcost / etc.). @@ -129,11 +129,11 @@ TxtValue GetTxtValue(std::string_view table, uint32_t row, std::string_view colu // unknown or its game data is not currently loaded (e.g. out of game). std::optional GetTxtTableRowCount(std::string_view table); -bool GetQuestFlag(uint32_t quest, QuestFlag flag); +bool GetQuestFlag(Quest quest, QuestFlag flag); // === Weapon / Stat / Skill Actions === void SwapWeapon(); -void UseStatPoint(uint32_t stat, uint32_t count); -void UseSkillPoint(uint32_t skill, uint32_t count); +void UseStatPoint(Stat stat, uint16_t count); +void UseSkillPoint(Skill skill, uint16_t count); void TakeScreenshot(); // === Item Actions === @@ -175,7 +175,7 @@ void ClickPartyMember(const Party& party, PartyMode mode); void LeaveParty(); uint32_t CheckUnitCollision(const Unit& unit1, const Unit& unit2, CollisionFlag mask); // === Skill Name Tables === -std::optional GetSkillByName(std::string_view name); +std::optional GetSkillByName(std::string_view name); // === IPC === // Send an IPC message. If targetHwnd != 0, send to that target directly. @@ -187,9 +187,9 @@ int32_t SendIPC(uint32_t mode, std::string_view data, uintptr_t targetHwnd = 0, void PlayGameSound(uint32_t soundId); void* GetHwnd(); -void GoldAction(GoldActionMode mode, int32_t amount); +void GoldAction(GoldActionMode mode, uint32_t amount); void MoveNPC(uint32_t npcId, Position pos); -bool RevealLevel(uint32_t levelNo, bool drawPresets = false); +bool RevealLevel(LevelId levelNo, bool drawPresets = false); // === NPC Interaction === bool IsScrollingText(); @@ -265,8 +265,8 @@ std::vector GetActiveFeatures(); // it, the monster level it raises the area to, what activated it, and the // zone's configured always-unique modifier (a monumod.txt id, 0 = none). struct DesecratedZone { - uint32_t area = 0; - uint32_t waypointArea = 0; + LevelId area = LevelId::None; + LevelId waypointArea = LevelId::None; uint32_t monsterLevel = 0; DesecratedZoneSource source = DesecratedZoneSource::Rotation; uint32_t uniqueMod = 0; diff --git a/src/contract/game/Level.h b/src/contract/game/Level.h index dfe764f..b56c1a5 100644 --- a/src/contract/game/Level.h +++ b/src/contract/game/Level.h @@ -20,7 +20,7 @@ class Room; // The game stores levels as subtiles (1 subtile = 5 game-coords); the conversion // happens in the game-layer implementation. See docs/coords.md. class Level { - uint32_t id_ = 0; + LevelId id_ = LevelId::None; HandleCache cache_; void* ResolvePtr() const; @@ -32,11 +32,11 @@ class Level { } public: - explicit Level(uint32_t id = 0) : id_(id) {} + explicit Level(LevelId id = LevelId::None) : id_(id) {} explicit operator bool() const; // Properties - uint32_t Id() const { return id_; } + LevelId Id() const { return id_; } std::string Name() const; Rect Bounds() const; @@ -45,7 +45,7 @@ class Level { // Factory - resolves a Level from the current act's misc data. // Returns std::nullopt if the level does not exist. - static std::optional Get(uint32_t levelNo); + static std::optional Get(LevelId levelNo); // === Framework-impl (defined inline in Finders.h) === // Find the room at `pos` (game coordinates) within this level. diff --git a/src/contract/game/Menu.h b/src/contract/game/Menu.h index fe8e157..ad40024 100644 --- a/src/contract/game/Menu.h +++ b/src/contract/game/Menu.h @@ -122,9 +122,11 @@ bool SelectCharacter(const std::string& charName); // Create a new character on the char-select screen. Reference: // reference/d2bs/Control.cpp:134-177 (OOG_CreateCharacter). Returns true on -// success, false on any failure. Reference has no isExpansion parameter - -// LoD-vs-classic isn't part of the JS API surface. -bool CreateCharacter(const std::string& name, CharacterClass charClass, bool isHardcore, bool isLadder); +// success, false on any failure, including a game type the backend cannot +// create. The caller has already rejected combinations the game refuses +// (Warlock outside RotW, Classic Assassin / Druid). +bool CreateCharacter(const std::string& name, CharacterClass charClass, bool isHardcore, bool isLadder, + GameType gameType); // Create a Battle.net / open game. Reference: // reference/d2bs/Control.cpp:434-546 (OOG_CreateGame). Returns true on diff --git a/src/contract/game/Party.h b/src/contract/game/Party.h index 4786742..a572794 100644 --- a/src/contract/game/Party.h +++ b/src/contract/game/Party.h @@ -33,14 +33,14 @@ class Party { // Properties Position Pos() const; - uint32_t LevelId() const; + LevelId LevelId() const; uint32_t Id() const; uint32_t Life() const; PartyState PartyState() const; uint16_t PartyId() const; std::string Name() const; - uint32_t ClassId() const; - uint32_t CharacterLevel() const; + CharacterClass ClassId() const; + uint16_t CharacterLevel() const; // Traversal Party GetNext() const; diff --git a/src/contract/game/Room.h b/src/contract/game/Room.h index b6d76db..c6bfc6b 100644 --- a/src/contract/game/Room.h +++ b/src/contract/game/Room.h @@ -20,7 +20,7 @@ class Unit; // The game stores rooms as subtiles (1 subtile = 5 game-coords); the conversion // happens in the game-layer implementation. See docs/coords.md. class Room { - uint32_t level_ = 0; + LevelId level_ = game::LevelId::None; Position pos_; HandleCache cache_; @@ -33,7 +33,7 @@ class Room { } public: - explicit Room(uint32_t level = 0, Position pos = {}) : level_(level), pos_(pos) {} + explicit Room(LevelId level = game::LevelId::None, Position pos = {}) : level_(level), pos_(pos) {} explicit operator bool() const; bool operator==(const Room& other) const; @@ -45,8 +45,8 @@ class Room { int32_t Number() const; int32_t SubNumber() const; Rect Bounds() const; - uint32_t LevelId() const { return level_; } - uint32_t CorrectTomb() const; + LevelId LevelId() const { return level_; } + game::LevelId CorrectTomb() const; // Collision (via the room's ActiveRoom side, with AddRoomData / // RemoveRoomData if needed). diff --git a/src/contract/game/Types.h b/src/contract/game/Types.h index 341df5b..47dc133 100644 --- a/src/contract/game/Types.h +++ b/src/contract/game/Types.h @@ -271,6 +271,1084 @@ struct LoginResult { std::string errorMessage; }; +// ============================================================================ +// Stats / states / skills +// ============================================================================ + +// Stat ids: the rows of itemstatcost.txt. Later versions keep every 1.14d id and only append +// or fill slots 1.14d left unused; names follow the latest rows. +enum class Stat : uint32_t { + Strength = 0, + Energy = 1, + Dexterity = 2, + Vitality = 3, + StatPoints = 4, + NewSkills = 5, + HitPoints = 6, + MaxHitPoints = 7, + Mana = 8, + MaxMana = 9, + Stamina = 10, + MaxStamina = 11, + Level = 12, + Experience = 13, + Gold = 14, + GoldBank = 15, + ItemArmorPercent = 16, + ItemMaxDamagePercent = 17, + ItemMinDamagePercent = 18, + ToHit = 19, + ToBlock = 20, + MinDamage = 21, + MaxDamage = 22, + SecondaryMinDamage = 23, + SecondaryMaxDamage = 24, + DamagePercent = 25, + ManaRecovery = 26, + ManaRecoveryBonus = 27, + StaminaRecoveryBonus = 28, + LastExperience = 29, + NextExperience = 30, + ArmorClass = 31, + ArmorClassVsMissile = 32, + ArmorClassVsHandToHand = 33, + NormalDamageReduction = 34, + MagicDamageReduction = 35, + DamageResist = 36, + MagicResist = 37, + MaxMagicResist = 38, + FireResist = 39, + MaxFireResist = 40, + LightningResist = 41, + MaxLightningResist = 42, + ColdResist = 43, + MaxColdResist = 44, + PoisonResist = 45, + MaxPoisonResist = 46, + DamageAura = 47, + FireMinDamage = 48, + FireMaxDamage = 49, + LightningMinDamage = 50, + LightningMaxDamage = 51, + MagicMinDamage = 52, + MagicMaxDamage = 53, + ColdMinDamage = 54, + ColdMaxDamage = 55, + ColdLength = 56, + PoisonMinDamage = 57, + PoisonMaxDamage = 58, + PoisonLength = 59, + LifeDrainMinDamage = 60, + LifeDrainMaxDamage = 61, + ManaDrainMinDamage = 62, + ManaDrainMaxDamage = 63, + StaminaDrainMinDamage = 64, + StaminaDrainMaxDamage = 65, + StunLength = 66, + VelocityPercent = 67, + AttackRate = 68, + OtherAnimRate = 69, + Quantity = 70, + Value = 71, + Durability = 72, + MaxDurability = 73, + HpRegen = 74, + ItemMaxDurabilityPercent = 75, + ItemMaxHpPercent = 76, + ItemMaxManaPercent = 77, + ItemAttackerTakesDamage = 78, + ItemGoldBonus = 79, + ItemMagicBonus = 80, + ItemKnockback = 81, + ItemTimeDuration = 82, + ItemAddClassSkills = 83, + UnsentParam1 = 84, + ItemAddExperience = 85, + ItemHealAfterKill = 86, + ItemReducedPrices = 87, + ItemDoubleHerbDuration = 88, + ItemLightRadius = 89, + ItemLightColor = 90, + ItemReqPercent = 91, + ItemLevelReq = 92, + ItemFasterAttackRate = 93, + ItemLevelReqPercent = 94, + LastBlockFrame = 95, + ItemFasterMoveVelocity = 96, + ItemNonClassSkill = 97, + State = 98, + ItemFasterGetHitRate = 99, + MonsterPlayerCount = 100, + SkillPoisonOverrideLength = 101, + ItemFasterBlockRate = 102, + SkillBypassUndead = 103, + SkillBypassDemons = 104, + ItemFasterCastRate = 105, + SkillBypassBeasts = 106, + ItemSingleSkill = 107, + ItemRestInPeace = 108, + CurseResistance = 109, + ItemPoisonLengthResist = 110, + ItemNormalDamage = 111, + ItemHowl = 112, + ItemStupidity = 113, + ItemDamageToMana = 114, + ItemIgnoreTargetAc = 115, + ItemFractionalTargetAc = 116, + ItemPreventHeal = 117, + ItemHalfFreezeDuration = 118, + ItemToHitPercent = 119, + ItemDamageTargetAc = 120, + ItemDemonDamagePercent = 121, + ItemUndeadDamagePercent = 122, + ItemDemonToHit = 123, + ItemUndeadToHit = 124, + ItemThrowable = 125, + ItemElemSkill = 126, + ItemAllSkills = 127, + ItemAttackerTakesLightDamage = 128, + IronMaidenLevel = 129, + LifeTapLevel = 130, + ThornsPercent = 131, + BoneArmor = 132, + BoneArmorMax = 133, + ItemFreeze = 134, + ItemOpenWounds = 135, + ItemCrushingBlow = 136, + ItemKickDamage = 137, + ItemManaAfterKill = 138, + ItemHealAfterDemonKill = 139, + ItemExtraBlood = 140, + ItemDeadlyStrike = 141, + ItemAbsorbFirePercent = 142, + ItemAbsorbFire = 143, + ItemAbsorbLightningPercent = 144, + ItemAbsorbLightning = 145, + ItemAbsorbMagicPercent = 146, + ItemAbsorbMagic = 147, + ItemAbsorbColdPercent = 148, + ItemAbsorbCold = 149, + ItemSlow = 150, + ItemAura = 151, + ItemIndestructible = 152, + ItemCannotBeFrozen = 153, + ItemStaminaDrainPercent = 154, + ItemReanimate = 155, + ItemPierce = 156, + ItemMagicArrow = 157, + ItemExplosiveArrow = 158, + ItemThrowMinDamage = 159, + ItemThrowMaxDamage = 160, + SkillHandOfAthena = 161, + SkillStaminaPercent = 162, + SkillPassiveStaminaPercent = 163, + SkillConcentration = 164, + SkillEnchant = 165, + SkillPierce = 166, + SkillConviction = 167, + SkillChillingArmor = 168, + SkillFrenzy = 169, + SkillDecrepify = 170, + SkillArmorPercent = 171, + Alignment = 172, + Target0 = 173, + Target1 = 174, + GoldLost = 175, + ConversionLevel = 176, + ConversionMaxHp = 177, + UnitDoOverlay = 178, + AttackVsMonsterType = 179, + DamageVsMonsterType = 180, + Fade = 181, + ArmorOverridePercent = 182, + LastHitReactFrame = 183, + CreateSeason = 184, + BonusMinDamage = 185, + BonusMaxDamage = 186, + ItemPierceColdImmunity = 187, + ItemAddSkillTab = 188, + ItemPierceFireImmunity = 189, + ItemPierceLightningImmunity = 190, + ItemPiercePoisonImmunity = 191, + ItemPierceDamageImmunity = 192, + ItemPierceMagicImmunity = 193, + ItemNumSockets = 194, + ItemSkillOnAttack = 195, + ItemSkillOnKill = 196, + ItemSkillOnDeath = 197, + ItemSkillOnHit = 198, + ItemSkillOnLevelUp = 199, + ItemChargeNoConsume = 200, + ItemSkillOnGetHit = 201, + ModifierListCastId = 202, + PassiveMasteryItemReqPercent = 203, + ItemChargedSkill = 204, + ItemNoConsume = 205, + PassiveMasteryNoConsume = 206, + PassiveMasteryReplenishOnCrit = 207, + MissileThornsPercent = 208, + PassiveMasteryItemLevelReqPercent = 209, + UberAncientsEscalation = 210, + UberAncientsDefeated = 211, + PassiveMasteryGetHitRate = 212, + PassiveMasteryAttackSpeed = 213, + ItemArmorPerLevel = 214, + ItemArmorPercentPerLevel = 215, + ItemHpPerLevel = 216, + ItemManaPerLevel = 217, + ItemMaxDamagePerLevel = 218, + ItemMaxDamagePercentPerLevel = 219, + ItemStrengthPerLevel = 220, + ItemDexterityPerLevel = 221, + ItemEnergyPerLevel = 222, + ItemVitalityPerLevel = 223, + ItemToHitPerLevel = 224, + ItemToHitPercentPerLevel = 225, + ItemColdDamageMaxPerLevel = 226, + ItemFireDamageMaxPerLevel = 227, + ItemLightningDamageMaxPerLevel = 228, + ItemPoisonDamageMaxPerLevel = 229, + ItemResistColdPerLevel = 230, + ItemResistFirePerLevel = 231, + ItemResistLightningPerLevel = 232, + ItemResistPoisonPerLevel = 233, + ItemAbsorbColdPerLevel = 234, + ItemAbsorbFirePerLevel = 235, + ItemAbsorbLightningPerLevel = 236, + ItemAbsorbPoisonPerLevel = 237, + ItemThornsPerLevel = 238, + ItemFindGoldPerLevel = 239, + ItemFindMagicPerLevel = 240, + ItemRegenStaminaPerLevel = 241, + ItemStaminaPerLevel = 242, + ItemDamageDemonPerLevel = 243, + ItemDamageUndeadPerLevel = 244, + ItemToHitDemonPerLevel = 245, + ItemToHitUndeadPerLevel = 246, + ItemCrushingBlowPerLevel = 247, + ItemOpenWoundsPerLevel = 248, + ItemKickDamagePerLevel = 249, + ItemDeadlyStrikePerLevel = 250, + ItemFindGemsPerLevel = 251, + ItemReplenishDurability = 252, + ItemReplenishQuantity = 253, + ItemExtraStack = 254, + ItemFindItem = 255, + ItemSlashDamage = 256, + ItemSlashDamagePercent = 257, + ItemCrushDamage = 258, + ItemCrushDamagePercent = 259, + ItemThrustDamage = 260, + ItemThrustDamagePercent = 261, + ItemAbsorbSlash = 262, + ItemAbsorbCrush = 263, + ItemAbsorbThrust = 264, + ItemAbsorbSlashPercent = 265, + ItemAbsorbCrushPercent = 266, + ItemAbsorbThrustPercent = 267, + ItemArmorByTime = 268, + ItemArmorPercentByTime = 269, + ItemHpByTime = 270, + ItemManaByTime = 271, + ItemMaxDamageByTime = 272, + ItemMaxDamagePercentByTime = 273, + ItemStrengthByTime = 274, + ItemDexterityByTime = 275, + ItemEnergyByTime = 276, + ItemVitalityByTime = 277, + ItemToHitByTime = 278, + ItemToHitPercentByTime = 279, + ItemColdDamageMaxByTime = 280, + ItemFireDamageMaxByTime = 281, + ItemLightningDamageMaxByTime = 282, + ItemPoisonDamageMaxByTime = 283, + ItemResistColdByTime = 284, + ItemResistFireByTime = 285, + ItemResistLightningByTime = 286, + ItemResistPoisonByTime = 287, + ItemAbsorbColdByTime = 288, + ItemAbsorbFireByTime = 289, + ItemAbsorbLightningByTime = 290, + ItemAbsorbPoisonByTime = 291, + ItemFindGoldByTime = 292, + ItemFindMagicByTime = 293, + ItemRegenStaminaByTime = 294, + ItemStaminaByTime = 295, + ItemDamageDemonByTime = 296, + ItemDamageUndeadByTime = 297, + ItemToHitDemonByTime = 298, + ItemToHitUndeadByTime = 299, + ItemCrushingBlowByTime = 300, + ItemOpenWoundsByTime = 301, + ItemKickDamageByTime = 302, + ItemDeadlyStrikeByTime = 303, + ItemFindGemsByTime = 304, + ItemPierceCold = 305, + ItemPierceFire = 306, + ItemPierceLightning = 307, + ItemPiercePoison = 308, + ItemDamageVsMonster = 309, + ItemDamagePercentVsMonster = 310, + ItemToHitVsMonster = 311, + ItemToHitPercentVsMonster = 312, + ItemAcVsMonster = 313, + ItemAcPercentVsMonster = 314, + FireLength = 315, + BurningMin = 316, + BurningMax = 317, + ProgressiveDamage = 318, + ProgressiveSteal = 319, + ProgressiveOther = 320, + ProgressiveFire = 321, + ProgressiveCold = 322, + ProgressiveLightning = 323, + ItemExtraCharges = 324, + ProgressiveToHit = 325, + PoisonCount = 326, + DamageFrameRate = 327, + PierceIndex = 328, + PassiveFireMastery = 329, + PassiveLightningMastery = 330, + PassiveColdMastery = 331, + PassivePoisonMastery = 332, + PassiveFirePierce = 333, + PassiveLightningPierce = 334, + PassiveColdPierce = 335, + PassivePoisonPierce = 336, + PassiveCriticalStrike = 337, + PassiveDodge = 338, + PassiveAvoid = 339, + PassiveEvade = 340, + PassiveWarmth = 341, + PassiveMasteryMeleeToHit = 342, + PassiveMasteryMeleeDamage = 343, + PassiveMasteryMeleeCrit = 344, + PassiveMasteryThrowToHit = 345, + PassiveMasteryThrowDamage = 346, + PassiveMasteryThrowCrit = 347, + PassiveWeaponBlock = 348, + PassiveSummonResist = 349, + ModifierListSkill = 350, + ModifierListLevel = 351, + LastSentHitPointPercent = 352, + SourceUnitType = 353, + SourceUnitId = 354, + ShortParam1 = 355, + QuestItemDifficulty = 356, + PassiveMagicMastery = 357, + PassiveMagicPierce = 358, + // Not in 1.14d. + SkillCooldown = 359, + SkillMissileDamageScale = 360, + PsychicWard = 361, + PsychicWardMax = 362, + SkillChannelingTick = 363, + CustomizationIndex = 364, + ItemMagicDamageMaxPerLevel = 365, + PassiveDamagePierce = 366, + HeraldTier = 367, +}; + +// D2 stores hp / mana / stamina (HitPoints..MaxStamina, a contiguous run) in 8.8 +// fixed point. +constexpr bool IsFixedPointStat(Stat stat) { + return stat >= Stat::HitPoints && stat <= Stat::MaxStamina; +} + +// A stat list's kind bits (D2StatListStrc::dwFlags): what the list holds and how it +// lives - base stats, an item's magic mods, a set tier, a timed buff, the extended layout. +/// @flags +enum class StatListFlags : uint32_t { + Base = 0x0, + Basic = 0x1, + NewLength = 0x2, + TempOnly = 0x4, + Buff = 0x8, + Curse = 0x20, + Magic = 0x40, + Overlay = 0x80, + Unk0x100 = 0x100, + Toggle = 0x200, + Convert = 0x800, + Set = 0x2000, + ItemEx = 0x200000, + Permanent = 0x20000000, + Dynamic = 0x40000000, + Extended = 0x80000000 +}; + +// State ids: the rows of states.txt, with the same id stability as Stat. +enum class State : uint32_t { + None = 0, + Freeze = 1, + Poison = 2, + ResistFire = 3, + ResistCold = 4, + ResistLight = 5, + ResistMagic = 6, + PlayerBody = 7, + ResistAll = 8, + AmplifyDamage = 9, + FrozenArmor = 10, + Cold = 11, + Inferno = 12, + Blaze = 13, + BoneArmor = 14, + Concentrate = 15, + Enchant = 16, + InnerSight = 17, + SkillMove = 18, + Weaken = 19, + ChillingArmor = 20, + Stunned = 21, + Spiderlay = 22, + DimVision = 23, + Slowed = 24, + FetishAura = 25, + Shout = 26, + Taunt = 27, + Conviction = 28, + Convicted = 29, + EnergyShield = 30, + VenomClaws = 31, + BattleOrders = 32, + Might = 33, + Prayer = 34, + HolyFire = 35, + Thorns = 36, + Defiance = 37, + Thunderstorm = 38, + LightningBolt = 39, + BlessedAim = 40, + Stamina = 41, + Concentration = 42, + HolyWind = 43, + HolyWindCold = 44, + Cleansing = 45, + HolyShock = 46, + Sanctuary = 47, + Meditation = 48, + Fanaticism = 49, + Redemption = 50, + BattleCommand = 51, + PreventHeal = 52, + Conversion = 53, + Uninterruptable = 54, + IronMaiden = 55, + Terror = 56, + Attract = 57, + LifeTap = 58, + Confuse = 59, + Decrepify = 60, + LowerResist = 61, + OpenWounds = 62, + Dopplezon = 63, + CriticalStrike = 64, + Dodge = 65, + Avoid = 66, + Penetrate = 67, + Evade = 68, + Pierce = 69, + Warmth = 70, + FireMastery = 71, + LightningMastery = 72, + ColdMastery = 73, + BladeMastery = 74, + AxeMastery = 75, + MaceMastery = 76, + PolearmMastery = 77, + ThrowingMastery = 78, + SpearMastery = 79, + IncreasedStamina = 80, + IronSkin = 81, + IncreasedSpeed = 82, + NaturalResistance = 83, + FingerMageCurse = 84, + NoManaRegen = 85, + JustHit = 86, + SlowMissiles = 87, + ShiverArmor = 88, + BattleCry = 89, + Blue = 90, + Red = 91, + DeathDelay = 92, + Valkyrie = 93, + Frenzy = 94, + Berserk = 95, + Revive = 96, + SkeletalMastery = 97, + SourceUnit = 98, + Redeemed = 99, + HealthPot = 100, + HolyShield = 101, + JustPortaled = 102, + MonFrenzy = 103, + CorpseNoDraw = 104, + Alignment = 105, + ManaPot = 106, + Shatter = 107, + SyncWarped = 108, + ConversionSave = 109, + Pregnant = 110, + GolemMastery = 111, + Rabies = 112, + DefenseCurse = 113, + BloodMana = 114, + Burning = 115, + DragonFlight = 116, + Maul = 117, + CorpseNoSelect = 118, + ShadowWarrior = 119, + FeralRage = 120, + SkillDelay = 121, + ProgressiveDamage = 122, + ProgressiveSteal = 123, + ProgressiveOther = 124, + ProgressiveFire = 125, + ProgressiveCold = 126, + ProgressiveLightning = 127, + ShrineArmor = 128, + ShrineCombat = 129, + ShrineResistLightning = 130, + ShrineResistFire = 131, + ShrineResistCold = 132, + ShrineResistPoison = 133, + ShrineSkill = 134, + ShrineManaRegen = 135, + ShrineStamina = 136, + ShrineExperience = 137, + FenrisRage = 138, + Wolf = 139, + Bear = 140, + BloodLust = 141, + ChangeClass = 142, + Attached = 143, + Hurricane = 144, + Armageddon = 145, + Invis = 146, + Barbs = 147, + Wolverine = 148, + Oaksage = 149, + VineBeast = 150, + CycloneArmor = 151, + ClawMastery = 152, + CloakOfShadows = 153, + Recycled = 154, + WeaponBlock = 155, + Cloaked = 156, + Quickness = 157, + BladeShield = 158, + Fade = 159, + SummonResist = 160, + OakSageControl = 161, + WolverineControl = 162, + BarbsControl = 163, + DebugControl = 164, + Itemset1 = 165, + Itemset2 = 166, + Itemset3 = 167, + Itemset4 = 168, + Itemset5 = 169, + Itemset6 = 170, + RuneWord = 171, + RestInPeace = 172, + CorpseExp = 173, + Whirlwind = 174, + FullSetGeneric = 175, + MonsterSet = 176, + Delerium = 177, + Antidote = 178, + Thawing = 179, + StaminaPot = 180, + PassiveResistFire = 181, + PassiveResistCold = 182, + PassiveResistLtng = 183, + UberMinion = 184, + // Not in 1.14d. + Cooldown = 185, + SharedStash = 186, + HideDead = 187, + Impale = 188, + Desecrated = 189, + MarkBear = 190, + MarkWolf = 191, + SigilLethargy = 192, + SigilRancor = 193, + SigilDeath = 194, + BloodOath = 195, + DemonicMastery = 196, + LevitateMastery = 197, + HexBane = 198, + HexBaneDebuff = 199, + HexSiphon = 200, + HexSiphonDebuff = 201, + HexPurge = 202, + MindBarrier = 203, + ShadowMastery = 204, + Sigil = 205, + PsychicWard = 206, + BindDemon = 207, + Consume = 208, + AdvancedStash = 209, + HealthLink = 210, + Engorge = 211, + DeathMark = 212, + HealthLinkCaster = 213, + WarlockDemonGround = 214, + WarlockDemonTorso = 215, + SkillChanneling = 216, + HexPurgeDebuff = 217, + ChronicleBacklight = 218, + ChronicleFootprints = 219, + ChroniclePortal = 220, + EldritchBlastPeriodic = 221, + LightningEnchant = 222, + ColdEnchant = 223, + TalicFirePierce = 224, + MadawcLightPierce = 225, + KorlicColdPierce = 226, + BindDemonUnderling = 227, + Herald = 228, + Apocalypse = 229, +}; + +// Skill ids: the rows of skills.txt, with the same id stability as Stat. 16 bits +// wide, the width the game stores and sends a skill id in. +enum class Skill : uint16_t { + Attack = 0, + Kick = 1, + Throw = 2, + Unsummon = 3, + LeftHandThrow = 4, + LeftHandSwing = 5, + MagicArrow = 6, + FireArrow = 7, + InnerSight = 8, + CriticalStrike = 9, + Jab = 10, + ColdArrow = 11, + MultipleShot = 12, + Dodge = 13, + PowerStrike = 14, + PoisonJavelin = 15, + ExplodingArrow = 16, + SlowMissiles = 17, + Avoid = 18, + Impale = 19, + LightningBolt = 20, + IceArrow = 21, + GuidedArrow = 22, + Penetrate = 23, + ChargedStrike = 24, + PlagueJavelin = 25, + Strafe = 26, + ImmolationArrow = 27, + Dopplezon = 28, + Evade = 29, + Fend = 30, + FreezingArrow = 31, + Valkyrie = 32, + Pierce = 33, + LightningStrike = 34, + LightningFury = 35, + FireBolt = 36, + Warmth = 37, + ChargedBolt = 38, + IceBolt = 39, + FrozenArmor = 40, + Inferno = 41, + StaticField = 42, + Telekinesis = 43, + FrostNova = 44, + IceBlast = 45, + Blaze = 46, + FireBall = 47, + Nova = 48, + Lightning = 49, + ShiverArmor = 50, + FireWall = 51, + Enchant = 52, + ChainLightning = 53, + Teleport = 54, + GlacialSpike = 55, + Meteor = 56, + ThunderStorm = 57, + EnergyShield = 58, + Blizzard = 59, + ChillingArmor = 60, + FireMastery = 61, + Hydra = 62, + LightningMastery = 63, + FrozenOrb = 64, + ColdMastery = 65, + AmplifyDamage = 66, + Teeth = 67, + BoneArmor = 68, + SkeletonMastery = 69, + RaiseSkeleton = 70, + DimVision = 71, + Weaken = 72, + PoisonDagger = 73, + CorpseExplosion = 74, + ClayGolem = 75, + IronMaiden = 76, + Terror = 77, + BoneWall = 78, + GolemMastery = 79, + RaiseSkeletalMage = 80, + Confuse = 81, + LifeTap = 82, + PoisonExplosion = 83, + BoneSpear = 84, + BloodGolem = 85, + Attract = 86, + Decrepify = 87, + BonePrison = 88, + SummonResist = 89, + IronGolem = 90, + LowerResist = 91, + PoisonNova = 92, + BoneSpirit = 93, + FireGolem = 94, + Revive = 95, + Sacrifice = 96, + Smite = 97, + Might = 98, + Prayer = 99, + ResistFire = 100, + HolyBolt = 101, + HolyFire = 102, + Thorns = 103, + Defiance = 104, + ResistCold = 105, + Zeal = 106, + Charge = 107, + BlessedAim = 108, + Cleansing = 109, + ResistLightning = 110, + Vengeance = 111, + BlessedHammer = 112, + Concentration = 113, + HolyFreeze = 114, + Vigor = 115, + Conversion = 116, + HolyShield = 117, + HolyShock = 118, + Sanctuary = 119, + Meditation = 120, + FistOfTheHeavens = 121, + Fanaticism = 122, + Conviction = 123, + Redemption = 124, + Salvation = 125, + Bash = 126, + BladeMastery = 127, + AxeMastery = 128, + MaceMastery = 129, + Howl = 130, + FindPotion = 131, + Leap = 132, + DoubleSwing = 133, + PoleArmMastery = 134, + ThrowingMastery = 135, + SpearMastery = 136, + Taunt = 137, + Shout = 138, + Stun = 139, + DoubleThrow = 140, + IncreasedStamina = 141, + FindItem = 142, + LeapAttack = 143, + Concentrate = 144, + IronSkin = 145, + BattleCry = 146, + Frenzy = 147, + IncreasedSpeed = 148, + BattleOrders = 149, + GrimWard = 150, + Whirlwind = 151, + Berserk = 152, + NaturalResistance = 153, + WarCry = 154, + BattleCommand = 155, + FireHit = 156, + UnHolyBolt = 157, + SkeletonRaise = 158, + MaggotEgg = 159, + ShamanFire = 160, + MagottUp = 161, + MagottDown = 162, + MagottLay = 163, + AndrialSpray = 164, + Jump = 165, + SwarmMove = 166, + Nest = 167, + QuickStrike = 168, + VampireFireball = 169, + VampireFirewall = 170, + VampireMeteor = 171, + GargoyleTrap = 172, + SpiderLay = 173, + VampireHeal = 174, + VampireRaise = 175, + Submerge = 176, + FetishAura = 177, + FetishInferno = 178, + ZakarumHeal = 179, + Emerge = 180, + Resurrect = 181, + Bestow = 182, + MissileSkill1 = 183, + MonTeleport = 184, + PrimeLightning = 185, + PrimeBolt = 186, + PrimeBlaze = 187, + PrimeFirewall = 188, + PrimeSpike = 189, + PrimeIceNova = 190, + PrimePoisonball = 191, + PrimePoisonNova = 192, + DiabLight = 193, + DiabCold = 194, + DiabFire = 195, + FingerMageSpider = 196, + DiabWall = 197, + DiabRun = 198, + DiabPrison = 199, + PoisonBallTrap = 200, + AndyPoisonBolt = 201, + HireableMissile = 202, + DesertTurret = 203, + ArcaneTower = 204, + MonBlizzard = 205, + Mosquito = 206, + CursedBallTrapRight = 207, + CursedBallTrapLeft = 208, + MonFrozenArmor = 209, + MonBoneArmor = 210, + MonBoneSpirit = 211, + MonCurseCast = 212, + HellMeteor = 213, + RegurgitatorEat = 214, + MonFrenzy = 215, + QueenDeath = 216, + ScrollOfIdentify = 217, + BookOfIdentify = 218, + ScrollOfTownportal = 219, + BookOfTownportal = 220, + Raven = 221, + PlaguePoppy = 222, + Wearwolf = 223, + ShapeShifting = 224, + Firestorm = 225, + OakSage = 226, + SummonSpiritWolf = 227, + Wearbear = 228, + MoltenBoulder = 229, + ArcticBlast = 230, + CycleOfLife = 231, + FeralRage = 232, + Maul = 233, + Eruption = 234, + CycloneArmor = 235, + HeartOfWolverine = 236, + SummonFenris = 237, + Rabies = 238, + FireClaws = 239, + Twister = 240, + Vines = 241, + Hunger = 242, + ShockWave = 243, + Volcano = 244, + Tornado = 245, + SpiritOfBarbs = 246, + SummonGrizzly = 247, + Fury = 248, + Armageddon = 249, + Hurricane = 250, + FireTrauma = 251, + ClawMastery = 252, + PsychicHammer = 253, + TigerStrike = 254, + DragonTalon = 255, + ShockField = 256, + BladeSentinel = 257, + Quickness = 258, + FistsOfFire = 259, + DragonClaw = 260, + ChargedBoltSentry = 261, + WakeOfFireSentry = 262, + WeaponBlock = 263, + CloakOfShadows = 264, + CobraStrike = 265, + BladeFury = 266, + Fade = 267, + ShadowWarrior = 268, + ClawsOfThunder = 269, + DragonTail = 270, + LightningSentry = 271, + InfernoSentry = 272, + MindBlast = 273, + BladesOfIce = 274, + DragonFlight = 275, + DeathSentry = 276, + BladeShield = 277, + Venom = 278, + ShadowMaster = 279, + RoyalStrike = 280, + WakeOfDestructionSentry = 281, + ImpInferno = 282, + ImpFireball = 283, + BaalTaunt = 284, + BaalCorpseExplode = 285, + BaalMonsterSpawn = 286, + CatapultChargedBall = 287, + CatapultSpikeBall = 288, + SuckBlood = 289, + CryHelp = 290, + HealingVortex = 291, + Teleport2 = 292, + SelfResurrect = 293, + VineAttack = 294, + OverseerWhip = 295, + BarbsAura = 296, + WolverineAura = 297, + OakSageAura = 298, + ImpFireMissile = 299, + Impregnate = 300, + SiegeBeastStomp = 301, + MinionSpawner = 302, + CatapultBlizzard = 303, + CatapultPlague = 304, + CatapultMeteor = 305, + BoltSentry = 306, + CorpseCycler = 307, + DeathMaul = 308, + DefenseCurse = 309, + BloodMana = 310, + MonInfernoSentry = 311, + MonDeathSentry = 312, + SentryLightning = 313, + FenrisRage = 314, + BaalTentacle = 315, + BaalNova = 316, + BaalInferno = 317, + BaalColdMissiles = 318, + MegademonInferno = 319, + EvilHutSpawner = 320, + CountessFirewall = 321, + ImpBolt = 322, + HorrorArcticBlast = 323, + DeathSentryLtng = 324, + VineCycler = 325, + BearSmite = 326, + Resurrect2 = 327, + BloodLordFrenzy = 328, + BaalTeleport = 329, + ImpTeleport = 330, + BaalCloneTeleport = 331, + ZakarumLightning = 332, + VampireMissile = 333, + MephistoMissile = 334, + DoomKnightMissile = 335, + RogueMissile = 336, + HydraMissile = 337, + NecromageMissile = 338, + MonBow = 339, + MonFireArrow = 340, + MonColdArrow = 341, + MonExplodingArrow = 342, + MonFreezingArrow = 343, + MonPowerStrike = 344, + SuccubusBolt = 345, + MephFrostNova = 346, + MonIceSpear = 347, + ShamanIce = 348, + Diablogeddon = 349, + DeleriumChange = 350, + NihlathakCorpseExplosion = 351, + SerpentCharge = 352, + TrapNova = 353, + UnHolyBoltEx = 354, + ShamanFireEx = 355, + ImpFireMissileEx = 356, + // Not in 1.14d. + Interact = 357, + Loot = 358, + TownPortal = 359, + EmoteWheel = 360, + SwapWeapons = 361, + Map = 362, + ShowItems = 363, + RunToggle = 364, + MonHolyFreeze = 365, + MonLeap = 366, + MonLeapAttack = 367, + MonHolyFire = 368, + MonHolyShock = 369, + CubeLoot = 370, + MarkOfTheBear = 371, + MarkOfTheWolf = 372, + SummonGoatman = 373, + DemonicMastery = 374, + DeathMark = 375, + SummonTainted = 376, + SummonDefiler = 377, + BloodOath = 378, + Engorge = 379, + BloodBoil = 380, + Consume = 381, + BindDemon = 382, + Levitate = 383, + EldritchBlast = 384, + HexBane = 385, + HexSiphon = 386, + PsychicWard = 387, + EchoingStrike = 388, + HexPurge = 389, + BladeWarp = 390, + Cleave = 391, + MirroredBlades = 392, + SigilLethargy = 393, + RingOfFire = 394, + MiasmaBolt = 395, + SigilRancor = 396, + EnhancedEntropy = 397, + FlameWave = 398, + MiasmaChains = 399, + SigilDeath = 400, + Apocalypse = 401, + Abyss = 402, + SigilDeathExplosion = 403, + HexPurgeExplosion = 404, + HealthLink = 405, + ColdFissure = 406, + KorlicsLeapAttack = 407, + ColdEnchant = 408, + LightningEnchant = 409, + TalicsWhirlwind = 410, + TownportalOSkill = 411, + FireTwisters = 412, + ColossalVolcano = 413, + ColossalThunderStorm = 414, + UberAncientsHeal = 415, + GoatmanStun = 416, + GoatmanFrenzy = 417, + GoatmanBerserk = 418, + GoatmanCleave = 419, + TaintedResistFire = 420, + TaintedFireBolt = 421, + TaintedFireBall = 422, + TalicsFirePierce = 423, + MadawcsLightningPierce = 424, + KorlicsColdPierce = 425, + ChargedBoltDisk = 426, + KorlicsBash = 427, + HeraldThorns = 428, + // The game's -1: no skill (an empty hotkey slot, an unset txt link). + Invalid = UINT16_MAX, +}; + // ============================================================================ // Unit / item // ============================================================================ @@ -452,6 +1530,153 @@ enum class Act : uint8_t { V = 4, }; +// Area (level) number: the row of levels.txt, as the game and scripts number it. +// 137-141 are not in 1.14d's levels.txt. +enum class LevelId : uint32_t { + None = 0, + RogueEncampment = 1, + BloodMoor = 2, + ColdPlains = 3, + StonyField = 4, + DarkWood = 5, + BlackMarsh = 6, + TamoeHighland = 7, + DenOfEvil = 8, + CaveLevel1 = 9, + UndergroundPassageLevel1 = 10, + HoleLevel1 = 11, + PitLevel1 = 12, + CaveLevel2 = 13, + UndergroundPassageLevel2 = 14, + HoleLevel2 = 15, + PitLevel2 = 16, + BurialGrounds = 17, + Crypt = 18, + Mausoleum = 19, + ForgottenTower = 20, + TowerCellarLevel1 = 21, + TowerCellarLevel2 = 22, + TowerCellarLevel3 = 23, + TowerCellarLevel4 = 24, + TowerCellarLevel5 = 25, + MonasteryGate = 26, + OuterCloister = 27, + Barracks = 28, + JailLevel1 = 29, + JailLevel2 = 30, + JailLevel3 = 31, + InnerCloister = 32, + Cathedral = 33, + CatacombsLevel1 = 34, + CatacombsLevel2 = 35, + CatacombsLevel3 = 36, + CatacombsLevel4 = 37, + Tristram = 38, + MooMooFarm = 39, + LutGholein = 40, + RockyWaste = 41, + DryHills = 42, + FarOasis = 43, + LostCity = 44, + ValleyOfSnakes = 45, + CanyonOfTheMagi = 46, + SewersLevel1Act2 = 47, + SewersLevel2Act2 = 48, + SewersLevel3Act2 = 49, + HaremLevel1 = 50, + HaremLevel2 = 51, + PalaceCellarLevel1 = 52, + PalaceCellarLevel2 = 53, + PalaceCellarLevel3 = 54, + StonyTombLevel1 = 55, + HallsOfTheDeadLevel1 = 56, + HallsOfTheDeadLevel2 = 57, + ClawViperTempleLevel1 = 58, + StonyTombLevel2 = 59, + HallsOfTheDeadLevel3 = 60, + ClawViperTempleLevel2 = 61, + MaggotLairLevel1 = 62, + MaggotLairLevel2 = 63, + MaggotLairLevel3 = 64, + AncientTunnels = 65, + TalRashasTomb1 = 66, + TalRashasTomb2 = 67, + TalRashasTomb3 = 68, + TalRashasTomb4 = 69, + TalRashasTomb5 = 70, + TalRashasTomb6 = 71, + TalRashasTomb7 = 72, + DurielsLair = 73, + ArcaneSanctuary = 74, + KurastDocks = 75, + SpiderForest = 76, + GreatMarsh = 77, + FlayerJungle = 78, + LowerKurast = 79, + KurastBazaar = 80, + UpperKurast = 81, + KurastCauseway = 82, + Travincal = 83, + SpiderCave = 84, + SpiderCavern = 85, + SwampyPitLevel1 = 86, + SwampyPitLevel2 = 87, + FlayerDungeonLevel1 = 88, + FlayerDungeonLevel2 = 89, + SwampyPitLevel3 = 90, + FlayerDungeonLevel3 = 91, + SewersLevel1Act3 = 92, + SewersLevel2Act3 = 93, + RuinedTemple = 94, + DisusedFane = 95, + ForgottenReliquary = 96, + ForgottenTemple = 97, + RuinedFane = 98, + DisusedReliquary = 99, + DuranceOfHateLevel1 = 100, + DuranceOfHateLevel2 = 101, + DuranceOfHateLevel3 = 102, + ThePandemoniumFortress = 103, + OuterSteppes = 104, + PlainsOfDespair = 105, + CityOfTheDamned = 106, + RiverOfFlame = 107, + ChaosSanctuary = 108, + Harrogath = 109, + BloodyFoothills = 110, + FrigidHighlands = 111, + ArreatPlateau = 112, + CrystallinePassage = 113, + FrozenRiver = 114, + GlacialTrail = 115, + DrifterCavern = 116, + FrozenTundra = 117, + TheAncientsWay = 118, + IcyCellar = 119, + ArreatSummit = 120, + NihlathaksTemple = 121, + HallsOfAnguish = 122, + HallsOfPain = 123, + HallsOfVaught = 124, + Abaddon = 125, + PitOfAcheron = 126, + InfernalPit = 127, + TheWorldStoneKeepLevel1 = 128, + TheWorldStoneKeepLevel2 = 129, + TheWorldStoneKeepLevel3 = 130, + ThroneOfDestruction = 131, + TheWorldstoneChamber = 132, + MatronsDen = 133, + ForgottenSands = 134, + FurnaceOfPain = 135, + UberTristram = 136, + MapsAncientTemple = 137, + MapsDesecratedTemple = 138, + MapsFrigidPlateau = 139, + MapsInfernalTrial = 140, + MapsRuinedCitadel = 141, +}; + // Unit.mode, by unit type: player (PlayerMode), monster (MonsterMode), object // (ObjectMode) and item (ItemMode) animation modes. Values follow D2MOO's // D2C_PlayerModes / D2C_MonModes / D2C_ObjModes / D2C_ItemModes; D2R keeps them. @@ -519,7 +1744,7 @@ enum class ItemMode : uint32_t { }; // Extended unit flag bits (Unit.flagsex). Values follow D2MOO's D2C_UnitFlagsEx; -// D2R keeps them and adds SharedStash. +// later versions keep them and add AnimEndOfCycle. /// @flags enum class UnitFlagEx : uint32_t { HasInventory = 0x00000001, @@ -537,7 +1762,61 @@ enum class UnitFlagEx : uint32_t { NoDraw = 0x00040000, IsExpansion = 0x02000000, ServerUnit = 0x04000000, - SharedStash = 0x08000000, // D2R only: a shared stash tab's owner unit + // Not in 1.14d: the current animation reached the end of its frame cycle, cached + // by the frame advance (1.14d computes UNITS_IsAtEndOfFrameCycle on demand) + // and cleared on every mode change. + AnimEndOfCycle = 0x08000000, +}; + +// A quest's index in the quest record (getQuest's quest). Values follow D2MOO's +// D2QuestStateFlagIds; names are the quest log's (the +// qstsaq strings), and the game's flag names where the log has none. +// 32 bits wide because the games read the whole register: the record lookup +// computes quest * 16 + flag. +enum class Quest : uint32_t { + Act1Prologue = 0, + DenOfEvil = 1, + SistersBurialGrounds = 2, + ToolsOfTheTrade = 3, + TheSearchForCain = 4, + TheForgottenTower = 5, + SistersToTheSlaughter = 6, + Act1Completed = 7, + Act2Prologue = 8, + RadamentsLair = 9, + TheHoradricStaff = 10, + TaintedSun = 11, + ArcaneSanctuary = 12, + TheSummoner = 13, + TheSevenTombs = 14, + Act2Completed = 15, + Act3Prologue = 16, + LamEsensTome = 17, + KhalimsWill = 18, + BladeOfTheOldReligion = 19, + TheGoldenBird = 20, + TheBlackenedTemple = 21, + TheGuardian = 22, + Act3Completed = 23, + Act4Prologue = 24, + TheFallenAngel = 25, + TerrorsEnd = 26, + HellsForge = 27, + Act4Completed = 28, + Act1Navi = 29, // QUESTSTATEFLAG_A1Q7, no quest-log entry + Act2GuardGossip1 = 30, // QUESTSTATEFLAG_A2Q7, unused by the game + Act2GuardGossip2 = 31, // QUESTSTATEFLAG_A2Q8, unused by the game + DarkWanderer = 32, // QUESTSTATEFLAG_A3Q7, no quest-log entry + Malachai = 33, // QUESTSTATEFLAG_A4Q4, no quest-log entry + SiegeOnHarrogath = 35, + RescueOnMountArreat = 36, + PrisonOfIce = 37, + BetrayalOfHarrogath = 38, + RiteOfPassage = 39, + EveOfDestruction = 40, + // The game's shared act-intro slot (QUESTSTATEFLAG_A*INTRO); scripts read + // Akara's free skill / stat reset from it. + Respec = 41, }; // A quest record's per-quest bit index (getQuest's flag). Values follow D2MOO's @@ -567,6 +1846,7 @@ enum class QuestFlag : uint32_t { enum class GameType : uint32_t { Classic = 0, Expansion = 1, + RotW = 2, // Reign of the Warlock }; enum class ScreenSize : uint32_t { @@ -584,6 +1864,22 @@ enum class MoveMode : uint32_t { Run = 1, }; +// The game client's text language (D2MOO's D2C_Language). +enum class Language : uint32_t { + English = 0, + Spanish = 1, + German = 2, + French = 3, + Portuguese = 4, + Italian = 5, + Japanese = 6, + Korean = 7, + ChineseSimplified = 8, + ChineseTraditional = 9, + Polish = 10, + Russian = 11, +}; + // ============================================================================ // Geometric primitives // ============================================================================ @@ -697,19 +1993,19 @@ struct UnitCursorState { // Result row for stat iteration (Unit::GetAllStats / GetDetailedStats). // `subIndex` disambiguates skill-specific / item-specific stat variants. struct StatEntry { - uint32_t statId; - uint32_t subIndex; + Stat statId; + uint16_t subIndex; int32_t value; }; // One leaf stat array off a unit's statlist chains (Unit::GetStatLists). -// `flags` (STATLIST_* bitmask) and `stateNo` are the game's own provenance fields, +// `flags` and `stateNo` are the game's own provenance fields, // copied verbatim: they say whether the array is the unit's base stats, an item mod, // a set tier or a runeword, and whether it currently contributes. GetAllStats / // GetDetailedStats merge that away. struct StatListEntry { - uint32_t flags = 0; - uint32_t stateNo = 0; + StatListFlags flags = StatListFlags::Base; + State stateNo = State::None; std::vector stats; }; @@ -719,12 +2015,12 @@ struct PresetUnitInfo { Position roomPos = {}; Position posInRoom = {}; // game coordinates - see docs/coords.md uint32_t id = 0; - uint32_t level = 0; + LevelId level = LevelId::None; // For UnitType::Tile presets, the destination level reached by // walking the tile transition - looked up via Room2::pRoomTiles in - // game-impl. 0 for non-tile presets or tile presets that don't appear + // game-impl. None for non-tile presets or tile presets that don't appear // in pRoomTiles (e.g. cosmetic tiles). - uint32_t tileTargetLevelId = 0; + LevelId tileTargetLevelId = LevelId::None; }; // Single dialog line returned from GetDialogLines. diff --git a/src/contract/game/Unit.h b/src/contract/game/Unit.h index 67616b0..752da46 100644 --- a/src/contract/game/Unit.h +++ b/src/contract/game/Unit.h @@ -61,7 +61,7 @@ class Unit { // getters - see docs/coords.md). Position Pos() const; Position TargetPos() const; - uint32_t Area() const; + LevelId Area() const; // === Stats === uint32_t Hp() const; @@ -71,8 +71,8 @@ class Unit { uint32_t Stamina() const; uint32_t StaminaMax() const; uint32_t CharLevel() const; - int32_t GetStat(uint32_t stat, uint32_t sub = 0) const; - bool HasState(uint32_t stateId) const; + int32_t GetStat(Stat stat, uint16_t sub = 0) const; + bool HasState(State state) const; std::vector GetAllStats() const; // Returns detailed stat list for getStat(-2): includes both base stats and stat-list stats. @@ -86,7 +86,7 @@ class Unit { // === Unit info === uint32_t Direction() const; - std::optional SuperUniqueId() const; + std::optional SuperUniqueId() const; MonsterSpecType SpecType() const; uint32_t ItemCount() const; @@ -152,7 +152,7 @@ class Unit { bool IsLocked() const; // === Player-specific === - uint32_t RunWalk() const; + MoveMode RunWalk() const; uint32_t WeaponSwitch() const; // === Traversal === @@ -198,34 +198,34 @@ class Unit { // === Skill methods === struct SkillInfo { - uint16_t skillId; + Skill skillId; uint32_t baseLevel; uint32_t totalLevel; }; std::string GetSkillName(Hand hand) const; - uint16_t GetSkillId(Hand hand) const; + Skill GetSkillId(Hand hand) const; std::vector GetAllSkills() const; // Charge filter: // nullopt -> non-charge skills only (IsCharge == 0). // false -> no filter (match any skill). // true -> charge-only skills (IsCharge == 1). - std::optional GetSkillLevel(uint16_t skillId, bool includeExtraLevels, + std::optional GetSkillLevel(Skill skill, bool includeExtraLevels, std::optional charge = std::nullopt) const; // === Actions === void Move(Position target) const; bool Interact() const; - // Returns true on dispatch, false when waypointId is not a valid waypoint - // (levels.txt[wpId].Waypoint == 255) or the unit can't be resolved. - bool TakeWaypoint(uint32_t waypointId) const; + // Returns true on dispatch, false when `level` is not a valid waypoint + // (levels.txt[level].Waypoint == 255) or the unit can't be resolved. + bool TakeWaypoint(LevelId level) const; void Repair() const; - bool UseMenu(uint32_t menuId) const; + bool UseMenu(uint16_t menuId) const; ClickResult EquipItem() const; void Overhead(const std::string& text) const; void Revive() const; bool Shop(ShopMode mode) const; - bool SetSkill(uint16_t skillId, Hand hand, std::optional itemId) const; + bool SetSkill(Skill skill, Hand hand, std::optional itemId) const; uint32_t GetMinionCount(uint32_t type) const; uint32_t GetRepairCost(uint32_t npcClassId) const; bool HasEnchant(uint32_t enchantId) const; diff --git a/src/runtime/api/classes/ClassRegistry.cpp b/src/runtime/api/classes/ClassRegistry.cpp index 2cd5985..4ce8d69 100644 --- a/src/runtime/api/classes/ClassRegistry.cpp +++ b/src/runtime/api/classes/ClassRegistry.cpp @@ -230,11 +230,11 @@ v8::Local CreateMeObject(v8::Isolate* isolate, v8::Local, const v8::PropertyCallbackInfo& info) { - info.GetReturnValue().Set(game::GetGameType()); + info.GetReturnValue().Set(std::to_underlying(game::GetGameType())); }); /// @description Whether an item is currently held on the cursor (picked up, awaiting placement). @@ -268,10 +268,10 @@ v8::Local CreateMeObject(v8::Isolate* isolate, v8::Local, const v8::PropertyCallbackInfo& info) { - info.GetReturnValue().Set(game::GetLocale()); + info.GetReturnValue().Set(std::to_underlying(game::GetLocale())); }); /// @description Whether the current character is Hardcore (derived from the hardcore character flag). diff --git a/src/runtime/api/classes/game/JSArea.h b/src/runtime/api/classes/game/JSArea.h index bac8d02..8a518e0 100644 --- a/src/runtime/api/classes/game/JSArea.h +++ b/src/runtime/api/classes/game/JSArea.h @@ -54,7 +54,7 @@ class JSArea : public ClassBase { }); /// @description Level number identifying this area, matching the Areas constant. - /// @type {number} + /// @type {LevelId} Property( isolate, inst, "id", +[](v8::Local, const v8::PropertyCallbackInfo& info) { const auto data = Unwrap(info.Holder()); diff --git a/src/runtime/api/classes/game/JSExit.h b/src/runtime/api/classes/game/JSExit.h index 0b95d63..08cb3ae 100644 --- a/src/runtime/api/classes/game/JSExit.h +++ b/src/runtime/api/classes/game/JSExit.h @@ -1,6 +1,9 @@ #pragma once +#include + #include + #include "api/core/Class.h" #include "api/core/Error.h" #include "components/navigation/ExitFinder.h" @@ -34,11 +37,11 @@ class JSExit : public ClassBase { }); /// @description The destination this exit leads to, interpreted according to `type`. - /// @type {number} + /// @type {LevelId} Property( isolate, inst, "target", +[](v8::Local, const v8::PropertyCallbackInfo& info) { auto* data = Unwrap(info.Holder()); - info.GetReturnValue().Set(data->target); + info.GetReturnValue().Set(std::to_underlying(data->target)); }); /// @description The kind of exit: 1 (linkage) or 2 (tile). @@ -58,11 +61,11 @@ class JSExit : public ClassBase { }); /// @description The level number this exit belongs to. - /// @type {number} + /// @type {LevelId} Property( isolate, inst, "level", +[](v8::Local, const v8::PropertyCallbackInfo& info) { auto* data = Unwrap(info.Holder()); - info.GetReturnValue().Set(data->level); + info.GetReturnValue().Set(std::to_underlying(data->level)); }); } }; diff --git a/src/runtime/api/classes/game/JSParty.h b/src/runtime/api/classes/game/JSParty.h index 99a7048..1f8ba82 100644 --- a/src/runtime/api/classes/game/JSParty.h +++ b/src/runtime/api/classes/game/JSParty.h @@ -49,7 +49,7 @@ class JSParty : public ClassBase { }); /// @description The level/area ID the party member is currently in. - /// @type {number} + /// @type {LevelId} Property( isolate, inst, "area", +[](v8::Local, const v8::PropertyCallbackInfo& info) { const auto data = Unwrap(info.Holder()); @@ -78,7 +78,7 @@ class JSParty : public ClassBase { if (!*data) { return; } - info.GetReturnValue().Set(static_cast(data->Life())); + info.GetReturnValue().Set(data->Life()); }); /// @description The party member's party button state (invite / in party / accept / leave / cancel). @@ -100,7 +100,7 @@ class JSParty : public ClassBase { if (!*data) { return; } - info.GetReturnValue().Set(static_cast(data->PartyId())); + info.GetReturnValue().Set(data->PartyId()); }); /// @description The party member's character name. @@ -114,16 +114,15 @@ class JSParty : public ClassBase { info.GetReturnValue().Set(convert::ToJS(info.GetIsolate(), data->Name())); }); - /// @description The party member's character class ID (0-6, e.g. Amazon/Sorceress/etc.). - /// 0 = amazon, 1 = sorceress, 2 = necromancer, 3 = paladin, 4 = barbarian, 5 = druid, 6 = assassin. - /// @type {number} + /// @description The party member's character class ID. + /// @type {CharacterClass} Property( isolate, inst, "classid", +[](v8::Local, const v8::PropertyCallbackInfo& info) { const auto data = Unwrap(info.Holder()); if (!*data) { return; } - info.GetReturnValue().Set(static_cast(data->ClassId())); + info.GetReturnValue().Set(std::to_underlying(data->ClassId())); }); /// @description The party member's character (experience) level. @@ -134,7 +133,7 @@ class JSParty : public ClassBase { if (!*data) { return; } - info.GetReturnValue().Set(static_cast(data->CharacterLevel())); + info.GetReturnValue().Set(data->CharacterLevel()); }); // Methods diff --git a/src/runtime/api/classes/game/JSPresetUnit.h b/src/runtime/api/classes/game/JSPresetUnit.h index ad833c9..c6b17be 100644 --- a/src/runtime/api/classes/game/JSPresetUnit.h +++ b/src/runtime/api/classes/game/JSPresetUnit.h @@ -70,11 +70,11 @@ class JSPresetUnit : public ClassBase { }); /// @description Level (area) number where the preset unit resides. - /// @type {number} + /// @type {LevelId} Property( isolate, inst, "level", +[](v8::Local, const v8::PropertyCallbackInfo& info) { auto* data = Unwrap(info.Holder()); - info.GetReturnValue().Set(data->level); + info.GetReturnValue().Set(std::to_underlying(data->level)); }); } }; diff --git a/src/runtime/api/classes/game/JSRoom.h b/src/runtime/api/classes/game/JSRoom.h index bac1f32..045b1f4 100644 --- a/src/runtime/api/classes/game/JSRoom.h +++ b/src/runtime/api/classes/game/JSRoom.h @@ -98,7 +98,7 @@ class JSRoom : public ClassBase { }); /// @description Level/area ID this room belongs to. Alias of the level property. - /// @type {number} + /// @type {LevelId} Property( isolate, inst, "area", +[](v8::Local, const v8::PropertyCallbackInfo& info) { const auto data = Unwrap(info.Holder()); @@ -106,12 +106,12 @@ class JSRoom : public ClassBase { info.GetReturnValue().Set(0); return; } - info.GetReturnValue().Set(data->LevelId()); + info.GetReturnValue().Set(std::to_underlying(data->LevelId())); }); /// @description Level/area ID number this room belongs to (not a Level object). Alias of the area /// property. - /// @type {number} + /// @type {LevelId} Property( isolate, inst, "level", +[](v8::Local, const v8::PropertyCallbackInfo& info) { const auto data = Unwrap(info.Holder()); @@ -119,11 +119,11 @@ class JSRoom : public ClassBase { info.GetReturnValue().Set(0); return; } - info.GetReturnValue().Set(data->LevelId()); + info.GetReturnValue().Set(std::to_underlying(data->LevelId())); }); /// @description Correct-tomb level number for this room (Tal Rasha's Tombs detection). - /// @type {number} + /// @type {LevelId} Property( isolate, inst, "correcttomb", +[](v8::Local, const v8::PropertyCallbackInfo& info) { const auto data = Unwrap(info.Holder()); @@ -131,7 +131,7 @@ class JSRoom : public ClassBase { info.GetReturnValue().Set(0); return; } - info.GetReturnValue().Set(data->CorrectTomb()); + info.GetReturnValue().Set(std::to_underlying(data->CorrectTomb())); }); // Methods @@ -231,7 +231,7 @@ class JSRoom : public ClassBase { /// @description Returns the room's collision grid as a 2D array indexed grid[y][x] (outer array is rows). /// @signature getCollision() - /// @returns {number[][]} - rows of collision cell flags; undefined when game not ready or room + /// @returns {CollisionFlag[][]} - rows of collision cell flags; undefined when game not ready or room /// unresolved. Method( isolate, proto, "getCollision", +[](const v8::FunctionCallbackInfo& args) { diff --git a/src/runtime/api/classes/game/JSUnit.cpp b/src/runtime/api/classes/game/JSUnit.cpp index 6ccdfe9..f9577e9 100644 --- a/src/runtime/api/classes/game/JSUnit.cpp +++ b/src/runtime/api/classes/game/JSUnit.cpp @@ -159,14 +159,14 @@ void JSUnit::ConfigureTemplate(v8::Isolate* isolate, v8::Local, const v8::PropertyCallbackInfo& info) { const auto data = Unwrap(info.Holder()); if (!*data) { return; } - info.GetReturnValue().Set(data->Area()); + info.GetReturnValue().Set(std::to_underlying(data->Area())); }); /// @description Current hit points of the unit (whole-number HP, fixed-point shift already applied). @@ -317,7 +317,11 @@ void JSUnit::ConfigureTemplate(v8::Isolate* isolate, v8::Local(data->SuperUniqueId().value_or(-1))); + if (const auto id = data->SuperUniqueId()) { + info.GetReturnValue().Set(*id); + } else { + info.GetReturnValue().Set(-1); + } }); /// @description Three/four-character item code (e.g. "rvl", "gcv"); Item units only, "Unknown" if unresolvable. @@ -368,7 +372,7 @@ void JSUnit::ConfigureTemplate(v8::Isolate* isolate, v8::LocalType() != UnitType::Item) { return; } - info.GetReturnValue().Set(static_cast(data->PrefixNum())); + info.GetReturnValue().Set(data->PrefixNum()); }); /// @description Numeric ID of the item's primary magic suffix (Item units only; 0 if none). @@ -379,7 +383,7 @@ void JSUnit::ConfigureTemplate(v8::Isolate* isolate, v8::LocalType() != UnitType::Item) { return; } - info.GetReturnValue().Set(static_cast(data->SuffixNum())); + info.GetReturnValue().Set(data->SuffixNum()); }); /// @description Numeric ID of the item's rare prefix, 1-based into the concatenated @@ -731,7 +735,7 @@ void JSUnit::ConfigureTemplate(v8::Isolate* isolate, v8::LocalRunWalk()); + info.GetReturnValue().Set(std::to_underlying(data->RunWalk())); } }); @@ -839,9 +843,8 @@ void JSUnit::ConfigureTemplate(v8::Isolate* isolate, v8::Local& args) { @@ -894,7 +897,7 @@ void JSUnit::ConfigureTemplate(v8::Isolate* isolate, v8::Local(isolate, args[0]); + const auto menuId = convert::To(isolate, args[0]); args.GetReturnValue().Set(unit.UseMenu(menuId)); }); @@ -904,9 +907,9 @@ void JSUnit::ConfigureTemplate(v8::Isolate* isolate, v8::LocalIsNumber()) { auto* isolate = args.GetIsolate(); - uint32_t waypointId = convert::To(isolate, args[0]); - args.GetReturnValue().Set(unit.TakeWaypoint(waypointId)); + args.GetReturnValue().Set(unit.TakeWaypoint(convert::To(isolate, args[0]))); } else { args.GetReturnValue().Set(unit.Interact()); } @@ -1032,8 +1034,8 @@ void JSUnit::ConfigureTemplate(v8::Isolate* isolate, v8::Local( + args.GetReturnValue().Set(std::to_underlying( data->GetSkillId((nSkillId - 2) > 0 ? game::Hand::Left : game::Hand::Right))); break; case 4: { @@ -1086,7 +1088,8 @@ void JSUnit::ConfigureTemplate(v8::Isolate* isolate, v8::LocalSet(context, 0, convert::ToJS(isolate, skill.skillId)).Check(); + skillArr->Set(context, 0, convert::ToJS(isolate, std::to_underlying(skill.skillId))) + .Check(); skillArr->Set(context, 1, convert::ToJS(isolate, skill.baseLevel)).Check(); skillArr->Set(context, 2, convert::ToJS(isolate, skill.totalLevel)).Check(); arr->Set(context, i, skillArr).Check(); @@ -1108,7 +1111,7 @@ void JSUnit::ConfigureTemplate(v8::Isolate* isolate, v8::LocalGetSkillLevel(nSkillId, includeExtraLevels, charge)) { + if (auto level = data->GetSkillLevel(static_cast(nSkillId), includeExtraLevels, charge)) { args.GetReturnValue().Set(level.value()); } else { args.GetReturnValue().SetFalse(); @@ -1254,8 +1257,8 @@ void JSUnit::ConfigureTemplate(v8::Isolate* isolate, v8::Local(isolate, args[0]); + const auto nQuest = convert::To(isolate, args[0]); const auto nFlag = convert::To(isolate, args[1]); args.GetReturnValue().Set(game::GetQuestFlag(nQuest, nFlag) ? 1 : 0); }); /// @description Tests whether the given status-effect state id is currently active on the unit. - /// @signature getState(stateId: number) - /// @param stateId {number} - Required non-negative state id (no upper bound, since mods may add states). + /// @signature getState(stateId: State|number) + /// @param stateId {State|number} - Required non-negative state id (no upper bound, since mods may add states). /// @returns {boolean} - True if the state is active, false otherwise. Method( isolate, proto, "getState", +[](const v8::FunctionCallbackInfo& args) { @@ -1299,13 +1302,13 @@ void JSUnit::ConfigureTemplate(v8::Isolate* isolate, v8::LocalHasState(static_cast(nState))); + args.GetReturnValue().Set(data->HasState(static_cast(nState))); }); /// @description Reads unit stats: a single stat value, or one of two whole-table modes selected by a negative stat /// id. - /// @signature getStat(statId: number, subIndex?: number) - /// @param statId {number} - Stat id; -1 = flat array of [statId, subIndex, value] triples, -2 = sparse array + /// @signature getStat(statId: Stat|number, subIndex?: number) + /// @param statId {Stat|number} - Stat id; -1 = flat array of [statId, subIndex, value] triples, -2 = sparse array /// indexed by statId with detailed charge/skill info, otherwise a specific stat id. Special case: statId 92 (item /// level requirement) returns the computed required level (after item/quality modifiers) rather than the raw stat. /// The experience stats are returned as an unsigned double to avoid int32 overflow. @@ -1328,9 +1331,9 @@ void JSUnit::ConfigureTemplate(v8::Isolate* isolate, v8::Local(isolate, args[0]); - uint32_t subIndex = 0; + uint16_t subIndex = 0; if (args.Length() > 1) { - subIndex = convert::To(isolate, args[1]); + subIndex = static_cast(convert::To(isolate, args[1])); } std::vector stats; @@ -1344,20 +1347,19 @@ void JSUnit::ConfigureTemplate(v8::Isolate* isolate, v8::LocalGetAllStats(); } else if (statId == -2) { stats = data->GetDetailedStats(); - } else if (statId == static_cast(game::STAT_ITEMLEVELREQ)) { + } else if (const auto stat = static_cast(statId); stat == game::Stat::ItemLevelReq) { // Reference lines 916-940: special cases for the normal (statId >= 0) path - // STAT_ITEMLEVELREQ: reference calls D2COMMON_GetItemLevelRequirement + // ItemLevelReq: reference calls D2COMMON_GetItemLevelRequirement args.GetReturnValue().Set(data->LevelRequirement()); return; } else { // GetStat handles the >>8 shift for stats 6-11 internally. - int32_t value = data->GetStat(static_cast(statId), subIndex); - // Stats EXP, LASTEXP, NEXTEXP: return as unsigned double + int32_t value = data->GetStat(stat, subIndex); + // Experience, LastExperience, NextExperience: return as unsigned double // to handle large XP values that overflow int32_t. // Reference line 918-921: JS_NumberValue((unsigned int)value) - if (statId == static_cast(game::STAT_EXP) || - statId == static_cast(game::STAT_LASTEXP) || - statId == static_cast(game::STAT_NEXTEXP)) { + if (stat == game::Stat::Experience || stat == game::Stat::LastExperience || + stat == game::Stat::NextExperience) { args.GetReturnValue().Set( convert::ToJS(isolate, static_cast(static_cast(value)))); return; @@ -1375,7 +1377,7 @@ void JSUnit::ConfigureTemplate(v8::Isolate* isolate, v8::LocalSet(context, 0, convert::ToJS(isolate, entry.statId)).Check(); + statArr->Set(context, 0, convert::ToJS(isolate, std::to_underlying(entry.statId))).Check(); statArr->Set(context, 1, convert::ToJS(isolate, entry.subIndex)).Check(); statArr->Set(context, 2, convert::ToJS(isolate, entry.value)).Check(); arr->Set(context, i, statArr).Check(); @@ -1393,6 +1395,7 @@ void JSUnit::ConfigureTemplate(v8::Isolate* isolate, v8::Local 0x200) { // Charge/skill stat: build object with {skill, level, charges, maxcharges} int32_t skill = static_cast(entry.subIndex >> 6); @@ -1414,7 +1417,7 @@ void JSUnit::ConfigureTemplate(v8::Isolate* isolate, v8::Local existing; - if (arr->Get(context, entry.statId).ToLocal(&existing) && !existing->IsUndefined()) { + if (arr->Get(context, statIndex).ToLocal(&existing) && !existing->IsUndefined()) { if (existing->IsArray()) { auto existingArr = existing.As(); existingArr->Set(context, existingArr->Length(), obj).Check(); @@ -1422,10 +1425,10 @@ void JSUnit::ConfigureTemplate(v8::Isolate* isolate, v8::LocalSet(context, 0, existing).Check(); newArr->Set(context, 1, obj).Check(); - arr->Set(context, entry.statId, newArr).Check(); + arr->Set(context, statIndex, newArr).Check(); } } else { - arr->Set(context, entry.statId, obj).Check(); + arr->Set(context, statIndex, obj).Check(); } } else { // Normal stat: value placed in sub-array at arr[statId][subIndex]. @@ -1434,9 +1437,9 @@ void JSUnit::ConfigureTemplate(v8::Isolate* isolate, v8::Local existing; - if (!arr->Get(context, entry.statId).ToLocal(&existing) || existing->IsUndefined()) { + if (!arr->Get(context, statIndex).ToLocal(&existing) || existing->IsUndefined()) { existing = v8::Array::New(isolate, 0); - arr->Set(context, entry.statId, existing).Check(); + arr->Set(context, statIndex, existing).Check(); } if (existing->IsArray()) { existing.As() @@ -1455,8 +1458,8 @@ void JSUnit::ConfigureTemplate(v8::Isolate* isolate, v8::Local}>} - The stat - /// lists, empty when the unit has none; false if the game was not ready. + /// @returns {Array<{flags:StatListFlags,stateNo:number,stats:Array<{id:Stat,layer:number,value:number}>}>} - The + /// stat lists, empty when the unit has none; false if the game was not ready. Method( isolate, proto, "getStatLists", +[](const v8::FunctionCallbackInfo& args) { auto* isolate = args.GetIsolate(); @@ -1529,12 +1532,12 @@ void JSUnit::ConfigureTemplate(v8::Isolate* isolate, v8::LocalIsString()) { std::string skillName = convert::ToString(isolate, args[0]); auto resolved = game::GetSkillByName(skillName); @@ -1626,7 +1629,7 @@ void JSUnit::ConfigureTemplate(v8::Isolate* isolate, v8::LocalIsNumber()) { - skillId = convert::To(isolate, args[0]); + skillId = convert::To(isolate, args[0]); } else { args.GetReturnValue().SetFalse(); return; @@ -1754,9 +1757,8 @@ void JSUnit::ConfigureTemplate(v8::Isolate* isolate, v8::Local& args) { @@ -1844,10 +1846,9 @@ void JSUnit::ConfigureTemplate(v8::Isolate* isolate, v8::Local(npc->ClassId()) : static_cast(game::NPC_CHARSI_CLASS_ID); + uint32_t npcClassId = npc ? npc->ClassId() : game::NPC_CHARSI_CLASS_ID; if (args.Length() > 0 && args[0]->IsNumber()) { - npcClassId = convert::To(isolate, args[0]); + npcClassId = convert::To(isolate, args[0]); } args.GetReturnValue().Set(player.GetRepairCost(npcClassId)); }); diff --git a/src/runtime/api/core/Convert.h b/src/runtime/api/core/Convert.h index 4b0d53b..8cb6f18 100644 --- a/src/runtime/api/core/Convert.h +++ b/src/runtime/api/core/Convert.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -181,7 +182,7 @@ inline v8::Local ToJS(v8::Isolate* isolate, const game::StatEntry& s auto& keys = detail::Keys(isolate); auto context = isolate->GetCurrentContext(); auto obj = v8::Object::New(isolate); - obj->CreateDataProperty(context, keys.id.Get(isolate), ToJS(isolate, stat.statId)).Check(); + obj->CreateDataProperty(context, keys.id.Get(isolate), ToJS(isolate, std::to_underlying(stat.statId))).Check(); obj->CreateDataProperty(context, keys.layer.Get(isolate), ToJS(isolate, stat.subIndex)).Check(); obj->CreateDataProperty(context, keys.value.Get(isolate), ToJS(isolate, stat.value)).Check(); return obj; @@ -200,8 +201,9 @@ inline v8::Local ToJS(v8::Isolate* isolate, const game::StatListEntr } auto stats = v8::Array::New(isolate, elements.data(), elements.size()); auto obj = v8::Object::New(isolate); - obj->CreateDataProperty(context, keys.flags.Get(isolate), ToJS(isolate, list.flags)).Check(); - obj->CreateDataProperty(context, keys.stateNo.Get(isolate), ToJS(isolate, list.stateNo)).Check(); + obj->CreateDataProperty(context, keys.flags.Get(isolate), ToJS(isolate, std::to_underlying(list.flags))).Check(); + obj->CreateDataProperty(context, keys.stateNo.Get(isolate), ToJS(isolate, std::to_underlying(list.stateNo))) + .Check(); obj->CreateDataProperty(context, keys.stats.Get(isolate), stats).Check(); return obj; } diff --git a/src/runtime/api/globals/GameFunctions.cpp b/src/runtime/api/globals/GameFunctions.cpp index dd43156..79ab0f2 100644 --- a/src/runtime/api/globals/GameFunctions.cpp +++ b/src/runtime/api/globals/GameFunctions.cpp @@ -68,8 +68,9 @@ void RegisterGameFunctions(v8::Isolate* isolate, v8::Local g /// @description Find the first game unit matching optional type/name/classId/mode/id criteria. /// @signature getUnit(special: number) /// @param special {number} - 100 = cursor item, 101 = selected unit (falls back to selected inventory item) - /// @signature getUnit(type: number, name?: string, mode?: number, unitId?: number) - /// @param type {number} - a UnitType value; out-of-range searches all types. Pass -1 (or omit) for no filter. + /// @signature getUnit(type: UnitType, name?: string, mode?: number, unitId?: number) + /// @param type {UnitType|number} - a UnitType value; out-of-range searches all types. Pass -1 (or omit) for no + /// filter. /// @param name {string} - unit name filter. Pass -1 (or omit) for no filter. /// @param mode {number} - unit mode filter. Pass -1 (or omit) for no filter. Two special forms: if mode >= 100 and /// the unit is an item, it filters by item location matching (mode - 100) - 100=ground, 101=equipped, 102=belt, @@ -77,7 +78,7 @@ void RegisterGameFunctions(v8::Isolate* isolate, v8::Local g /// bits are a bitmask: the unit matches when its mode equals any bit position 0..27 that is set in mode (e.g. /// 0x20000003 matches modes 0 or 1). /// @param unitId {number} - specific unit id filter. Pass -1 (or omit) for no filter. - /// @signature getUnit(type: number, classId?: number, mode?: number, unitId?: number) + /// @signature getUnit(type: UnitType, classId?: number, mode?: number, unitId?: number) /// @param classId {number} - class id filter (mutually exclusive with name). Pass -1 (or omit) for no filter. /// @returns {Unit|undefined} - matching unit, or undefined if not found function::Register( @@ -164,9 +165,9 @@ void RegisterGameFunctions(v8::Isolate* isolate, v8::Local g }); /// @description Compute an A* path between two world coordinates on a level. - /// @signature getPath(area: number, srcX: number, srcY: number, dstX: number, dstY: number, reductionType?: number, - /// radius?: number, reject?: function, reduce?: function, mutate?: function) - /// @param area {number} - level/area id (must be non-zero) + /// @signature getPath(area: LevelId, srcX: number, srcY: number, dstX: number, dstY: number, reductionType?: + /// number, radius?: number, reject?: function, reduce?: function, mutate?: function) + /// @param area {LevelId} - level/area id (must be non-zero) /// @param srcX {number} - source x world coordinate /// @param srcY {number} - source y world coordinate /// @param dstX {number} - destination x world coordinate @@ -201,7 +202,7 @@ void RegisterGameFunctions(v8::Isolate* isolate, v8::Local g return; } - uint32_t area = convert::To(isolate, args[0]); + const auto area = convert::To(isolate, args[0]); auto src = extract::Position(args, 1).value_or(game::Position::Zero); auto dst = extract::Position(args, 3).value_or(game::Position::Zero); uint32_t reductionType = 0; @@ -213,7 +214,7 @@ void RegisterGameFunctions(v8::Isolate* isolate, v8::Local g radius = convert::To(isolate, args[6]); } - if (area == 0) { + if (area == game::LevelId::None) { error::ThrowError(isolate, "Invalid level passed to getPath"); return; } @@ -306,8 +307,8 @@ void RegisterGameFunctions(v8::Isolate* isolate, v8::Local g }); /// @description Read the collision flag at a world coordinate on a level. - /// @signature getCollision(levelId: number, x: number, y: number) - /// @param levelId {number} - level id + /// @signature getCollision(levelId: LevelId, x: number, y: number) + /// @param levelId {LevelId} - level id /// @param x {number} - world x coordinate /// @param y {number} - world y coordinate /// @returns {CollisionFlag} - collision flags at the cell, or 0 if no room contains the position @@ -326,7 +327,7 @@ void RegisterGameFunctions(v8::Isolate* isolate, v8::Local g return; } - uint32_t levelId = convert::To(isolate, args[0]); + const auto levelId = convert::To(isolate, args[0]); auto pos = extract::Position(args, 1).value(); // strict IsUint32 above guarantees this bool isLevelLoaded = false; @@ -388,13 +389,13 @@ void RegisterGameFunctions(v8::Isolate* isolate, v8::Local g // type 1 = shop mode (check for shop-specific cursor), 0 = regular cursor bool isShop = (type == 1); - args.GetReturnValue().Set(static_cast(game::GetCursorType(isShop))); + args.GetReturnValue().Set(game::GetCursorType(isShop)); }); /// @description Look up a skill id by its localized name. /// @signature getSkillByName(name: string) /// @param name {string} - skill name - /// @returns {number|undefined} - skill id, or undefined if not found + /// @returns {Skill|undefined} - skill id, or undefined if not found function::Register( isolate, global, "getSkillByName", +[](const v8::FunctionCallbackInfo& args) { auto* isolate = args.GetIsolate(); @@ -408,12 +409,12 @@ void RegisterGameFunctions(v8::Isolate* isolate, v8::Local g if (!skillId) { return; } - args.GetReturnValue().Set(*skillId); + args.GetReturnValue().Set(std::to_underlying(*skillId)); }); /// @description Resolve a skill's localized name from its id (skills.txt -> skilldesc -> locale string). - /// @signature getSkillById(skillId: number) - /// @param skillId {number} - skill id + /// @signature getSkillById(skillId: Skill) + /// @param skillId {Skill} - skill id /// @returns {string} - localized skill name, or "Unknown" on lookup failure function::Register( isolate, global, "getSkillById", +[](const v8::FunctionCallbackInfo& args) { @@ -525,8 +526,8 @@ void RegisterGameFunctions(v8::Isolate* isolate, v8::Local g }); /// @description Query trade-related info by mode. - /// @signature getTradeInfo(mode: number) - /// @param mode {number} - 0 = recent trade id, 1 = recent trade name, 2 = recent trade id (alt) + /// @signature getTradeInfo(mode: TradeInfoMode) + /// @param mode {TradeInfoMode} - which trade value to read /// @returns {number|string|null|false} - id for modes 0/2, name-or-null for mode 1, false otherwise function::Register( isolate, global, "getTradeInfo", +[](const v8::FunctionCallbackInfo& args) { @@ -600,12 +601,12 @@ void RegisterGameFunctions(v8::Isolate* isolate, v8::Local g /// @description Get a Room object by level, by coordinates, or for the player's current level. /// @signature getRoom() - /// @signature getRoom(levelId: number) - /// @param levelId {number} - level id; 0 returns the player's current room, else the level's first room + /// @signature getRoom(levelId: LevelId) + /// @param levelId {LevelId} - level id; 0 returns the player's current room, else the level's first room /// @signature getRoom(x: number, y: number) /// @param x {number} - world x coordinate (must be non-zero); searches the player's level /// @param y {number} - world y coordinate (must be non-zero) - /// @signature getRoom(levelId: number, x: number, y: number) + /// @signature getRoom(levelId: LevelId, x: number, y: number) /// @returns {Room|undefined} - the matching Room; falls back to the level's first room if no coordinate match /// @throws {Error} - the game is not ready function::Register( @@ -647,8 +648,8 @@ void RegisterGameFunctions(v8::Isolate* isolate, v8::Local g room = level.GetFirstRoom(); } else if (args.Length() == 1 && args[0]->IsNumber()) { // getRoom(levelId): get first room of the level, or player's room if 0 - uint32_t levelId = convert::To(isolate, args[0]); - if (levelId == 0) { + const auto levelId = convert::To(isolate, args[0]); + if (levelId == game::LevelId::None) { // Reference: levelId==0 returns player's current room auto player = game::Unit::Player(); if (!player) { @@ -669,8 +670,7 @@ void RegisterGameFunctions(v8::Isolate* isolate, v8::Local g if (isLevelForm) { // getRoom(levelId, x, y) - uint32_t levelId = convert::To(isolate, args[0]); - level = game::Level::Get(levelId); + level = game::Level::Get(convert::To(isolate, args[0])); if (!level) { return; } @@ -778,8 +778,8 @@ void RegisterGameFunctions(v8::Isolate* isolate, v8::Local g }); /// @description Find the first preset unit in a level, optionally filtered by type and class id. - /// @signature getPresetUnit(areaId: number, type?: UnitType|number, classId?: number) - /// @param areaId {number} - level/area id + /// @signature getPresetUnit(areaId: LevelId, type?: UnitType|number, classId?: number) + /// @param areaId {LevelId} - level/area id /// @param type {UnitType|number} - unit type to match; pass -1 or omit to match any type. /// @param classId {number} - unit class id to match; pass -1 or omit to match any class. /// @returns {PresetUnit|false} - the first matching preset unit, or false if none @@ -799,7 +799,7 @@ void RegisterGameFunctions(v8::Isolate* isolate, v8::Local g return; } - uint32_t areaId = convert::To(isolate, args[0]); + const auto areaId = convert::To(isolate, args[0]); std::optional nType; std::optional nClassId; if (args.Length() > 1 && args[1]->IsUint32()) { @@ -837,8 +837,8 @@ void RegisterGameFunctions(v8::Isolate* isolate, v8::Local g }); /// @description Get all preset units in a level as an array, optionally filtered by type and class id. - /// @signature getPresetUnits(areaId: number, type?: UnitType|number, classId?: number) - /// @param areaId {number} - level/area id + /// @signature getPresetUnits(areaId: LevelId, type?: UnitType|number, classId?: number) + /// @param areaId {LevelId} - level/area id /// @param type {UnitType|number} - unit type to match; pass -1 or omit to match any type. /// @param classId {number} - unit class id to match; pass -1 or omit to match any class. /// @returns {Array|false} - array of matching preset units (possibly empty), or false if no args @@ -858,7 +858,7 @@ void RegisterGameFunctions(v8::Isolate* isolate, v8::Local g return; } - uint32_t areaId = convert::To(isolate, args[0]); + const auto areaId = convert::To(isolate, args[0]); std::optional nType; std::optional nClassId; if (args.Length() > 1 && args[1]->IsUint32()) { @@ -897,8 +897,8 @@ void RegisterGameFunctions(v8::Isolate* isolate, v8::Local g }); /// @description Get an Area object for a level, defaulting to the player's current area. - /// @signature getArea(areaId?: number) - /// @param areaId {number} - area/level id (non-negative); omitted uses the player's current area + /// @signature getArea(areaId?: LevelId) + /// @param areaId {LevelId} - area/level id (non-negative); omitted uses the player's current area /// @returns {Area|false|undefined} - the Area object, false if the level is not loaded, undefined if no player /// @throws {Error} - the game is not ready /// @throws {Error} - areaId is negative @@ -912,7 +912,7 @@ void RegisterGameFunctions(v8::Isolate* isolate, v8::Local g return; } - uint32_t areaId = 0; + auto areaId = game::LevelId::None; if (args.Length() >= 1) { if (!args[0]->IsNumber()) { error::ThrowError(isolate, "Invalid parameter passed to getArea!"); @@ -923,7 +923,7 @@ void RegisterGameFunctions(v8::Isolate* isolate, v8::Local g error::ThrowError(isolate, "Invalid parameter passed to getArea!"); return; } - areaId = static_cast(signedId); + areaId = static_cast(signedId); } else { // Default: use player's current area game::GameReadLock lock; @@ -998,8 +998,8 @@ void RegisterGameFunctions(v8::Isolate* isolate, v8::Local g }); /// @description Find the first UI control matching optional position/size filters; menu state only. - /// @signature getControl(type?: number, x?: number, y?: number, xsize?: number, ysize?: number) - /// @param type {number} - control type filter + /// @signature getControl(type?: ControlType, x?: number, y?: number, xsize?: number, ysize?: number) + /// @param type {ControlType} - control type filter /// @param x {number} - x position filter /// @param y {number} - y position filter /// @param xsize {number} - width filter @@ -1276,16 +1276,16 @@ void RegisterGameFunctions(v8::Isolate* isolate, v8::Local g /// @description Equip an item, click a body slot, click an item by handle, or click a container grid slot. /// @signature clickItem(item: Unit) /// @param item {Unit} - item Unit; equipped into the player body slot its data points to - /// @signature clickItem(clickType: number, bodyLoc: number) + /// @signature clickItem(clickType: number, bodyLoc: BodyLocation) /// @param clickType {number} - 0 = player body slot, 4 = merc body slot; any other value is a no-op that returns /// true - /// @param bodyLoc {number} - body location id + /// @param bodyLoc {BodyLocation} - body location id /// @signature clickItem(clickType: number, item: Unit) - /// @signature clickItem(button: number, x: number, y: number, location: number) - /// @param button {number} - click button id + /// @signature clickItem(button: ClickButton, x: number, y: number, location: ItemLocation) + /// @param button {ClickButton} - click button id /// @param x {number} - container grid x /// @param y {number} - container grid y - /// @param location {number} - item container/location id + /// @param location {ItemLocation} - item container/location id /// @returns {boolean|null} - true/null/false per shape (false while a trade is open); throws "Object is not an /// item!" for item-by-handle on a non-item /// @throws {Error} - item-by-handle shape is used on a unit that is not an item @@ -1466,10 +1466,9 @@ void RegisterGameFunctions(v8::Isolate* isolate, v8::Local g }); /// @description Perform a party action (invite/leave/loot/hostile) on a party member. - /// @signature clickParty(party: Party, mode: number) + /// @signature clickParty(party: Party, mode: PartyMode) /// @param party {Party} - the target party member object (cannot be yourself) - /// @param mode {number} - party action mode: 0 = allow loot, 1 = unhostile, 2 = invite, 3 = leave, 4 = hostile, 5 = - /// hostile (alt) + /// @param mode {PartyMode} - party action mode /// @returns {boolean} - true if the action dispatched, false on bad args or no-op conditions function::Register( isolate, global, "clickParty", +[](const v8::FunctionCallbackInfo& args) { @@ -1673,7 +1672,7 @@ void RegisterGameFunctions(v8::Isolate* isolate, v8::Local g if (nParameter != 0) { // Return current weapon switch state - args.GetReturnValue().Set(static_cast(game::GetWeaponSwitch())); + args.GetReturnValue().Set(game::GetWeaponSwitch()); } else { // Reference JSGame.cpp:1243 - classic D2 has no weapon switch. if (!HasFlag(game::GetCharFlags(), game::CharFlag::Expansion)) { @@ -1686,8 +1685,8 @@ void RegisterGameFunctions(v8::Isolate* isolate, v8::Local g }); /// @description Spend unallocated stat points on an attribute. - /// @signature useStatPoint(stat: number, count?: number) - /// @param stat {number} - stat/attribute id + /// @signature useStatPoint(stat: Stat, count?: number) + /// @param stat {Stat} - stat/attribute id /// @param count {number} - number of points to spend (default 1) /// @returns {undefined} - no return value function::Register( @@ -1704,17 +1703,17 @@ void RegisterGameFunctions(v8::Isolate* isolate, v8::Local g return; } - uint32_t stat = convert::To(isolate, args[0]); - int32_t count = 1; + const auto stat = convert::To(isolate, args[0]); + uint16_t count = 1; if (args.Length() > 1) { - count = convert::To(isolate, args[1]); + count = convert::To(isolate, args[1]); } game::UseStatPoint(stat, count); }); /// @description Spend unallocated skill points on a skill. - /// @signature useSkillPoint(skill: number, count?: number) - /// @param skill {number} - skill id + /// @signature useSkillPoint(skill: Skill|number, count?: number) + /// @param skill {Skill|number} - skill id /// @param count {number} - number of points to spend (default 1) /// @returns {undefined} - no return value function::Register( @@ -1731,10 +1730,10 @@ void RegisterGameFunctions(v8::Isolate* isolate, v8::Local g return; } - uint32_t skill = convert::To(isolate, args[0]); - int32_t count = 1; + const auto skill = convert::To(isolate, args[0]); + uint16_t count = 1; if (args.Length() > 1) { - count = convert::To(isolate, args[1]); + count = convert::To(isolate, args[1]); } game::UseSkillPoint(skill, count); }); @@ -2096,10 +2095,10 @@ void RegisterGameFunctions(v8::Isolate* isolate, v8::Local g /// @description Perform a gold action through the game's gold dialog: drop, move to the trade window, or move /// between carried gold and the stash (stash panel open). - /// @signature gold(amount?: number, mode?: number) - /// @param amount {number} - gold amount (default 0) - /// @param mode {number} - 1 = drop, 2 = inventory to trade, 3 = deposit into the stash, 4 = withdraw from the stash - /// (default 1, drop, as in the reference) + /// @signature gold(amount?: number, mode?: GoldActionMode) + /// @param amount {number} - gold amount (default 0); a negative amount does nothing + /// @param mode {GoldActionMode} - drop, move to the trade window, or deposit into / withdraw from the stash + /// (default Drop, as in the reference) /// @returns {undefined} - no return value function::Register( isolate, global, "gold", +[](const v8::FunctionCallbackInfo& args) { @@ -2110,10 +2109,13 @@ void RegisterGameFunctions(v8::Isolate* isolate, v8::Local g return; } - int32_t nGold = 0; + uint32_t nGold = 0; auto nMode = game::GoldActionMode::Drop; if (args.Length() > 0 && args[0]->IsNumber()) { - nGold = convert::To(isolate, args[0]); + if (args[0].As()->Value() < 0) { + return; + } + nGold = convert::To(isolate, args[0]); } if (args.Length() > 1 && args[1]->IsNumber()) { nMode = convert::To(isolate, args[1]); @@ -2181,8 +2183,8 @@ void RegisterGameFunctions(v8::Isolate* isolate, v8::Local g }); /// @description Accept a trade, or query trade state by mode. - /// @signature acceptTrade(mode?: number) - /// @param mode {number} - query: 1 = is accepted, 2 = recent trade id, 3 = is blocked; omit/other to accept + /// @signature acceptTrade(mode?: AcceptTradeQueryMode) + /// @param mode {AcceptTradeQueryMode} - the state to query; omit (or any other value) to accept /// @returns {boolean|number} - query result (bool or id) for query modes, else the accept result (bool) function::Register( isolate, global, "acceptTrade", +[](const v8::FunctionCallbackInfo& args) { @@ -2336,7 +2338,7 @@ void RegisterGameFunctions(v8::Isolate* isolate, v8::Local g /// @description The areas currently desecrated (terror zones), any number of them in the game's order; always /// empty on 1.14d, which has none. /// @signature getDesecratedZones() - /// @returns {Array<{area:number, waypointArea:number, monsterLevel:number, source:DesecratedZoneSource, + /// @returns {Array<{area:LevelId, waypointArea:LevelId, monsterLevel:number, source:DesecratedZoneSource, /// uniqueMod:number}>} - one entry per zone: the area, the area whose waypoint marks it, the monster level it /// raises the area to, what activated it, and the zone's configured always-unique modifier (a monumod.txt id, 0 = /// none). @@ -2349,8 +2351,10 @@ void RegisterGameFunctions(v8::Isolate* isolate, v8::Local g uint32_t i = 0; for (const auto& zone : zones) { auto obj = v8::Object::New(isolate); - obj->Set(context, convert::ToJS(isolate, "area"), convert::ToJS(isolate, zone.area)).Check(); - obj->Set(context, convert::ToJS(isolate, "waypointArea"), convert::ToJS(isolate, zone.waypointArea)) + obj->Set(context, convert::ToJS(isolate, "area"), convert::ToJS(isolate, std::to_underlying(zone.area))) + .Check(); + obj->Set(context, convert::ToJS(isolate, "waypointArea"), + convert::ToJS(isolate, std::to_underlying(zone.waypointArea))) .Check(); obj->Set(context, convert::ToJS(isolate, "monsterLevel"), convert::ToJS(isolate, zone.monsterLevel)) .Check(); @@ -2380,7 +2384,7 @@ void RegisterGameFunctions(v8::Isolate* isolate, v8::Local g drawPresets = args[0]->BooleanValue(isolate); } - uint32_t levelNo = 0; + auto levelNo = game::LevelId::None; { game::GameReadLock lock; auto player = game::Unit::Player(); diff --git a/src/runtime/api/globals/MenuFunctions.cpp b/src/runtime/api/globals/MenuFunctions.cpp index 8ba08cc..e2a116d 100644 --- a/src/runtime/api/globals/MenuFunctions.cpp +++ b/src/runtime/api/globals/MenuFunctions.cpp @@ -103,14 +103,19 @@ void RegisterMenuFunctions(v8::Isolate* isolate, v8::Local g }); /// @description Creates a new character at the menu with the given name and class. - /// @signature createCharacter(name: string, type: number, hardcore?: boolean, ladder?: boolean) + /// @signature createCharacter(name: string, type: CharacterClass, hardcore?: boolean, ladder?: boolean, gameType?: + /// GameType) /// @param name {string} - Desired character name. - /// @param type {number} - Character class index: 0 = Amazon, 1 = Sorceress, 2 = Necromancer, 3 = Paladin, 4 = - /// Barbarian, 5 = Druid, 6 = Assassin. + /// @param type {CharacterClass} - Character class. /// @param hardcore {boolean} - Create as hardcore; defaults to false. /// @param ladder {boolean} - Create as ladder; defaults to false. + /// @param gameType {GameType} - The character's game type; defaults to RotW for a Warlock and Expansion for every + /// other class. A Warlock must be RotW, and a Classic character cannot be an Assassin or a Druid. The Warlock + /// class and the RotW game type do nothing on 1.14d: it creates only Expansion characters and returns false for + /// any other game type, so a Warlock is never created there. /// @returns {boolean} - Result of the creation attempt; undefined if not at the menu. Throws on invalid arguments. - /// @throws {Error} - Character type is outside the valid class range (0-6). + /// @throws {Error} - Character type is outside the valid class range (0-7), the game type is not a GameType, or + /// the class cannot be created with that game type. function::Register( isolate, global, "createCharacter", +[](const v8::FunctionCallbackInfo& args) { auto* isolate = args.GetIsolate(); @@ -134,7 +139,7 @@ void RegisterMenuFunctions(v8::Isolate* isolate, v8::Local g } std::string name = convert::ToString(isolate, args[0]); - int32_t type = convert::To(isolate, args[1]); + const auto charClass = convert::To(isolate, args[1]); bool hardcore = false; if (args.Length() > 2 && args[2]->IsBoolean()) { @@ -146,22 +151,43 @@ void RegisterMenuFunctions(v8::Isolate* isolate, v8::Local g ladder = args[3]->BooleanValue(isolate); } - // Validate character class (0-6) - if (type < 0 || type > static_cast(game::CharacterClass::Assassin)) { + // Unsigned, so a negative class wraps past Warlock too. + if (charClass > game::CharacterClass::Warlock) { error::ThrowError(isolate, "Invalid character type"); return; } - args.GetReturnValue().Set( - game::CreateCharacter(name, static_cast(type), hardcore, ladder)); + auto gameType = + charClass == game::CharacterClass::Warlock ? game::GameType::RotW : game::GameType::Expansion; + if (args.Length() > 4 && !args[4]->IsUndefined()) { + if (!error::CheckIsNumber(args, 4, "gameType")) { + return; + } + gameType = convert::To(isolate, args[4]); + if (gameType > game::GameType::RotW) { + error::ThrowError(isolate, "Invalid game type"); + return; + } + } + // The game refuses both combinations. + if (charClass == game::CharacterClass::Warlock && gameType != game::GameType::RotW) { + error::ThrowError(isolate, "A Warlock can only be created as a Reign of the Warlock character"); + return; + } + if (gameType == game::GameType::Classic && + (charClass == game::CharacterClass::Assassin || charClass == game::CharacterClass::Druid)) { + error::ThrowError(isolate, "Assassins and Druids cannot be Classic characters"); + return; + } + + args.GetReturnValue().Set(game::CreateCharacter(name, charClass, hardcore, ladder, gameType)); }); /// @description Creates an online game at the menu with the given name, password, and difficulty. - /// @signature createGame(name: string, password?: string, difficulty?: number) + /// @signature createGame(name: string, password?: string, difficulty?: Difficulty) /// @param name {string} - Game name, max 15 characters. /// @param password {string} - Game password, max 15 characters. - /// @param difficulty {number} - Difficulty: 0 = Normal, 1 = Nightmare, 2 = Hell, 3 = highest available; defaults to - /// 3. + /// @param difficulty {Difficulty} - Defaults to HighestAvailable. /// @returns {null} - Always null; no-op unless at the menu. Throws on validation or create failure. /// @throws {Error} - Game name or password exceeds 15 characters. /// @throws {Error} - Difficulty is outside 0-3. @@ -207,7 +233,7 @@ void RegisterMenuFunctions(v8::Isolate* isolate, v8::Local g } // Get optional difficulty (0-2, default 3 for highest available) - int32_t diff = 3; + int32_t diff = static_cast(game::Difficulty::HighestAvailable); if (args.Length() > 2) { if (!args[2]->IsNumber()) { error::ThrowTypeError(isolate, "Invalid arguments specified to createGame"); @@ -282,15 +308,14 @@ void RegisterMenuFunctions(v8::Isolate* isolate, v8::Local g /// @description Adds or overwrites a stored profile in the d2bs profile config. /// @signature addProfile(profileName: string, mode: string, gateway: string, username: string, password: string, - /// charname: string, spdifficulty?: number) + /// charname: string, spdifficulty?: Difficulty) /// @param profileName {string} - Profile name (key). /// @param mode {string} - Profile mode string; must map to a known profile type. /// @param gateway {string} - Realm/gateway. /// @param username {string} - Account username; used as the IP address for TCP/IP join profiles. /// @param password {string} - Account password. /// @param charname {string} - Character name. - /// @param spdifficulty {number} - Single-player difficulty: 0 = Normal, 1 = Nightmare, 2 = Hell, 3 = highest - /// available; defaults to 3. + /// @param spdifficulty {Difficulty} - Single-player difficulty; defaults to HighestAvailable. /// @returns {null} - Always null. Throws on invalid arguments. /// @throws {Error} - spdifficulty is outside 0-3. /// @throws {Error} - mode string does not map to a known profile type. diff --git a/src/runtime/components/characterstate/CharacterState.cpp b/src/runtime/components/characterstate/CharacterState.cpp index 6343b1c..c29715c 100644 --- a/src/runtime/components/characterstate/CharacterState.cpp +++ b/src/runtime/components/characterstate/CharacterState.cpp @@ -52,7 +52,7 @@ constexpr size_t BUCKET_COUNT = 6; // is complete when its reward is granted or pending - not the COMPLETEDNOW/BEFORE // bits, which the record load clears, so a quest done in a prior game reads as // incomplete. -constexpr uint32_t QUEST_COUNT = 41; +constexpr uint32_t QUEST_COUNT = std::to_underlying(game::Quest::EveOfDestruction) + 1; constexpr uint32_t WAYPOINT_COUNT = 39; json BuildItems(const std::vector& items) { @@ -129,8 +129,9 @@ json BuildProgression() { json quests = json::array(); for (uint32_t questId = 0; questId < QUEST_COUNT; ++questId) { - if (game::GetQuestFlag(questId, game::QuestFlag::RewardGranted) || - game::GetQuestFlag(questId, game::QuestFlag::RewardPending)) { + const auto quest = static_cast(questId); + if (game::GetQuestFlag(quest, game::QuestFlag::RewardGranted) || + game::GetQuestFlag(quest, game::QuestFlag::RewardPending)) { quests.push_back(questId); // reward granted or pending == completed } } diff --git a/src/runtime/components/characterstate/UnitJson.cpp b/src/runtime/components/characterstate/UnitJson.cpp index 7200457..13aebc8 100644 --- a/src/runtime/components/characterstate/UnitJson.cpp +++ b/src/runtime/components/characterstate/UnitJson.cpp @@ -75,7 +75,7 @@ std::string ReverseLines(const std::string& text) { // Stat values are stored raw (pre-nValShift) so they stay stable across wearers; undo the // shift game::Unit::GetStatLists applies to hp/mana/stamina. int32_t RawValue(const game::StatEntry& stat) { - if (stat.statId >= game::STAT_FIXED_POINT_FIRST && stat.statId <= game::STAT_FIXED_POINT_LAST) { + if (game::IsFixedPointStat(stat.statId)) { return stat.value << game::STAT_FIXED_POINT_SHIFT; } return stat.value; @@ -85,12 +85,12 @@ void VisitStatLists(UnitVisitor& visitor, const game::Unit& item) { visitor.BeginArray("statsLists"); for (const auto& list : item.GetStatLists()) { visitor.BeginElement(); - visitor.Int("stateNo", list.stateNo); - visitor.Int("flags", list.flags); + visitor.Int("stateNo", std::to_underlying(list.stateNo)); + visitor.Int("flags", std::to_underlying(list.flags)); visitor.BeginArray("stats"); for (const auto& stat : list.stats) { visitor.BeginElement(); - visitor.Int("id", stat.statId); + visitor.Int("id", std::to_underlying(stat.statId)); visitor.Int("value", RawValue(stat)); if (stat.subIndex != 0) { visitor.Int("layer", stat.subIndex); @@ -176,7 +176,7 @@ void VisitWearer(UnitVisitor& visitor, const game::Unit& wearer) { visitor.BeginArray("skills"); for (const auto& skill : skills) { visitor.BeginElement(); - visitor.Int("skill", skill.skillId); + visitor.Int("skill", std::to_underlying(skill.skillId)); visitor.Int("hard", skill.baseLevel); visitor.Int("level", skill.totalLevel); visitor.EndElement(); @@ -223,8 +223,30 @@ class JsonVisitor final : public UnitVisitor { // Curated stat ids the manager's StatsPanel labels (D2BotNG stats.ts). Order is // irrelevant; the manager renders only ids it recognises. -constexpr std::array STAT_IDS = {0, 1, 2, 3, 7, 9, 12, 13, 14, 15, 39, - 40, 41, 42, 43, 44, 45, 46, 80, 96, 99, 105}; +constexpr std::array STAT_IDS = { + game::Stat::Strength, + game::Stat::Energy, + game::Stat::Dexterity, + game::Stat::Vitality, + game::Stat::MaxHitPoints, + game::Stat::MaxMana, + game::Stat::Level, + game::Stat::Experience, + game::Stat::Gold, + game::Stat::GoldBank, + game::Stat::FireResist, + game::Stat::MaxFireResist, + game::Stat::LightningResist, + game::Stat::MaxLightningResist, + game::Stat::ColdResist, + game::Stat::MaxColdResist, + game::Stat::PoisonResist, + game::Stat::MaxPoisonResist, + game::Stat::ItemMagicBonus, + game::Stat::ItemFasterMoveVelocity, + game::Stat::ItemFasterGetHitRate, + game::Stat::ItemFasterCastRate, +}; } // namespace @@ -243,7 +265,7 @@ void VisitUnit(UnitVisitor& visitor, const game::Unit& unit, Detail detail) { break; case game::UnitType::Player: VisitWearer(visitor, unit); - visitor.Int("area", unit.Area()); + visitor.Int("area", std::to_underlying(unit.Area())); // Reads a client global, so it only answers for the local player. visitor.Int("hand", unit.WeaponSwitch()); break; @@ -268,11 +290,11 @@ json WearerStats(const game::Unit& wearer) { // each id once. Only latched once every lookup returned a real cell - caching a miss // (tables not loaded yet) would render signed stats wrong for the process lifetime. // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) - game-thread only - static std::vector> curated; + static std::vector> curated; if (curated.size() != STAT_IDS.size()) { curated.clear(); - for (const uint32_t id : STAT_IDS) { - const auto cell = game::GetTxtValue("itemstatcost", id, "signed"); + for (const auto id : STAT_IDS) { + const auto cell = game::GetTxtValue("itemstatcost", std::to_underlying(id), "signed"); const auto* flag = std::get_if(&cell); if (flag == nullptr) { curated.clear(); @@ -286,7 +308,7 @@ json WearerStats(const game::Unit& wearer) { for (const auto& [id, isSigned] : curated) { const int32_t raw = wearer.GetStat(id); json entry = json::object(); - entry["id"] = id; + entry["id"] = std::to_underlying(id); entry["value"] = isSigned ? static_cast(raw) : static_cast(static_cast(raw)); stats.push_back(std::move(entry)); } diff --git a/src/runtime/components/console/SettingsPanel.cpp b/src/runtime/components/console/SettingsPanel.cpp index ef7073e..ef98c50 100644 --- a/src/runtime/components/console/SettingsPanel.cpp +++ b/src/runtime/components/console/SettingsPanel.cpp @@ -160,7 +160,7 @@ void SettingsPanel::Draw() { DisplayRow("OOG location", "%u (%s)", static_cast(oog), oogName.c_str()); } EnumRow("Difficulty", game::GetDifficulty()); - DisplayRow("Mode", "%s", game::GetGameType() == 0 ? "Classic" : "Expansion"); + EnumRow("Game type", game::GetGameType()); DisplayRow("Ping", "%u ms", game::GetPing()); DisplayRow("FPS", "%u", game::GetFPS()); const auto gameName = game::GetGameName(); diff --git a/src/runtime/components/gameloop/GameLoop.h b/src/runtime/components/gameloop/GameLoop.h index e261223..ef439ba 100644 --- a/src/runtime/components/gameloop/GameLoop.h +++ b/src/runtime/components/gameloop/GameLoop.h @@ -33,7 +33,7 @@ struct Snapshot { std::optional playerId; std::optional hp; std::optional mp; - uint32_t areaId = 0; + game::LevelId areaId = game::LevelId::None; bool waitForProfile = false; std::string profileName; diff --git a/src/runtime/components/navigation/ExitFinder.cpp b/src/runtime/components/navigation/ExitFinder.cpp index 9fb5a3e..fa3e3b1 100644 --- a/src/runtime/components/navigation/ExitFinder.cpp +++ b/src/runtime/components/navigation/ExitFinder.cpp @@ -45,6 +45,7 @@ namespace d2bs::runtime::navigation { using game::GameReadLock; +using game::LevelId; using game::Point; using game::Position; using game::Rect; @@ -115,7 +116,7 @@ namespace { return std::ranges::any_of(exits, [&pos](const ExitInfo& e) { return e.pos == pos; }); } -[[nodiscard]] bool HasExitToLevel(const std::vector& exits, uint32_t target) { +[[nodiscard]] bool HasExitToLevel(const std::vector& exits, LevelId target) { return std::ranges::any_of(exits, [target](const ExitInfo& e) { return e.target == target; }); } @@ -126,7 +127,7 @@ std::vector GetExits(game::Level level) { return {}; } GameReadLock guard; - const uint32_t levelId = level.Id(); + const LevelId levelId = level.Id(); std::vector exits; // ----------------------------------------------------------------- @@ -135,7 +136,7 @@ std::vector GetExits(game::Level level) { // ----------------------------------------------------------------- for (auto room = level.GetFirstRoom(); room; room = room.GetNext()) { for (const auto& preset : room.GetPresetUnits(UnitType::Tile)) { - if (preset.tileTargetLevelId == 0) { + if (preset.tileTargetLevelId == LevelId::None) { continue; } const auto roomOrigin = room.Bounds().origin; @@ -172,7 +173,7 @@ std::vector GetExits(game::Level level) { Position pos; int64_t centreDistSq; }; - std::multimap candidates; + std::multimap candidates; for (auto room = level.GetFirstRoom(); room; room = room.GetNext()) { const Rect a = room.Bounds(); @@ -182,7 +183,7 @@ std::vector GetExits(game::Level level) { const int32_t aMaxY = aMinY + static_cast(a.size.height); for (auto neighbour : room.GetNearby()) { - const uint32_t neighbourLevel = neighbour.LevelId(); + const LevelId neighbourLevel = neighbour.LevelId(); if (neighbourLevel == levelId) { continue; } @@ -300,7 +301,7 @@ std::vector GetExits(game::Level level) { // Pick the best (smallest centre-distance) run for each destination // level and emit as the linkage exit. for (auto it = candidates.begin(); it != candidates.end();) { - const uint32_t target = it->first; + const LevelId target = it->first; auto upper = candidates.upper_bound(target); const RunCandidate* best = &it->second; for (auto cur = std::next(it); cur != upper; ++cur) { diff --git a/src/runtime/components/navigation/ExitFinder.h b/src/runtime/components/navigation/ExitFinder.h index a352d15..79880c4 100644 --- a/src/runtime/components/navigation/ExitFinder.h +++ b/src/runtime/components/navigation/ExitFinder.h @@ -21,10 +21,10 @@ enum class ExitType : uint32_t { struct ExitInfo { game::Position pos; // game coordinates - see docs/coords.md - uint32_t target; + game::LevelId target; ExitType type; uint32_t tileId; - uint32_t level; + game::LevelId level; }; // Level transitions leading out of `level`: tile warps and room-to-room diff --git a/src/runtime/components/navigation/Pathfinder.cpp b/src/runtime/components/navigation/Pathfinder.cpp index 2562586..9ec66a5 100644 --- a/src/runtime/components/navigation/Pathfinder.cpp +++ b/src/runtime/components/navigation/Pathfinder.cpp @@ -173,8 +173,10 @@ LevelGrid BuildLevelGrid(game::Level level) { } // Barricade tower avoidance for specific levels - uint32_t levelId = level.Id(); - if (levelId == 74 || levelId == 111 || levelId == 112 || levelId == 117) { + using game::LevelId; + const LevelId levelId = level.Id(); + if (levelId == LevelId::ArcaneSanctuary || levelId == LevelId::FrigidHighlands || + levelId == LevelId::ArreatPlateau || levelId == LevelId::FrozenTundra) { for (auto room = level.GetFirstRoom(); room; room = room.GetNext()) { auto roomPos = room.Bounds().origin; for (const auto& preset : room.GetPresetUnits()) { diff --git a/src/runtime/components/navigation/Pathfinder.h b/src/runtime/components/navigation/Pathfinder.h index a6f7876..9182c8b 100644 --- a/src/runtime/components/navigation/Pathfinder.h +++ b/src/runtime/components/navigation/Pathfinder.h @@ -45,13 +45,13 @@ struct LevelGrid { Rect rect; // Identity of the grid: which level it represents, and the map seed of - // the game that produced it. Both default to 0; BuildLevelGrid stamps + // the game that produced it. Both default to None / 0; BuildLevelGrid stamps // them. Together they uniquely identify a generated map - same levelId // across two games has different room layouts because the map seed (D2 // randomly generates each level per game) differs. Caches (e.g. // FindPath's thread-local) compare both to know if a cached slab is // still valid for a follow-up request. - uint32_t levelId = 0; + game::LevelId levelId = game::LevelId::None; uint32_t mapSeed = 0; LevelGrid() = default; @@ -121,7 +121,7 @@ struct GridWindow { struct CollisionLookup { LevelGrid primary; - std::unordered_map secondary; + std::unordered_map secondary; // Spatial-locality cache: once A* exits `primary` into a secondary grid, // subsequent probes repeatedly hit that same grid. `Get()` keeps this up @@ -214,7 +214,7 @@ struct CollisionLookup { }; struct PathRequest { - uint32_t areaId; + game::LevelId areaId; Position start; Position end; ReductionType reduction = ReductionType::Walk; diff --git a/tests/runtime/fakes/GameHelpers.cpp b/tests/runtime/fakes/GameHelpers.cpp index d105ef7..2d464af 100644 --- a/tests/runtime/fakes/GameHelpers.cpp +++ b/tests/runtime/fakes/GameHelpers.cpp @@ -51,14 +51,14 @@ void SetNoPickUp(bool /*value*/) {} uint32_t GetWeaponSwitch() { return 0; } -uint32_t GetGameType() { - return 0; +GameType GetGameType() { + return GameType::Classic; } uint32_t GetMercReviveCost() { return 0; } -uint32_t GetLocale() { - return 0; +Language GetLocale() { + return Language::English; } // === BnetData Queries === @@ -181,7 +181,7 @@ bool TestPvpFlag(const Unit& /*a*/, const Unit& /*b*/, RosterFlag /*flag*/) { bool HasWaypoint(uint32_t /*waypointId*/) { return false; } -bool IsTownByLevelNo(uint32_t levelNo) { +bool IsTownByLevelNo(LevelId levelNo) { auto& s = test::State(); return s.isTown && levelNo == s.areaId; } @@ -197,8 +197,8 @@ bool GetQuestFlag(uint32_t /*quest*/, QuestFlag /*flag*/) { // === Weapon / Stat / Skill Actions === void SwapWeapon() {} -void UseStatPoint(uint32_t /*stat*/, uint32_t /*count*/) {} -void UseSkillPoint(uint32_t /*skill*/, uint32_t /*count*/) {} +void UseStatPoint(Stat /*stat*/, uint32_t /*count*/) {} +void UseSkillPoint(Skill /*skill*/, uint32_t /*count*/) {} void TakeScreenshot() {} // === Item Actions === @@ -227,7 +227,7 @@ void* GetHwnd() { } void GoldAction(GoldActionMode /*mode*/, int32_t /*amount*/) {} void MoveNPC(uint32_t /*npcId*/, Position /*pos*/) {} -bool RevealLevel(uint32_t /*levelNo*/, bool /*drawPresets*/) { +bool RevealLevel(LevelId /*levelNo*/, bool /*drawPresets*/) { return false; } diff --git a/tests/runtime/fakes/GameLoopCollaborators.h b/tests/runtime/fakes/GameLoopCollaborators.h index 74f5ab1..85d8148 100644 --- a/tests/runtime/fakes/GameLoopCollaborators.h +++ b/tests/runtime/fakes/GameLoopCollaborators.h @@ -21,7 +21,7 @@ struct GameLoopState { std::optional playerId; std::optional hp; std::optional mp; - uint32_t areaId = 0; + d2bs::game::LevelId areaId = d2bs::game::LevelId::None; bool isTown = false; // Side effects captured from GameLoop::OnSleep. diff --git a/tests/runtime/fakes/Level.cpp b/tests/runtime/fakes/Level.cpp index b3aa634..6a6fcfe 100644 --- a/tests/runtime/fakes/Level.cpp +++ b/tests/runtime/fakes/Level.cpp @@ -28,7 +28,7 @@ Room Level::GetFirstRoom() const { return Room(); } -std::optional Level::Get(uint32_t /*levelNo*/) { +std::optional Level::Get(LevelId /*levelNo*/) { return std::nullopt; } diff --git a/tests/runtime/fakes/Menu.cpp b/tests/runtime/fakes/Menu.cpp index 5435d2f..cb41d09 100644 --- a/tests/runtime/fakes/Menu.cpp +++ b/tests/runtime/fakes/Menu.cpp @@ -8,8 +8,8 @@ LoginResult Login(const config::ProfileData& /*profile*/) { bool SelectCharacter(const std::string& /*charName*/) { return false; } -bool CreateCharacter(const std::string& /*name*/, CharacterClass /*charClass*/, bool /*isHardcore*/, - bool /*isLadder*/) { +bool CreateCharacter(const std::string& /*name*/, CharacterClass /*charClass*/, bool /*isHardcore*/, bool /*isLadder*/, + GameType /*gameType*/) { return false; } bool CreateGame(const std::string& /*name*/, const std::string& /*password*/, Difficulty /*difficulty*/) { diff --git a/tests/runtime/fakes/Room.cpp b/tests/runtime/fakes/Room.cpp index 7242834..a84db46 100644 --- a/tests/runtime/fakes/Room.cpp +++ b/tests/runtime/fakes/Room.cpp @@ -33,8 +33,8 @@ Rect Room::Bounds() const { return Rect::Zero; } -uint32_t Room::CorrectTomb() const { - return 0; +LevelId Room::CorrectTomb() const { + return game::LevelId::None; } std::vector> Room::GetCollision() const { diff --git a/tests/runtime/fakes/Unit.cpp b/tests/runtime/fakes/Unit.cpp index 0ede1c5..77d9ed8 100644 --- a/tests/runtime/fakes/Unit.cpp +++ b/tests/runtime/fakes/Unit.cpp @@ -35,7 +35,7 @@ uint32_t Unit::Mp() const { return test::State().mp.value_or(0); } -uint32_t Unit::Area() const { +LevelId Unit::Area() const { return test::State().areaId; } diff --git a/tests/runtime/fixtures/MapFixture.cpp b/tests/runtime/fixtures/MapFixture.cpp index ed23bec..50f68b3 100644 --- a/tests/runtime/fixtures/MapFixture.cpp +++ b/tests/runtime/fixtures/MapFixture.cpp @@ -29,7 +29,7 @@ namespace { // but every valid value is non-negative, so the bit pattern is identical to // uint32 and reads correctly into the unsigned aggregates. struct D2ColHeader { - uint32_t levelId; + game::LevelId levelId; game::Rect rect; }; diff --git a/tests/runtime/fixtures/MapFixture.h b/tests/runtime/fixtures/MapFixture.h index 52ab992..9debefc 100644 --- a/tests/runtime/fixtures/MapFixture.h +++ b/tests/runtime/fixtures/MapFixture.h @@ -17,7 +17,7 @@ namespace d2bs::test { // int32[width*height] collision (uint16 values stored as int32) struct MapFixture { - uint32_t levelId = 0; + game::LevelId levelId = game::LevelId::None; d2bs::runtime::navigation::LevelGrid grid; static std::optional Load(const std::filesystem::path& path); diff --git a/tests/runtime/gameloop/GameLoopTest.cpp b/tests/runtime/gameloop/GameLoopTest.cpp index cbfda39..d78b4b9 100644 --- a/tests/runtime/gameloop/GameLoopTest.cpp +++ b/tests/runtime/gameloop/GameLoopTest.cpp @@ -56,7 +56,8 @@ TEST_CASE_FIXTURE(GameLoopFixture, "First tick emits melife/memana/playerassign s.playerId = 42; s.hp = 500; s.mp = 200; - s.areaId = 1; // Act 1 rogue camp would be isTown=true, but we leave isTown=false to not trigger chicken + s.areaId = d2bs::game::LevelId::RogueEncampment; // Act 1 rogue camp would be isTown=true, but we leave + // isTown=false to not trigger chicken s.isTown = true; GameLoop::Instance().OnSleep(std::chrono::milliseconds{0}); @@ -75,7 +76,7 @@ TEST_CASE_FIXTURE(GameLoopFixture, "Successive ticks with no change emit nothing s.playerId = 42; s.hp = 500; s.mp = 200; - s.areaId = 1; + s.areaId = d2bs::game::LevelId::RogueEncampment; s.isTown = true; GameLoop::Instance().OnSleep(std::chrono::milliseconds{0}); @@ -97,7 +98,7 @@ TEST_CASE_FIXTURE(GameLoopFixture, "HP change emits melife on second tick") { s.playerId = 42; s.hp = 500; s.mp = 200; - s.areaId = 1; + s.areaId = d2bs::game::LevelId::RogueEncampment; s.isTown = true; GameLoop::Instance().OnSleep(std::chrono::milliseconds{0}); @@ -119,7 +120,7 @@ TEST_CASE_FIXTURE(GameLoopFixture, "HP drop below threshold triggers chicken whe s.playerId = 42; s.hp = 500; s.mp = 200; - s.areaId = 10; // not a town area + s.areaId = d2bs::game::LevelId::UndergroundPassageLevel1; // not a town area s.isTown = false; GameLoop::Instance().OnSleep(std::chrono::milliseconds{0}); @@ -139,7 +140,7 @@ TEST_CASE_FIXTURE(GameLoopFixture, "Town exclusion suppresses HP chicken") { s.playerId = 42; s.hp = 50; // below threshold on tick 1 s.mp = 200; - s.areaId = 1; + s.areaId = d2bs::game::LevelId::RogueEncampment; s.isTown = true; GameLoop::Instance().OnSleep(std::chrono::milliseconds{0}); @@ -157,7 +158,7 @@ TEST_CASE_FIXTURE(GameLoopFixture, "Town exclusion does not suppress maxGameTime s.playerId = 42; s.hp = 500; s.mp = 200; - s.areaId = 1; + s.areaId = d2bs::game::LevelId::RogueEncampment; s.isTown = true; // First tick anchors gameStartedAt_. @@ -775,7 +776,7 @@ TEST_CASE_FIXTURE(GameLoopFixture, "OnSleep is a no-op pre-Initialize") { s.playerId = 42; s.hp = 500; s.mp = 200; - s.areaId = 1; + s.areaId = d2bs::game::LevelId::RogueEncampment; s.isTown = false; GameLoop::Instance().OnSleep(std::chrono::milliseconds{0}); diff --git a/tests/runtime/pathfinding/tests/FixtureTest.cpp b/tests/runtime/pathfinding/tests/FixtureTest.cpp index 3082ad1..5c4ddb4 100644 --- a/tests/runtime/pathfinding/tests/FixtureTest.cpp +++ b/tests/runtime/pathfinding/tests/FixtureTest.cpp @@ -18,7 +18,7 @@ namespace { MapFixture MakeSyntheticFixture() { MapFixture fixture; - fixture.levelId = 42; + fixture.levelId = d2bs::game::LevelId::DryHills; fixture.grid = LevelGrid({.origin = {.x = 500, .y = 500}, .size = {.width = 200, .height = 200}}); // Vertical wall at x=600, gap at y=599..601 for cross-check clearance diff --git a/tests/runtime/pathfinding/tests/comparisons/RealWorldBenchmarkTest.cpp b/tests/runtime/pathfinding/tests/comparisons/RealWorldBenchmarkTest.cpp index ad78915..b556c72 100644 --- a/tests/runtime/pathfinding/tests/comparisons/RealWorldBenchmarkTest.cpp +++ b/tests/runtime/pathfinding/tests/comparisons/RealWorldBenchmarkTest.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include @@ -48,8 +49,8 @@ TEST_CASE("Real world benchmarks") { SUBCASE(file.stem().string().c_str()) { auto& grid = fixture->grid; - MESSAGE("Level " << fixture->levelId << " (" << grid.rect.size.width << "x" << grid.rect.size.height - << ")"); + MESSAGE("Level " << std::to_underlying(fixture->levelId) << " (" << grid.rect.size.width << "x" + << grid.rect.size.height << ")"); // Find walkable start and end points. // Start: first walkable tile from top-left.