From 9093975bddd83933393d0bd895cb3686ab3d0dc2 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 15:04:48 -0700 Subject: [PATCH] feat(pipeline): add WIT (Wowee Item Template) format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Novel open replacement for Blizzard's Item.dbc + ItemDisplayInfo.dbc + the SQL item_template tables that AzerothCore-style servers store item definitions in. The 12th open format added to the editor. A WIT file holds the catalog of all items in a content pack: weapons, armor, consumables, quest items, trade goods. Each entry pairs gameplay metadata (stats, level reqs, flags, weapon damage / speed) with display metadata (displayId for icon / model, quality color), so the runtime can render inventory tooltips and equip slots from a single load. Format: • magic "WITM", version 1, little-endian • per item: itemId / displayId / quality / itemClass / itemSubClass / inventoryType / flags / requiredLevel / itemLevel / sellPrice / buyPrice / maxStack / durability / damageMin / damageMax / attackSpeedMs / statCount + stats[] / name / description Enums: • Quality: Poor..Heirloom (8 levels) • Class: Consumable, Weapon, Armor, Quest, ... (13) • InventoryType: Head..Cloak..Weapon2H (18 slots) • Flags: Unique, BoP, BoE, QuestItem, Conjured, ... • StatType: Stamina, Strength, Intellect, Defense, ... API: WoweeItemLoader::save / load / exists / findById; presets makeStarter (4-item demo), makeWeapons (5 items common -> legendary), makeArmor (6-piece mail set with BoE flag). CLI added (5 flags, 480 documented total now): --gen-items / --gen-items-weapons / --gen-items-armor --info-wit / --validate-wit Validator catches: itemId=0, duplicate itemIds, weapons with 0 damage or attackSpeed, weapons with non-weapon slot, equippables with durability=0 or maxStack>1, sell price >= buy price (vendor would lose money), out-of-range quality. All 3 presets save / load / re-validate clean. Info-table output includes a gold/silver/copper price formatter for hand-readability. --- CMakeLists.txt | 3 + include/pipeline/wowee_items.hpp | 185 ++++++++++++++ src/pipeline/wowee_items.cpp | 378 +++++++++++++++++++++++++++++ tools/editor/cli_arg_required.cpp | 2 + tools/editor/cli_dispatch.cpp | 2 + tools/editor/cli_help.cpp | 10 + tools/editor/cli_items_catalog.cpp | 279 +++++++++++++++++++++ tools/editor/cli_items_catalog.hpp | 11 + 8 files changed, 870 insertions(+) create mode 100644 include/pipeline/wowee_items.hpp create mode 100644 src/pipeline/wowee_items.cpp create mode 100644 tools/editor/cli_items_catalog.cpp create mode 100644 tools/editor/cli_items_catalog.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 23c30d2f..bc0aa751 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -599,6 +599,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_world_map.cpp src/pipeline/wowee_sound.cpp src/pipeline/wowee_spawns.cpp + src/pipeline/wowee_items.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1344,6 +1345,7 @@ add_executable(wowee_editor tools/editor/cli_world_map.cpp tools/editor/cli_sound_catalog.cpp tools/editor/cli_spawns_catalog.cpp + tools/editor/cli_items_catalog.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp @@ -1421,6 +1423,7 @@ add_executable(wowee_editor src/pipeline/wowee_world_map.cpp src/pipeline/wowee_sound.cpp src/pipeline/wowee_spawns.cpp + src/pipeline/wowee_items.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_items.hpp b/include/pipeline/wowee_items.hpp new file mode 100644 index 00000000..07968ed1 --- /dev/null +++ b/include/pipeline/wowee_items.hpp @@ -0,0 +1,185 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Item Template (.wit) — novel replacement for +// Blizzard's Item.dbc + ItemDisplayInfo.dbc + the SQL +// item_template tables that AzerothCore-style servers store +// item definitions in. The 12th open format added to the +// editor. +// +// One file holds the catalog of all items in a content pack: +// weapons, armor, consumables, quest items, trade goods, etc. +// Each entry pairs the gameplay metadata (stats, level reqs, +// flags) with the display metadata (icon / model displayId, +// quality color) — the runtime needs both together to render +// inventory tooltips and equip slots. +// +// Binary layout (little-endian): +// magic[4] = "WITM" +// version (uint32) = current 1 +// nameLen (uint32) + name bytes -- catalog label +// entryCount (uint32) +// entries (each): +// itemId (uint32) +// displayId (uint32) +// quality (uint8) -- poor..artifact +// itemClass (uint8) -- consumable / weapon / armor / ... +// itemSubClass (uint8) +// inventoryType (uint8) -- equip slot (0 = non-equip) +// flags (uint32) -- unique / BoP / BoE / quest / ... +// requiredLevel (uint16) +// itemLevel (uint16) +// sellPriceCopper (uint32) +// buyPriceCopper (uint32) +// maxStack (uint16) +// durability (uint16) -- 0 for non-equippable +// damageMin (uint32) -- weapons only; 0 otherwise +// damageMax (uint32) +// attackSpeedMs (uint32) +// statCount (uint8) + pad[3] +// stats (statCount × {type: uint8, pad, value: int16}) +// nameLen (uint32) + name bytes +// descLen (uint32) + description bytes +struct WoweeItem { + enum Quality : uint8_t { + Poor = 0, + Common = 1, + Uncommon = 2, + Rare = 3, + Epic = 4, + Legendary = 5, + Artifact = 6, + Heirloom = 7, + }; + + enum Class : uint8_t { + Consumable = 0, + Container = 1, + Weapon = 2, + Gem = 3, + Armor = 4, + Reagent = 5, + Projectile = 6, + TradeGoods = 7, + Recipe = 9, + Quiver = 11, + Quest = 12, + Key = 13, + Misc = 15, + }; + + enum InventoryType : uint8_t { + NonEquip = 0, + Head = 1, + Neck = 2, + Shoulders = 3, + Body = 4, // shirt + Chest = 5, + Waist = 6, + Legs = 7, + Feet = 8, + Wrists = 9, + Hands = 10, + Finger = 11, + Trinket = 12, + Weapon1H = 13, + Shield = 14, + Ranged = 15, + Cloak = 16, + Weapon2H = 17, + }; + + enum Flags : uint32_t { + Unique = 0x01, + Conjured = 0x02, + Openable = 0x04, + Heroic = 0x08, + BindOnPickup = 0x10, + BindOnEquip = 0x20, + QuestItem = 0x40, + NoSellable = 0x80, + }; + + // Common stat types (matches WoW's ItemModType numbering + // for the most-used subset; the format permits any uint8). + enum StatType : uint8_t { + StatNone = 0, + StatHealth = 1, + StatMana = 2, + StatAgility = 3, + StatStrength = 4, + StatIntellect = 5, + StatSpirit = 6, + StatStamina = 7, + StatDefense = 31, + }; + + struct Stat { + uint8_t type = StatNone; + int16_t value = 0; + }; + + struct Entry { + uint32_t itemId = 0; + uint32_t displayId = 0; + uint8_t quality = Common; + uint8_t itemClass = Misc; + uint8_t itemSubClass = 0; + uint8_t inventoryType = NonEquip; + uint32_t flags = 0; + uint16_t requiredLevel = 0; + uint16_t itemLevel = 1; + uint32_t sellPriceCopper = 0; + uint32_t buyPriceCopper = 0; + uint16_t maxStack = 1; + uint16_t durability = 0; + uint32_t damageMin = 0; + uint32_t damageMax = 0; + uint32_t attackSpeedMs = 0; + std::vector stats; // up to 255 (uint8 count) + std::string name; + std::string description; + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + // Lookup by itemId — nullptr if not present. + const Entry* findById(uint32_t itemId) const; + + static const char* qualityName(uint8_t q); + static const char* classNameOf(uint8_t c); + static const char* slotName(uint8_t s); + static const char* statName(uint8_t t); +}; + +class WoweeItemLoader { +public: + static bool save(const WoweeItem& cat, + const std::string& basePath); + static WoweeItem load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-items* variants. + // + // makeStarter — a tiny demo catalog: 1 weapon + 1 chest + // armor + 1 healing potion + 1 quest item. + // makeWeapons — 5 weapon entries spanning common, + // uncommon, rare, epic; both 1H and 2H. + // makeArmor — full gear set: head + chest + legs + + // feet + hands + cloak. + static WoweeItem makeStarter(const std::string& catalogName); + static WoweeItem makeWeapons(const std::string& catalogName); + static WoweeItem makeArmor(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_items.cpp b/src/pipeline/wowee_items.cpp new file mode 100644 index 00000000..ea26aede --- /dev/null +++ b/src/pipeline/wowee_items.cpp @@ -0,0 +1,378 @@ +#include "pipeline/wowee_items.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'I', 'T', 'M'}; +constexpr uint32_t kVersion = 1; + +template +void writePOD(std::ofstream& os, const T& v) { + os.write(reinterpret_cast(&v), sizeof(T)); +} + +template +bool readPOD(std::ifstream& is, T& v) { + is.read(reinterpret_cast(&v), sizeof(T)); + return is.gcount() == static_cast(sizeof(T)); +} + +void writeStr(std::ofstream& os, const std::string& s) { + uint32_t n = static_cast(s.size()); + writePOD(os, n); + if (n > 0) os.write(s.data(), n); +} + +bool readStr(std::ifstream& is, std::string& s) { + uint32_t n = 0; + if (!readPOD(is, n)) return false; + if (n > (1u << 20)) return false; // 1 MiB sanity cap + s.resize(n); + if (n > 0) { + is.read(s.data(), n); + if (is.gcount() != static_cast(n)) { + s.clear(); + return false; + } + } + return true; +} + +std::string normalizePath(std::string base) { + if (base.size() < 4 || base.substr(base.size() - 4) != ".wit") { + base += ".wit"; + } + return base; +} + +} // namespace + +const WoweeItem::Entry* WoweeItem::findById(uint32_t itemId) const { + for (const auto& e : entries) { + if (e.itemId == itemId) return &e; + } + return nullptr; +} + +const char* WoweeItem::qualityName(uint8_t q) { + switch (q) { + case Poor: return "poor"; + case Common: return "common"; + case Uncommon: return "uncommon"; + case Rare: return "rare"; + case Epic: return "epic"; + case Legendary: return "legendary"; + case Artifact: return "artifact"; + case Heirloom: return "heirloom"; + default: return "unknown"; + } +} + +const char* WoweeItem::classNameOf(uint8_t c) { + switch (c) { + case Consumable: return "consumable"; + case Container: return "container"; + case Weapon: return "weapon"; + case Gem: return "gem"; + case Armor: return "armor"; + case Reagent: return "reagent"; + case Projectile: return "projectile"; + case TradeGoods: return "trade-goods"; + case Recipe: return "recipe"; + case Quiver: return "quiver"; + case Quest: return "quest"; + case Key: return "key"; + case Misc: return "misc"; + default: return "unknown"; + } +} + +const char* WoweeItem::slotName(uint8_t s) { + switch (s) { + case NonEquip: return "-"; + case Head: return "head"; + case Neck: return "neck"; + case Shoulders: return "shoulders"; + case Body: return "shirt"; + case Chest: return "chest"; + case Waist: return "waist"; + case Legs: return "legs"; + case Feet: return "feet"; + case Wrists: return "wrists"; + case Hands: return "hands"; + case Finger: return "finger"; + case Trinket: return "trinket"; + case Weapon1H: return "weapon-1h"; + case Shield: return "shield"; + case Ranged: return "ranged"; + case Cloak: return "cloak"; + case Weapon2H: return "weapon-2h"; + default: return "?"; + } +} + +const char* WoweeItem::statName(uint8_t t) { + switch (t) { + case StatNone: return "-"; + case StatHealth: return "health"; + case StatMana: return "mana"; + case StatAgility: return "agility"; + case StatStrength: return "strength"; + case StatIntellect: return "intellect"; + case StatSpirit: return "spirit"; + case StatStamina: return "stamina"; + case StatDefense: return "defense"; + default: return "stat?"; + } +} + +bool WoweeItemLoader::save(const WoweeItem& cat, + const std::string& basePath) { + std::ofstream os(normalizePath(basePath), std::ios::binary); + if (!os) return false; + os.write(kMagic, 4); + writePOD(os, kVersion); + writeStr(os, cat.name); + uint32_t entryCount = static_cast(cat.entries.size()); + writePOD(os, entryCount); + for (const auto& e : cat.entries) { + writePOD(os, e.itemId); + writePOD(os, e.displayId); + writePOD(os, e.quality); + writePOD(os, e.itemClass); + writePOD(os, e.itemSubClass); + writePOD(os, e.inventoryType); + writePOD(os, e.flags); + writePOD(os, e.requiredLevel); + writePOD(os, e.itemLevel); + writePOD(os, e.sellPriceCopper); + writePOD(os, e.buyPriceCopper); + writePOD(os, e.maxStack); + writePOD(os, e.durability); + writePOD(os, e.damageMin); + writePOD(os, e.damageMax); + writePOD(os, e.attackSpeedMs); + // statCount is uint8; cap to 255 to avoid silent truncation + // when a hand-edit JSON has more. + uint8_t statCount = static_cast( + e.stats.size() > 255 ? 255 : e.stats.size()); + writePOD(os, statCount); + uint8_t pad[3] = {0, 0, 0}; + os.write(reinterpret_cast(pad), 3); + for (uint8_t k = 0; k < statCount; ++k) { + const auto& s = e.stats[k]; + writePOD(os, s.type); + uint8_t spad = 0; + writePOD(os, spad); + writePOD(os, s.value); + } + writeStr(os, e.name); + writeStr(os, e.description); + } + return os.good(); +} + +WoweeItem WoweeItemLoader::load(const std::string& basePath) { + WoweeItem out; + std::ifstream is(normalizePath(basePath), std::ios::binary); + if (!is) return out; + char magic[4]; + is.read(magic, 4); + if (std::memcmp(magic, kMagic, 4) != 0) return out; + uint32_t version = 0; + if (!readPOD(is, version) || version != kVersion) return out; + if (!readStr(is, out.name)) return out; + uint32_t entryCount = 0; + if (!readPOD(is, entryCount)) return out; + if (entryCount > (1u << 20)) return out; + out.entries.resize(entryCount); + for (auto& e : out.entries) { + if (!readPOD(is, e.itemId) || + !readPOD(is, e.displayId) || + !readPOD(is, e.quality) || + !readPOD(is, e.itemClass) || + !readPOD(is, e.itemSubClass) || + !readPOD(is, e.inventoryType) || + !readPOD(is, e.flags) || + !readPOD(is, e.requiredLevel) || + !readPOD(is, e.itemLevel) || + !readPOD(is, e.sellPriceCopper) || + !readPOD(is, e.buyPriceCopper) || + !readPOD(is, e.maxStack) || + !readPOD(is, e.durability) || + !readPOD(is, e.damageMin) || + !readPOD(is, e.damageMax) || + !readPOD(is, e.attackSpeedMs)) { + out.entries.clear(); + return out; + } + uint8_t statCount = 0; + if (!readPOD(is, statCount)) { + out.entries.clear(); return out; + } + uint8_t pad[3]; + is.read(reinterpret_cast(pad), 3); + if (is.gcount() != 3) { out.entries.clear(); return out; } + e.stats.resize(statCount); + for (uint8_t k = 0; k < statCount; ++k) { + if (!readPOD(is, e.stats[k].type)) { + out.entries.clear(); return out; + } + uint8_t spad = 0; + if (!readPOD(is, spad)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.stats[k].value)) { + out.entries.clear(); return out; + } + } + if (!readStr(is, e.name) || !readStr(is, e.description)) { + out.entries.clear(); + return out; + } + } + return out; +} + +bool WoweeItemLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeItem WoweeItemLoader::makeStarter(const std::string& catalogName) { + WoweeItem c; + c.name = catalogName; + { + WoweeItem::Entry e; + e.itemId = 1; e.displayId = 100; + e.quality = WoweeItem::Common; e.itemClass = WoweeItem::Weapon; + e.itemSubClass = 0; // sword 1H + e.inventoryType = WoweeItem::Weapon1H; + e.requiredLevel = 1; e.itemLevel = 5; + e.sellPriceCopper = 50; e.buyPriceCopper = 200; + e.maxStack = 1; e.durability = 50; + e.damageMin = 4; e.damageMax = 9; e.attackSpeedMs = 1800; + e.stats.push_back({WoweeItem::StatStrength, 1}); + e.name = "Worn Shortsword"; + e.description = "A simple training sword."; + c.entries.push_back(e); + } + { + WoweeItem::Entry e; + e.itemId = 2; e.displayId = 200; + e.quality = WoweeItem::Common; e.itemClass = WoweeItem::Armor; + e.itemSubClass = 1; // cloth chest + e.inventoryType = WoweeItem::Chest; + e.requiredLevel = 1; e.itemLevel = 5; + e.sellPriceCopper = 30; e.buyPriceCopper = 150; + e.maxStack = 1; e.durability = 40; + e.stats.push_back({WoweeItem::StatStamina, 1}); + e.name = "Linen Vest"; + e.description = "Plain linen."; + c.entries.push_back(e); + } + { + WoweeItem::Entry e; + e.itemId = 3; e.displayId = 300; + e.quality = WoweeItem::Common; + e.itemClass = WoweeItem::Consumable; + e.itemSubClass = 0; // potion + e.inventoryType = WoweeItem::NonEquip; + e.requiredLevel = 1; e.itemLevel = 1; + e.sellPriceCopper = 5; e.buyPriceCopper = 25; + e.maxStack = 20; e.durability = 0; + e.name = "Minor Healing Potion"; + e.description = "Restores a small amount of health."; + c.entries.push_back(e); + } + { + WoweeItem::Entry e; + e.itemId = 4; e.displayId = 400; + e.quality = WoweeItem::Common; + e.itemClass = WoweeItem::Quest; + e.itemSubClass = 0; + e.inventoryType = WoweeItem::NonEquip; + e.flags = WoweeItem::QuestItem | WoweeItem::Unique | + WoweeItem::NoSellable; + e.maxStack = 1; e.durability = 0; + e.name = "Tattered Letter"; + e.description = "Deliver to the captain in the next town."; + c.entries.push_back(e); + } + return c; +} + +WoweeItem WoweeItemLoader::makeWeapons(const std::string& catalogName) { + WoweeItem c; + c.name = catalogName; + auto addWeapon = [&](uint32_t id, uint32_t disp, uint8_t qual, + uint8_t slot, uint16_t lvlReq, uint16_t ilvl, + uint32_t dmgMin, uint32_t dmgMax, uint32_t speedMs, + const char* name) { + WoweeItem::Entry e; + e.itemId = id; e.displayId = disp; + e.quality = qual; e.itemClass = WoweeItem::Weapon; + e.itemSubClass = (slot == WoweeItem::Weapon2H ? 1 : 0); + e.inventoryType = slot; + e.requiredLevel = lvlReq; e.itemLevel = ilvl; + e.sellPriceCopper = ilvl * 10u; + e.buyPriceCopper = ilvl * 100u; + e.maxStack = 1; + e.durability = static_cast(40 + ilvl); + e.damageMin = dmgMin; e.damageMax = dmgMax; + e.attackSpeedMs = speedMs; + e.stats.push_back({WoweeItem::StatStrength, + static_cast(1 + ilvl / 5)}); + e.name = name; + c.entries.push_back(e); + }; + addWeapon(1001, 1100, WoweeItem::Common, WoweeItem::Weapon1H, 1, 5, 4, 9, 1800, "Apprentice Sword"); + addWeapon(1002, 1101, WoweeItem::Uncommon, WoweeItem::Weapon1H, 10, 15, 12, 22, 1700, "Journeyman Blade"); + addWeapon(1003, 1102, WoweeItem::Rare, WoweeItem::Weapon1H, 20, 30, 28, 48, 1600, "Steelthorn Edge"); + addWeapon(1004, 1103, WoweeItem::Epic, WoweeItem::Weapon2H, 35, 50, 70, 110, 3200, "Bloodforged Greatsword"); + addWeapon(1005, 1104, WoweeItem::Legendary, WoweeItem::Weapon2H, 50, 70, 130, 195, 3000, "Doombringer"); + return c; +} + +WoweeItem WoweeItemLoader::makeArmor(const std::string& catalogName) { + WoweeItem c; + c.name = catalogName; + auto addArmor = [&](uint32_t id, uint32_t disp, uint8_t slot, + uint16_t ilvl, int16_t stam, int16_t str_, + const char* name) { + WoweeItem::Entry e; + e.itemId = id; e.displayId = disp; + e.quality = WoweeItem::Uncommon; + e.itemClass = WoweeItem::Armor; + e.itemSubClass = 3; // mail + e.inventoryType = slot; + e.requiredLevel = 20; e.itemLevel = ilvl; + e.sellPriceCopper = ilvl * 8u; + e.buyPriceCopper = ilvl * 80u; + e.maxStack = 1; + e.durability = static_cast(60 + ilvl); + e.flags = WoweeItem::BindOnEquip; + if (stam) e.stats.push_back({WoweeItem::StatStamina, stam}); + if (str_) e.stats.push_back({WoweeItem::StatStrength, str_}); + e.stats.push_back({WoweeItem::StatDefense, + static_cast(ilvl / 5)}); + e.name = name; + c.entries.push_back(e); + }; + addArmor(2001, 2100, WoweeItem::Head, 25, 6, 4, "Iron Helm"); + addArmor(2002, 2101, WoweeItem::Chest, 25, 9, 6, "Iron Chestguard"); + addArmor(2003, 2102, WoweeItem::Legs, 25, 7, 5, "Iron Legguards"); + addArmor(2004, 2103, WoweeItem::Feet, 25, 4, 3, "Iron Boots"); + addArmor(2005, 2104, WoweeItem::Hands, 25, 4, 3, "Iron Gauntlets"); + addArmor(2006, 2105, WoweeItem::Cloak, 25, 5, 0, "Traveler's Cloak"); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index cb450c7a..b33c29e5 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -32,6 +32,8 @@ const char* const kArgRequired[] = { "--gen-spawns", "--gen-spawns-camp", "--gen-spawns-village", "--info-wspn", "--validate-wspn", "--export-wspn-json", "--import-wspn-json", + "--gen-items", "--gen-items-weapons", "--gen-items-armor", + "--info-wit", "--validate-wit", "--gen-weather-temperate", "--gen-weather-arctic", "--gen-weather-desert", "--gen-weather-stormy", "--gen-zone-atmosphere", diff --git a/tools/editor/cli_dispatch.cpp b/tools/editor/cli_dispatch.cpp index 6ad4bacd..8ac19b13 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -39,6 +39,7 @@ #include "cli_world_map.hpp" #include "cli_sound_catalog.hpp" #include "cli_spawns_catalog.hpp" +#include "cli_items_catalog.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -119,6 +120,7 @@ constexpr DispatchFn kDispatchTable[] = { handleWorldMap, handleSoundCatalog, handleSpawnsCatalog, + handleItemsCatalog, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index 581b0d19..4de838e1 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -865,6 +865,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wspn to a human-editable JSON sidecar (defaults to .wspn.json)\n"); std::printf(" --import-wspn-json [out-base]\n"); std::printf(" Import a .wspn.json sidecar back into binary .wspn (accepts either kind int OR kindName string)\n"); + std::printf(" --gen-items [name]\n"); + std::printf(" Emit .wit starter item catalog: 1 weapon + 1 chest + 1 potion + 1 quest item\n"); + std::printf(" --gen-items-weapons [name]\n"); + std::printf(" Emit .wit weapon catalog: 5 entries spanning common -> legendary, both 1H and 2H\n"); + std::printf(" --gen-items-armor [name]\n"); + std::printf(" Emit .wit full mail-armor set: head + chest + legs + feet + hands + cloak (BoE)\n"); + std::printf(" --info-wit [--json]\n"); + std::printf(" Print WIT item entries (id / ilvl / quality / class / slot / buy price / name)\n"); + std::printf(" --validate-wit [--json]\n"); + std::printf(" Static checks: itemId>0 + unique, weapon damage>0 + min<=max, equippable durability>0, sell [zoneName]\n"); std::printf(" Emit .wow weather schedule: clear-dominant + occasional rain + fog (forest / grassland)\n"); std::printf(" --gen-weather-arctic [zoneName]\n"); diff --git a/tools/editor/cli_items_catalog.cpp b/tools/editor/cli_items_catalog.cpp new file mode 100644 index 00000000..5f5d8edb --- /dev/null +++ b/tools/editor/cli_items_catalog.cpp @@ -0,0 +1,279 @@ +#include "cli_items_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_items.hpp" +#include + +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWitExt(std::string base) { + stripExt(base, ".wit"); + return base; +} + +bool saveOrError(const wowee::pipeline::WoweeItem& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeItemLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wit\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeItem& c, + const std::string& base) { + std::printf("Wrote %s.wit\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" entries : %zu\n", c.entries.size()); +} + +int handleGenStarter(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "StarterItems"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWitExt(base); + auto c = wowee::pipeline::WoweeItemLoader::makeStarter(name); + if (!saveOrError(c, base, "gen-items")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenWeapons(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "WeaponCatalog"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWitExt(base); + auto c = wowee::pipeline::WoweeItemLoader::makeWeapons(name); + if (!saveOrError(c, base, "gen-items-weapons")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenArmor(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "ArmorCatalog"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWitExt(base); + auto c = wowee::pipeline::WoweeItemLoader::makeArmor(name); + if (!saveOrError(c, base, "gen-items-armor")) return 1; + printGenSummary(c, base); + return 0; +} + +void printPriceCopper(uint32_t copper) { + uint32_t gold = copper / 10000; + uint32_t silver = (copper / 100) % 100; + uint32_t cop = copper % 100; + std::printf("%ug %us %uc", gold, silver, cop); +} + +int handleInfo(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWitExt(base); + if (!wowee::pipeline::WoweeItemLoader::exists(base)) { + std::fprintf(stderr, "WIT not found: %s.wit\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeItemLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wit"] = base + ".wit"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + nlohmann::json je; + je["itemId"] = e.itemId; + je["displayId"] = e.displayId; + je["quality"] = e.quality; + je["qualityName"] = wowee::pipeline::WoweeItem::qualityName(e.quality); + je["itemClass"] = e.itemClass; + je["itemClassName"] = wowee::pipeline::WoweeItem::classNameOf(e.itemClass); + je["itemSubClass"] = e.itemSubClass; + je["inventoryType"] = e.inventoryType; + je["slotName"] = wowee::pipeline::WoweeItem::slotName(e.inventoryType); + je["flags"] = e.flags; + je["requiredLevel"] = e.requiredLevel; + je["itemLevel"] = e.itemLevel; + je["sellPriceCopper"] = e.sellPriceCopper; + je["buyPriceCopper"] = e.buyPriceCopper; + je["maxStack"] = e.maxStack; + je["durability"] = e.durability; + je["damageMin"] = e.damageMin; + je["damageMax"] = e.damageMax; + je["attackSpeedMs"] = e.attackSpeedMs; + nlohmann::json sa = nlohmann::json::array(); + for (const auto& s : e.stats) { + sa.push_back({ + {"type", s.type}, + {"typeName", wowee::pipeline::WoweeItem::statName(s.type)}, + {"value", s.value} + }); + } + je["stats"] = sa; + je["name"] = e.name; + je["description"] = e.description; + arr.push_back(je); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WIT: %s.wit\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" entries : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id ilvl quality class slot buy name\n"); + for (const auto& e : c.entries) { + std::printf(" %4u %4u %-9s %-11s %-10s ", + e.itemId, e.itemLevel, + wowee::pipeline::WoweeItem::qualityName(e.quality), + wowee::pipeline::WoweeItem::classNameOf(e.itemClass), + wowee::pipeline::WoweeItem::slotName(e.inventoryType)); + printPriceCopper(e.buyPriceCopper); + std::printf(" %s\n", e.name.c_str()); + } + return 0; +} + +int handleValidate(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWitExt(base); + if (!wowee::pipeline::WoweeItemLoader::exists(base)) { + std::fprintf(stderr, + "validate-wit: WIT not found: %s.wit\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeItemLoader::load(base); + std::vector errors; + std::vector warnings; + if (c.entries.empty()) { + warnings.push_back("catalog has zero entries"); + } + std::vector idsSeen; + idsSeen.reserve(c.entries.size()); + for (size_t k = 0; k < c.entries.size(); ++k) { + const auto& e = c.entries[k]; + std::string ctx = "entry " + std::to_string(k) + + " (id=" + std::to_string(e.itemId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.itemId == 0) { + errors.push_back(ctx + ": itemId is 0"); + } + if (e.quality > wowee::pipeline::WoweeItem::Heirloom) { + errors.push_back(ctx + ": quality " + + std::to_string(e.quality) + " not in 0..7"); + } + // Weapon class implies damage fields > 0 and a 1H/2H slot. + if (e.itemClass == wowee::pipeline::WoweeItem::Weapon) { + if (e.damageMin == 0 || e.damageMax == 0) { + errors.push_back(ctx + ": weapon has zero damage"); + } + if (e.damageMin > e.damageMax) { + errors.push_back(ctx + ": damageMin > damageMax"); + } + if (e.attackSpeedMs == 0) { + errors.push_back(ctx + ": weapon has zero attackSpeedMs"); + } + if (e.inventoryType != wowee::pipeline::WoweeItem::Weapon1H && + e.inventoryType != wowee::pipeline::WoweeItem::Weapon2H && + e.inventoryType != wowee::pipeline::WoweeItem::Ranged) { + warnings.push_back(ctx + ": weapon has non-weapon inventoryType"); + } + } + // Equippable items should have non-zero durability (catches + // common armor authoring oversight). + if (e.inventoryType != wowee::pipeline::WoweeItem::NonEquip && + e.durability == 0) { + warnings.push_back(ctx + + ": equippable item with durability=0"); + } + // Stack-of-one items shouldn't have maxStack > 1 + // (unique-equip case is already guarded by the Unique flag). + if (e.inventoryType != wowee::pipeline::WoweeItem::NonEquip && + e.maxStack > 1) { + warnings.push_back(ctx + + ": equippable item with maxStack > 1"); + } + // Buy price should be greater than sell price (vendor margin). + if (e.buyPriceCopper > 0 && e.sellPriceCopper > 0 && + e.sellPriceCopper >= e.buyPriceCopper) { + warnings.push_back(ctx + + ": sellPrice >= buyPrice (vendor would lose money)"); + } + for (uint32_t prev : idsSeen) { + if (prev == e.itemId) { + errors.push_back(ctx + ": duplicate itemId"); + break; + } + } + idsSeen.push_back(e.itemId); + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wit"] = base + ".wit"; + j["ok"] = ok; + j["errors"] = errors; + j["warnings"] = warnings; + std::printf("%s\n", j.dump(2).c_str()); + return ok ? 0 : 1; + } + std::printf("validate-wit: %s.wit\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu items, all itemIds unique\n", + c.entries.size()); + return 0; + } + if (!warnings.empty()) { + std::printf(" warnings (%zu):\n", warnings.size()); + for (const auto& w : warnings) + std::printf(" - %s\n", w.c_str()); + } + if (!errors.empty()) { + std::printf(" ERRORS (%zu):\n", errors.size()); + for (const auto& e : errors) + std::printf(" - %s\n", e.c_str()); + } + return ok ? 0 : 1; +} + +} // namespace + +bool handleItemsCatalog(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--gen-items") == 0 && i + 1 < argc) { + outRc = handleGenStarter(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-items-weapons") == 0 && i + 1 < argc) { + outRc = handleGenWeapons(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-items-armor") == 0 && i + 1 < argc) { + outRc = handleGenArmor(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wit") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wit") == 0 && i + 1 < argc) { + outRc = handleValidate(i, argc, argv); return true; + } + return false; +} + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_items_catalog.hpp b/tools/editor/cli_items_catalog.hpp new file mode 100644 index 00000000..f562cf2b --- /dev/null +++ b/tools/editor/cli_items_catalog.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleItemsCatalog(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee