From b220eeba6195e70243b342e66e478260c66ea542 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 22:14:44 -0700 Subject: [PATCH] feat(editor): add WMAT (Item Material) open catalog format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Open replacement for Blizzard's Material.dbc plus the Material and SheatheType fields in ItemDisplayInfo.dbc. Defines the material categorization that items reference (Cloth / Leather / Mail / Plate / Wood / Steel / Crystal / Ethereal / etc), each with its own foley sound (played on item use), impact sound (played on drop / hit), weight category, and material-property flags (IsBreakable / IsMagical / IsFlammable / IsConductive / IsHolyCharged / IsCursed). The engine plays a sword's metallic clang from impactSoundId when it hits a stone wall, but a cloth tabard makes no such sound — the difference is exactly the material assigned by this catalog. Every armor and weapon item in WIT references a materialId here. Twelve materialKind values cover the standard armor classes (Cloth/Leather/Mail/Plate/Hide), structural materials (Wood / Stone / Metal), and special categories (Liquid / Organic / Crystal / Ethereal). Three weight tiers (Light / Medium / Heavy) control encumbrance UI hints. Cross-references back to WSND (foleySoundId / impactSoundId reference WSND sound entries) and forward to WIT (item entries reference materialId here). Three preset emitters: --gen-mat (5 armor materials matching WoW's armor classes), --gen-mat-weapon (5 weapon materials from breakable+flammable Wood through enchanted endgame steel), --gen-mat-magical (4 magical materials with special flags including the IsHolyCharged anti-undead property). Validation enforces id+name presence, materialKind 0..11, weightCategory 0..2, no duplicate ids; warns on: - IsHolyCharged + IsCursed both set (engine picks one, typically IsCursed wins) - Plate kind that's not Heavy weight (canonical violation) - Cloth kind that's not Light weight (canonical violation) Wired through the cross-format table; WMAT appears automatically in all 11 cross-format utilities. Format count 75 -> 76; CLI flag count 943 -> 948. --- CMakeLists.txt | 3 + include/pipeline/wowee_item_materials.hpp | 130 +++++++++ src/pipeline/wowee_item_materials.cpp | 277 ++++++++++++++++++++ tools/editor/cli_arg_required.cpp | 2 + tools/editor/cli_dispatch.cpp | 2 + tools/editor/cli_format_table.cpp | 1 + tools/editor/cli_help.cpp | 10 + tools/editor/cli_item_materials_catalog.cpp | 277 ++++++++++++++++++++ tools/editor/cli_item_materials_catalog.hpp | 12 + tools/editor/cli_list_formats.cpp | 1 + 10 files changed, 715 insertions(+) create mode 100644 include/pipeline/wowee_item_materials.hpp create mode 100644 src/pipeline/wowee_item_materials.cpp create mode 100644 tools/editor/cli_item_materials_catalog.cpp create mode 100644 tools/editor/cli_item_materials_catalog.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 85a7ae5c..685f9e29 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -664,6 +664,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_spell_power_costs.cpp src/pipeline/wowee_glyph_slots.cpp src/pipeline/wowee_creature_difficulties.cpp + src/pipeline/wowee_item_materials.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1485,6 +1486,7 @@ add_executable(wowee_editor tools/editor/cli_spell_power_costs_catalog.cpp tools/editor/cli_glyph_slots_catalog.cpp tools/editor/cli_creature_difficulties_catalog.cpp + tools/editor/cli_item_materials_catalog.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp @@ -1627,6 +1629,7 @@ add_executable(wowee_editor src/pipeline/wowee_spell_power_costs.cpp src/pipeline/wowee_glyph_slots.cpp src/pipeline/wowee_creature_difficulties.cpp + src/pipeline/wowee_item_materials.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_item_materials.hpp b/include/pipeline/wowee_item_materials.hpp new file mode 100644 index 00000000..0028a304 --- /dev/null +++ b/include/pipeline/wowee_item_materials.hpp @@ -0,0 +1,130 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Item Material catalog (.wmat) — novel +// replacement for Blizzard's Material.dbc plus the +// Material/SheatheType fields in ItemDisplayInfo.dbc. +// Defines the material categorization that items +// reference (Cloth / Leather / Mail / Plate / Wood / +// Steel / Crystal / etc), each with its own foley sound +// (played on use), impact sound (played on drop / hit), +// weight category, and material-property flags +// (IsBreakable / IsMagical / IsFlammable / IsConductive). +// +// The engine plays a sword's metallic clang from +// impactSoundId when it hits a stone wall, but a cloth +// tabard makes no such sound — the difference is exactly +// the material assigned by this catalog. Every armor and +// weapon item in WIT references a materialId here. +// +// Cross-references with previously-added formats: +// WSND: foleySoundId / impactSoundId reference WSND +// sound entries. +// WIT: item entries reference materialId here. +// +// Binary layout (little-endian): +// magic[4] = "WMAT" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// materialId (uint32) +// nameLen + name +// descLen + description +// materialKind (uint8) / weightCategory (uint8) / pad[2] +// foleySoundId (uint32) +// impactSoundId (uint32) +// materialFlags (uint32) +// iconColorRGBA (uint32) +struct WoweeItemMaterial { + enum MaterialKind : uint8_t { + Cloth = 0, + Leather = 1, + Mail = 2, + Plate = 3, + Wood = 4, + Stone = 5, + Metal = 6, + Liquid = 7, + Organic = 8, // bone / hide / chitin + Crystal = 9, + Ethereal = 10, // ghostly / energy / soul + Hide = 11, // raw furred hide (distinct from Leather) + }; + + enum WeightCategory : uint8_t { + Light = 0, + Medium = 1, + Heavy = 2, + }; + + enum MaterialFlag : uint32_t { + IsBreakable = 1u << 0, // can be shattered by enough damage + IsMagical = 1u << 1, // glows / has magical properties + IsFlammable = 1u << 2, // ignites on fire damage + IsConductive = 1u << 3, // amplifies lightning damage + IsHolyCharged = 1u << 4, // damages undead on contact + IsCursed = 1u << 5, // applies a debuff when equipped + }; + + struct Entry { + uint32_t materialId = 0; + std::string name; + std::string description; + uint8_t materialKind = Cloth; + uint8_t weightCategory = Light; + uint8_t pad0 = 0; + uint8_t pad1 = 0; + uint32_t foleySoundId = 0; + uint32_t impactSoundId = 0; + uint32_t materialFlags = 0; + uint32_t iconColorRGBA = 0xFFFFFFFFu; + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t materialId) const; + + static const char* materialKindName(uint8_t k); + static const char* weightCategoryName(uint8_t w); +}; + +class WoweeItemMaterialLoader { +public: + static bool save(const WoweeItemMaterial& cat, + const std::string& basePath); + static WoweeItemMaterial load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-mat* variants. + // + // makeArmor — 5 armor-class materials (Cloth / + // Leather / Mail / Plate / Hide) + // with weight categories matching + // WoW's armor classes. + // makeWeapon — 5 weapon materials (Wood / Steel / + // Mithril / Adamantite / + // EnchantedSteel) covering the + // vendor-buy through endgame + // progression. + // makeMagical — 4 magical materials (Crystal / + // Ethereal / Cursed / HolyForged) + // carrying special flags + // (IsMagical, IsCursed, + // IsHolyCharged). + static WoweeItemMaterial makeArmor(const std::string& catalogName); + static WoweeItemMaterial makeWeapon(const std::string& catalogName); + static WoweeItemMaterial makeMagical(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_item_materials.cpp b/src/pipeline/wowee_item_materials.cpp new file mode 100644 index 00000000..bc08f8d9 --- /dev/null +++ b/src/pipeline/wowee_item_materials.cpp @@ -0,0 +1,277 @@ +#include "pipeline/wowee_item_materials.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'M', 'A', 'T'}; +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; + 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() < 5 || base.substr(base.size() - 5) != ".wmat") { + base += ".wmat"; + } + return base; +} + +uint32_t packRgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a = 0xFF) { + return (static_cast(a) << 24) | + (static_cast(b) << 16) | + (static_cast(g) << 8) | + static_cast(r); +} + +} // namespace + +const WoweeItemMaterial::Entry* +WoweeItemMaterial::findById(uint32_t materialId) const { + for (const auto& e : entries) + if (e.materialId == materialId) return &e; + return nullptr; +} + +const char* WoweeItemMaterial::materialKindName(uint8_t k) { + switch (k) { + case Cloth: return "cloth"; + case Leather: return "leather"; + case Mail: return "mail"; + case Plate: return "plate"; + case Wood: return "wood"; + case Stone: return "stone"; + case Metal: return "metal"; + case Liquid: return "liquid"; + case Organic: return "organic"; + case Crystal: return "crystal"; + case Ethereal: return "ethereal"; + case Hide: return "hide"; + default: return "unknown"; + } +} + +const char* WoweeItemMaterial::weightCategoryName(uint8_t w) { + switch (w) { + case Light: return "light"; + case Medium: return "medium"; + case Heavy: return "heavy"; + default: return "unknown"; + } +} + +bool WoweeItemMaterialLoader::save(const WoweeItemMaterial& 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.materialId); + writeStr(os, e.name); + writeStr(os, e.description); + writePOD(os, e.materialKind); + writePOD(os, e.weightCategory); + writePOD(os, e.pad0); + writePOD(os, e.pad1); + writePOD(os, e.foleySoundId); + writePOD(os, e.impactSoundId); + writePOD(os, e.materialFlags); + writePOD(os, e.iconColorRGBA); + } + return os.good(); +} + +WoweeItemMaterial WoweeItemMaterialLoader::load( + const std::string& basePath) { + WoweeItemMaterial 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.materialId)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.name) || !readStr(is, e.description)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.materialKind) || + !readPOD(is, e.weightCategory) || + !readPOD(is, e.pad0) || + !readPOD(is, e.pad1) || + !readPOD(is, e.foleySoundId) || + !readPOD(is, e.impactSoundId) || + !readPOD(is, e.materialFlags) || + !readPOD(is, e.iconColorRGBA)) { + out.entries.clear(); return out; + } + } + return out; +} + +bool WoweeItemMaterialLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeItemMaterial WoweeItemMaterialLoader::makeArmor( + const std::string& catalogName) { + using M = WoweeItemMaterial; + WoweeItemMaterial c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint8_t kind, + uint8_t weight, uint32_t foley, uint32_t impact, + uint32_t flags, uint8_t r, uint8_t g, uint8_t b, + const char* desc) { + M::Entry e; + e.materialId = id; e.name = name; e.description = desc; + e.materialKind = kind; + e.weightCategory = weight; + e.foleySoundId = foley; + e.impactSoundId = impact; + e.materialFlags = flags; + e.iconColorRGBA = packRgba(r, g, b); + c.entries.push_back(e); + }; + // foley/impact sound ids reference WSND entries; using + // 1xx range for foley + 2xx for impact as illustrative + // anchors that engine projects override. + add(1, "Cloth", M::Cloth, M::Light, 101, 201, + M::IsFlammable, + 220, 220, 200, "Cloth — light, flammable, no impact sound."); + add(2, "Leather", M::Leather, M::Light, 102, 202, 0, + 160, 100, 60, + "Leather — light, supple, dull thud on impact."); + add(3, "Mail", M::Mail, M::Medium, 103, 203, + M::IsConductive, + 180, 180, 200, "Mail — medium, metallic ring, conducts lightning."); + add(4, "Plate", M::Plate, M::Heavy, 104, 204, + M::IsConductive, + 220, 220, 230, + "Plate — heavy, loud clang, conducts lightning."); + add(5, "Hide", M::Hide, M::Medium, 105, 205, + M::IsFlammable, + 140, 90, 50, + "Hide — raw furred hide, medium weight, flammable."); + return c; +} + +WoweeItemMaterial WoweeItemMaterialLoader::makeWeapon( + const std::string& catalogName) { + using M = WoweeItemMaterial; + WoweeItemMaterial c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint8_t kind, + uint8_t weight, uint32_t foley, uint32_t impact, + uint32_t flags, const char* desc) { + M::Entry e; + e.materialId = id; e.name = name; e.description = desc; + e.materialKind = kind; + e.weightCategory = weight; + e.foleySoundId = foley; + e.impactSoundId = impact; + e.materialFlags = flags; + e.iconColorRGBA = packRgba(180, 180, 200); // weapon-steel grey + c.entries.push_back(e); + }; + add(100, "Wood", M::Wood, M::Light, 110, 210, + M::IsBreakable | M::IsFlammable, + "Wood — staves and bows. Breakable + flammable."); + add(101, "Steel", M::Metal, M::Medium, 111, 211, + M::IsConductive, + "Steel — vendor-buy weapons. Conducts lightning."); + add(102, "Mithril", M::Metal, M::Medium, 112, 212, + M::IsConductive, + "Mithril — mid-tier weapons (40-50). Lighter than steel."); + add(103, "Adamantite", M::Metal, M::Medium, 113, 213, + M::IsConductive, + "Adamantite — endgame raw material (TBC-era). Tough metal."); + add(104, "EnchantedSteel", M::Metal, M::Medium, 114, 214, + M::IsConductive | M::IsMagical, + "Enchanted steel — magical raid weapons. Glows + conducts."); + return c; +} + +WoweeItemMaterial WoweeItemMaterialLoader::makeMagical( + const std::string& catalogName) { + using M = WoweeItemMaterial; + WoweeItemMaterial c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint8_t kind, + uint32_t flags, uint8_t r, uint8_t g, uint8_t b, + const char* desc) { + M::Entry e; + e.materialId = id; e.name = name; e.description = desc; + e.materialKind = kind; + e.weightCategory = M::Light; // magical things are weightless + e.materialFlags = flags; + e.iconColorRGBA = packRgba(r, g, b); + c.entries.push_back(e); + }; + add(200, "Crystal", M::Crystal, + M::IsMagical | M::IsBreakable, + 180, 220, 240, + "Crystal — magical, breakable, refracts light."); + add(201, "Ethereal", M::Ethereal, + M::IsMagical, + 200, 200, 240, + "Ethereal — ghostly weightless material."); + add(202, "CursedBone", M::Organic, + M::IsCursed, + 100, 60, 60, + "Cursed bone — applies a debuff to wearer."); + add(203, "HolyForged", M::Metal, + M::IsMagical | M::IsHolyCharged, + 240, 240, 200, + "Holy-forged steel — damages undead on contact."); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 266012c9..d81df0b5 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -231,6 +231,8 @@ const char* const kArgRequired[] = { "--gen-cdf", "--gen-cdf-wotlk-raid", "--gen-cdf-fiveman", "--info-wcdf", "--validate-wcdf", "--export-wcdf-json", "--import-wcdf-json", + "--gen-mat", "--gen-mat-weapon", "--gen-mat-magical", + "--info-wmat", "--validate-wmat", "--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 5fa007d6..267cc52f 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -114,6 +114,7 @@ #include "cli_spell_power_costs_catalog.hpp" #include "cli_glyph_slots_catalog.hpp" #include "cli_creature_difficulties_catalog.hpp" +#include "cli_item_materials_catalog.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -269,6 +270,7 @@ constexpr DispatchFn kDispatchTable[] = { handleSpellPowerCostsCatalog, handleGlyphSlotsCatalog, handleCreatureDifficultiesCatalog, + handleItemMaterialsCatalog, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_format_table.cpp b/tools/editor/cli_format_table.cpp index 4d45e884..5eae2eea 100644 --- a/tools/editor/cli_format_table.cpp +++ b/tools/editor/cli_format_table.cpp @@ -78,6 +78,7 @@ constexpr FormatMagicEntry kFormats[] = { {{'W','S','P','C'}, ".wspc", "spells", "--info-wspc", "Spell power cost bucket catalog"}, {{'W','G','F','S'}, ".wgfs", "glyphs", "--info-wgfs", "Glyph slot layout catalog"}, {{'W','C','D','F'}, ".wcdf", "creatures", "--info-wcdf", "Creature difficulty variant catalog"}, + {{'W','M','A','T'}, ".wmat", "items", "--info-wmat", "Item material catalog"}, {{'W','F','A','C'}, ".wfac", "factions", nullptr, "Faction catalog"}, {{'W','L','C','K'}, ".wlck", "locks", nullptr, "Lock catalog"}, {{'W','S','K','L'}, ".wskl", "skills", nullptr, "Skill catalog"}, diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index 9c24f00c..f0362ee1 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1803,6 +1803,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wcdf to a human-editable JSON sidecar (defaults to .wcdf.json)\n"); std::printf(" --import-wcdf-json [out-base]\n"); std::printf(" Import a .wcdf.json sidecar back into binary .wcdf (accepts spawnGroupKind int OR spawnGroupKindName string)\n"); + std::printf(" --gen-mat [name]\n"); + std::printf(" Emit .wmat 5 armor materials (Cloth/Leather/Mail/Plate/Hide) with weight categories matching WoW's armor classes\n"); + std::printf(" --gen-mat-weapon [name]\n"); + std::printf(" Emit .wmat 5 weapon materials (Wood breakable+flammable / Steel / Mithril / Adamantite / EnchantedSteel magical)\n"); + std::printf(" --gen-mat-magical [name]\n"); + std::printf(" Emit .wmat 4 magical materials (Crystal magical+breakable / Ethereal / CursedBone / HolyForged charged)\n"); + std::printf(" --info-wmat [--json]\n"); + std::printf(" Print WMAT entries (id / kind / weight / foley/impact sound ids / material flags / name) — flags decoded as label list\n"); + std::printf(" --validate-wmat [--json]\n"); + std::printf(" Static checks: id+name required, materialKind 0..11, weightCategory 0..2, no duplicate ids; warns on Holy+Cursed combo, Plate non-heavy, Cloth non-light\n"); std::printf(" --gen-weather-temperate [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_item_materials_catalog.cpp b/tools/editor/cli_item_materials_catalog.cpp new file mode 100644 index 00000000..9c8849c3 --- /dev/null +++ b/tools/editor/cli_item_materials_catalog.cpp @@ -0,0 +1,277 @@ +#include "cli_item_materials_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_item_materials.hpp" +#include + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWmatExt(std::string base) { + stripExt(base, ".wmat"); + return base; +} + +bool saveOrError(const wowee::pipeline::WoweeItemMaterial& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeItemMaterialLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wmat\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeItemMaterial& c, + const std::string& base) { + std::printf("Wrote %s.wmat\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" materials : %zu\n", c.entries.size()); +} + +int handleGenArmor(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "ArmorMaterials"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWmatExt(base); + auto c = wowee::pipeline::WoweeItemMaterialLoader::makeArmor(name); + if (!saveOrError(c, base, "gen-mat")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenWeapon(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "WeaponMaterials"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWmatExt(base); + auto c = wowee::pipeline::WoweeItemMaterialLoader::makeWeapon(name); + if (!saveOrError(c, base, "gen-mat-weapon")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenMagical(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "MagicalMaterials"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWmatExt(base); + auto c = wowee::pipeline::WoweeItemMaterialLoader::makeMagical(name); + if (!saveOrError(c, base, "gen-mat-magical")) return 1; + printGenSummary(c, base); + return 0; +} + +void appendMaterialFlagNames(uint32_t flags, std::string& out) { + using F = wowee::pipeline::WoweeItemMaterial; + auto add = [&](const char* n) { + if (!out.empty()) out += "|"; + out += n; + }; + if (flags & F::IsBreakable) add("IsBreakable"); + if (flags & F::IsMagical) add("IsMagical"); + if (flags & F::IsFlammable) add("IsFlammable"); + if (flags & F::IsConductive) add("IsConductive"); + if (flags & F::IsHolyCharged) add("IsHolyCharged"); + if (flags & F::IsCursed) add("IsCursed"); + if (out.empty()) out = "-"; +} + +int handleInfo(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWmatExt(base); + if (!wowee::pipeline::WoweeItemMaterialLoader::exists(base)) { + std::fprintf(stderr, "WMAT not found: %s.wmat\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeItemMaterialLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wmat"] = base + ".wmat"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + std::string flagNames; + appendMaterialFlagNames(e.materialFlags, flagNames); + arr.push_back({ + {"materialId", e.materialId}, + {"name", e.name}, + {"description", e.description}, + {"materialKind", e.materialKind}, + {"materialKindName", wowee::pipeline::WoweeItemMaterial::materialKindName(e.materialKind)}, + {"weightCategory", e.weightCategory}, + {"weightCategoryName", wowee::pipeline::WoweeItemMaterial::weightCategoryName(e.weightCategory)}, + {"foleySoundId", e.foleySoundId}, + {"impactSoundId", e.impactSoundId}, + {"materialFlags", e.materialFlags}, + {"materialFlagsLabels", flagNames}, + {"iconColorRGBA", e.iconColorRGBA}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WMAT: %s.wmat\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" materials : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id kind weight foley impact flags name\n"); + for (const auto& e : c.entries) { + std::string flagNames; + appendMaterialFlagNames(e.materialFlags, flagNames); + std::printf(" %4u %-9s %-6s %5u %5u %-30s %s\n", + e.materialId, + wowee::pipeline::WoweeItemMaterial::materialKindName(e.materialKind), + wowee::pipeline::WoweeItemMaterial::weightCategoryName(e.weightCategory), + e.foleySoundId, e.impactSoundId, + flagNames.c_str(), + 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 = stripWmatExt(base); + if (!wowee::pipeline::WoweeItemMaterialLoader::exists(base)) { + std::fprintf(stderr, + "validate-wmat: WMAT not found: %s.wmat\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeItemMaterialLoader::load(base); + std::vector errors; + std::vector warnings; + if (c.entries.empty()) { + warnings.push_back("catalog has zero entries"); + } + std::vector idsSeen; + constexpr uint32_t kKnownFlagMask = + wowee::pipeline::WoweeItemMaterial::IsBreakable | + wowee::pipeline::WoweeItemMaterial::IsMagical | + wowee::pipeline::WoweeItemMaterial::IsFlammable | + wowee::pipeline::WoweeItemMaterial::IsConductive | + wowee::pipeline::WoweeItemMaterial::IsHolyCharged | + wowee::pipeline::WoweeItemMaterial::IsCursed; + 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.materialId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.materialId == 0) + errors.push_back(ctx + ": materialId is 0"); + if (e.name.empty()) + errors.push_back(ctx + ": name is empty"); + if (e.materialKind > wowee::pipeline::WoweeItemMaterial::Hide) { + errors.push_back(ctx + ": materialKind " + + std::to_string(e.materialKind) + " not in 0..11"); + } + if (e.weightCategory > wowee::pipeline::WoweeItemMaterial::Heavy) { + errors.push_back(ctx + ": weightCategory " + + std::to_string(e.weightCategory) + " not in 0..2"); + } + if (e.materialFlags & ~kKnownFlagMask) { + warnings.push_back(ctx + + ": materialFlags has bits outside known mask " + + "(0x" + std::to_string(e.materialFlags & ~kKnownFlagMask) + + ") — engine will ignore unknown flags"); + } + // Contradictory flag combos. + if ((e.materialFlags & wowee::pipeline::WoweeItemMaterial::IsHolyCharged) && + (e.materialFlags & wowee::pipeline::WoweeItemMaterial::IsCursed)) { + warnings.push_back(ctx + + ": both IsHolyCharged and IsCursed flags set — " + "engine will pick one (typically IsCursed wins)"); + } + // Plate kind is canonically heavy. Cloth is light. + if (e.materialKind == wowee::pipeline::WoweeItemMaterial::Plate && + e.weightCategory != wowee::pipeline::WoweeItemMaterial::Heavy) { + warnings.push_back(ctx + + ": Plate kind with weightCategory=" + + wowee::pipeline::WoweeItemMaterial::weightCategoryName(e.weightCategory) + + " — plate armor is canonically heavy"); + } + if (e.materialKind == wowee::pipeline::WoweeItemMaterial::Cloth && + e.weightCategory != wowee::pipeline::WoweeItemMaterial::Light) { + warnings.push_back(ctx + + ": Cloth kind with weightCategory=" + + wowee::pipeline::WoweeItemMaterial::weightCategoryName(e.weightCategory) + + " — cloth is canonically light"); + } + for (uint32_t prev : idsSeen) { + if (prev == e.materialId) { + errors.push_back(ctx + ": duplicate materialId"); + break; + } + } + idsSeen.push_back(e.materialId); + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wmat"] = base + ".wmat"; + 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-wmat: %s.wmat\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu materials, all materialIds 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 handleItemMaterialsCatalog(int& i, int argc, char** argv, + int& outRc) { + if (std::strcmp(argv[i], "--gen-mat") == 0 && i + 1 < argc) { + outRc = handleGenArmor(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-mat-weapon") == 0 && i + 1 < argc) { + outRc = handleGenWeapon(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-mat-magical") == 0 && i + 1 < argc) { + outRc = handleGenMagical(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wmat") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wmat") == 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_item_materials_catalog.hpp b/tools/editor/cli_item_materials_catalog.hpp new file mode 100644 index 00000000..71eb495c --- /dev/null +++ b/tools/editor/cli_item_materials_catalog.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleItemMaterialsCatalog(int& i, int argc, char** argv, + int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_list_formats.cpp b/tools/editor/cli_list_formats.cpp index 7426b892..8db75c0b 100644 --- a/tools/editor/cli_list_formats.cpp +++ b/tools/editor/cli_list_formats.cpp @@ -100,6 +100,7 @@ constexpr FormatRow kFormats[] = { {"WSPC", ".wspc", "spells", "Spell.dbc power-cost fields", "Spell power cost bucket catalog"}, {"WGFS", ".wgfs", "glyphs", "GlyphSlot.dbc", "Glyph slot layout catalog"}, {"WCDF", ".wcdf", "creatures", "CreatureDifficulty.dbc", "Creature difficulty variant catalog"}, + {"WMAT", ".wmat", "items", "Material.dbc + ItemDisplayInfo", "Item material catalog"}, // Additional pipeline catalogs without the alternating // gen/info/validate CLI surface (loaded by the engine