feat(pipeline): add WIT (Wowee Item Template) format

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.
This commit is contained in:
Kelsi 2026-05-09 15:04:48 -07:00
parent 23f262c655
commit 9093975bdd
8 changed files with 870 additions and 0 deletions

View file

@ -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

View file

@ -0,0 +1,185 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
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<Stat> stats; // up to 255 (uint8 count)
std::string name;
std::string description;
};
std::string name;
std::vector<Entry> 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

View file

@ -0,0 +1,378 @@
#include "pipeline/wowee_items.hpp"
#include <cstdio>
#include <cstring>
#include <fstream>
namespace wowee {
namespace pipeline {
namespace {
constexpr char kMagic[4] = {'W', 'I', 'T', 'M'};
constexpr uint32_t kVersion = 1;
template <typename T>
void writePOD(std::ofstream& os, const T& v) {
os.write(reinterpret_cast<const char*>(&v), sizeof(T));
}
template <typename T>
bool readPOD(std::ifstream& is, T& v) {
is.read(reinterpret_cast<char*>(&v), sizeof(T));
return is.gcount() == static_cast<std::streamsize>(sizeof(T));
}
void writeStr(std::ofstream& os, const std::string& s) {
uint32_t n = static_cast<uint32_t>(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<std::streamsize>(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<uint32_t>(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<uint8_t>(
e.stats.size() > 255 ? 255 : e.stats.size());
writePOD(os, statCount);
uint8_t pad[3] = {0, 0, 0};
os.write(reinterpret_cast<const char*>(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<char*>(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<uint16_t>(40 + ilvl);
e.damageMin = dmgMin; e.damageMax = dmgMax;
e.attackSpeedMs = speedMs;
e.stats.push_back({WoweeItem::StatStrength,
static_cast<int16_t>(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<uint16_t>(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<int16_t>(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

View file

@ -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",

View file

@ -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,

View file

@ -865,6 +865,16 @@ void printUsage(const char* argv0) {
std::printf(" Export binary .wspn to a human-editable JSON sidecar (defaults to <base>.wspn.json)\n");
std::printf(" --import-wspn-json <json-path> [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 <wit-base> [name]\n");
std::printf(" Emit .wit starter item catalog: 1 weapon + 1 chest + 1 potion + 1 quest item\n");
std::printf(" --gen-items-weapons <wit-base> [name]\n");
std::printf(" Emit .wit weapon catalog: 5 entries spanning common -> legendary, both 1H and 2H\n");
std::printf(" --gen-items-armor <wit-base> [name]\n");
std::printf(" Emit .wit full mail-armor set: head + chest + legs + feet + hands + cloak (BoE)\n");
std::printf(" --info-wit <wit-base> [--json]\n");
std::printf(" Print WIT item entries (id / ilvl / quality / class / slot / buy price / name)\n");
std::printf(" --validate-wit <wit-base> [--json]\n");
std::printf(" Static checks: itemId>0 + unique, weapon damage>0 + min<=max, equippable durability>0, sell<buy\n");
std::printf(" --gen-weather-temperate <wow-base> [zoneName]\n");
std::printf(" Emit .wow weather schedule: clear-dominant + occasional rain + fog (forest / grassland)\n");
std::printf(" --gen-weather-arctic <wow-base> [zoneName]\n");

View file

@ -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 <nlohmann/json.hpp>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
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<std::string> errors;
std::vector<std::string> warnings;
if (c.entries.empty()) {
warnings.push_back("catalog has zero entries");
}
std::vector<uint32_t> 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

View file

@ -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