diff --git a/CMakeLists.txt b/CMakeLists.txt index 4d21cd4e..83150d1f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -603,6 +603,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_loot.cpp src/pipeline/wowee_creatures.cpp src/pipeline/wowee_quests.cpp + src/pipeline/wowee_objects.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1352,6 +1353,7 @@ add_executable(wowee_editor tools/editor/cli_loot_catalog.cpp tools/editor/cli_creatures_catalog.cpp tools/editor/cli_quests_catalog.cpp + tools/editor/cli_objects_catalog.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp @@ -1433,6 +1435,7 @@ add_executable(wowee_editor src/pipeline/wowee_loot.cpp src/pipeline/wowee_creatures.cpp src/pipeline/wowee_quests.cpp + src/pipeline/wowee_objects.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_objects.hpp b/include/pipeline/wowee_objects.hpp new file mode 100644 index 00000000..1b9f9df9 --- /dev/null +++ b/include/pipeline/wowee_objects.hpp @@ -0,0 +1,126 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Game Object Template (.wgot) — novel replacement +// for AzerothCore-style gameobject_template SQL tables PLUS +// the Blizzard GameObjectDisplayInfo.dbc / GameObject types +// metadata. The 16th open format added to the editor. +// +// Game objects are the non-creature interactable scenery: +// chests (with loot), doors, buttons, mailboxes, herb / ore +// gathering nodes, fishing pools, signposts, mounts. Each +// has a displayId for the model, a typeId driving its +// interaction logic, and optional cross-references to a lock +// (future WLCK) and loot table (existing WLOT). +// +// Cross-references with previously-added formats: +// WSPN.entry.entryId (kind=GameObject) → WGOT.entry.objectId +// WGOT.entry.lootTableId → WLOT.entry.creatureId +// (loot tables are +// universal — game +// objects + creatures +// both key by ID) +// +// Binary layout (little-endian): +// magic[4] = "WGOT" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// objectId (uint32) +// displayId (uint32) +// nameLen + name +// typeId (uint8) + pad[3] +// size (float) +// castBarLen + castBarCaption -- e.g. "Mining" +// requiredSkill (uint32) -- 0 = none, else SkillLine ID +// requiredSkillValue (uint32) +// lockId (uint32) -- 0 = no lock +// lootTableId (uint32) -- 0 = no loot +// minOpenTimeMs (uint32) +// maxOpenTimeMs (uint32) +// flags (uint32) +struct WoweeGameObject { + enum TypeId : uint8_t { + Door = 0, + Button = 1, + Chest = 2, + Container = 3, + QuestGiver = 4, + Text = 5, + Trap = 6, + Goober = 7, // generic activatable script object + Transport = 8, + Mailbox = 9, + MineralNode = 10, + HerbNode = 11, + FishingNode = 12, + Mount = 13, + Sign = 14, + Bonfire = 15, + }; + + enum Flags : uint32_t { + Disabled = 0x01, + ScriptOnly = 0x02, // not interactable except by scripts + UsableFromMount = 0x04, + Despawn = 0x08, // disappears after first use + Frozen = 0x10, // never animates + QuestGated = 0x20, // visible only with matching quest + }; + + struct Entry { + uint32_t objectId = 0; + uint32_t displayId = 0; + std::string name; + uint8_t typeId = Goober; + float size = 1.0f; + std::string castBarCaption; + uint32_t requiredSkill = 0; + uint32_t requiredSkillValue = 0; + uint32_t lockId = 0; + uint32_t lootTableId = 0; + uint32_t minOpenTimeMs = 0; + uint32_t maxOpenTimeMs = 0; + uint32_t flags = 0; + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + // Lookup by objectId — nullptr if not present. + const Entry* findById(uint32_t objectId) const; + + static const char* typeName(uint8_t t); +}; + +class WoweeGameObjectLoader { +public: + static bool save(const WoweeGameObject& cat, + const std::string& basePath); + static WoweeGameObject load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-objects* variants. + // + // makeStarter — 1 chest + 1 mailbox + 1 sign. + // makeDungeon — door + button + 2 chests (regular + boss + // loot) + trap. The bandit chest in slot 2000 + // matches WLOT.makeBandit. + // makeGather — gathering nodes: 1 herb (Peacebloom), + // 1 ore (Tin Vein), 1 fishing pool. + static WoweeGameObject makeStarter(const std::string& catalogName); + static WoweeGameObject makeDungeon(const std::string& catalogName); + static WoweeGameObject makeGather(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_objects.cpp b/src/pipeline/wowee_objects.cpp new file mode 100644 index 00000000..453551c2 --- /dev/null +++ b/src/pipeline/wowee_objects.cpp @@ -0,0 +1,274 @@ +#include "pipeline/wowee_objects.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'G', 'O', '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) != ".wgot") { + base += ".wgot"; + } + return base; +} + +} // namespace + +const WoweeGameObject::Entry* WoweeGameObject::findById(uint32_t objectId) const { + for (const auto& e : entries) { + if (e.objectId == objectId) return &e; + } + return nullptr; +} + +const char* WoweeGameObject::typeName(uint8_t t) { + switch (t) { + case Door: return "door"; + case Button: return "button"; + case Chest: return "chest"; + case Container: return "container"; + case QuestGiver: return "quest-giver"; + case Text: return "text"; + case Trap: return "trap"; + case Goober: return "goober"; + case Transport: return "transport"; + case Mailbox: return "mailbox"; + case MineralNode: return "ore-node"; + case HerbNode: return "herb-node"; + case FishingNode: return "fishing-node"; + case Mount: return "mount"; + case Sign: return "sign"; + case Bonfire: return "bonfire"; + default: return "unknown"; + } +} + +bool WoweeGameObjectLoader::save(const WoweeGameObject& 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.objectId); + writePOD(os, e.displayId); + writeStr(os, e.name); + writePOD(os, e.typeId); + uint8_t pad[3] = {0, 0, 0}; + os.write(reinterpret_cast(pad), 3); + writePOD(os, e.size); + writeStr(os, e.castBarCaption); + writePOD(os, e.requiredSkill); + writePOD(os, e.requiredSkillValue); + writePOD(os, e.lockId); + writePOD(os, e.lootTableId); + writePOD(os, e.minOpenTimeMs); + writePOD(os, e.maxOpenTimeMs); + writePOD(os, e.flags); + } + return os.good(); +} + +WoweeGameObject WoweeGameObjectLoader::load(const std::string& basePath) { + WoweeGameObject 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.objectId) || + !readPOD(is, e.displayId)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.name)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.typeId)) { + out.entries.clear(); return out; + } + uint8_t pad[3]; + is.read(reinterpret_cast(pad), 3); + if (is.gcount() != 3) { out.entries.clear(); return out; } + if (!readPOD(is, e.size) || + !readStr(is, e.castBarCaption) || + !readPOD(is, e.requiredSkill) || + !readPOD(is, e.requiredSkillValue) || + !readPOD(is, e.lockId) || + !readPOD(is, e.lootTableId) || + !readPOD(is, e.minOpenTimeMs) || + !readPOD(is, e.maxOpenTimeMs) || + !readPOD(is, e.flags)) { + out.entries.clear(); return out; + } + } + return out; +} + +bool WoweeGameObjectLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeGameObject WoweeGameObjectLoader::makeStarter(const std::string& catalogName) { + WoweeGameObject c; + c.name = catalogName; + { + WoweeGameObject::Entry e; + e.objectId = 1; e.displayId = 100; + e.name = "Wooden Chest"; e.typeId = WoweeGameObject::Chest; + e.castBarCaption = "Opening..."; + e.lootTableId = 1; // matches WLOT.makeStarter creatureId + c.entries.push_back(e); + } + { + WoweeGameObject::Entry e; + e.objectId = 2; e.displayId = 110; + e.name = "Standard Mailbox"; e.typeId = WoweeGameObject::Mailbox; + c.entries.push_back(e); + } + { + WoweeGameObject::Entry e; + e.objectId = 3; e.displayId = 120; + e.name = "Roadside Sign"; e.typeId = WoweeGameObject::Sign; + c.entries.push_back(e); + } + return c; +} + +WoweeGameObject WoweeGameObjectLoader::makeDungeon(const std::string& catalogName) { + WoweeGameObject c; + c.name = catalogName; + { + WoweeGameObject::Entry e; + e.objectId = 1500; e.displayId = 130; + e.name = "Iron Door"; e.typeId = WoweeGameObject::Door; + e.lockId = 1; // requires key / lockpick (future WLCK) + e.flags = WoweeGameObject::Frozen; + c.entries.push_back(e); + } + { + WoweeGameObject::Entry e; + e.objectId = 1501; e.displayId = 131; + e.name = "Pressure Plate"; e.typeId = WoweeGameObject::Button; + e.minOpenTimeMs = 5000; e.maxOpenTimeMs = 10000; + c.entries.push_back(e); + } + { + WoweeGameObject::Entry e; + // objectId = 2000 deliberately matches WLOT.makeBandit + // creatureId-keyed loot table. + e.objectId = 2000; e.displayId = 140; + e.name = "Bandit Strongbox"; e.typeId = WoweeGameObject::Chest; + e.castBarCaption = "Opening..."; + e.lootTableId = 2000; // -> WLOT bandit chest table + e.lockId = 2; // light lockpick + c.entries.push_back(e); + } + { + WoweeGameObject::Entry e; + e.objectId = 1502; e.displayId = 141; + e.name = "Boss Treasure Chest"; e.typeId = WoweeGameObject::Chest; + e.castBarCaption = "Opening..."; + e.lootTableId = 9999; // -> WLOT boss table + e.size = 1.5f; // visibly bigger than regular + c.entries.push_back(e); + } + { + WoweeGameObject::Entry e; + e.objectId = 1503; e.displayId = 150; + e.name = "Spike Trap"; e.typeId = WoweeGameObject::Trap; + e.minOpenTimeMs = 1500; e.maxOpenTimeMs = 1500; + e.flags = WoweeGameObject::ScriptOnly; + c.entries.push_back(e); + } + return c; +} + +WoweeGameObject WoweeGameObjectLoader::makeGather(const std::string& catalogName) { + WoweeGameObject c; + c.name = catalogName; + { + WoweeGameObject::Entry e; + e.objectId = 3000; e.displayId = 170; + e.name = "Peacebloom"; e.typeId = WoweeGameObject::HerbNode; + e.castBarCaption = "Herbalism"; + e.requiredSkill = 182; // SkillLine herbalism (canonical id) + e.requiredSkillValue = 1; + e.flags = WoweeGameObject::Despawn; + c.entries.push_back(e); + } + { + WoweeGameObject::Entry e; + e.objectId = 3001; e.displayId = 171; + e.name = "Tin Vein"; e.typeId = WoweeGameObject::MineralNode; + e.castBarCaption = "Mining"; + e.requiredSkill = 186; // SkillLine mining (canonical id) + e.requiredSkillValue = 65; + e.flags = WoweeGameObject::Despawn; + c.entries.push_back(e); + } + { + WoweeGameObject::Entry e; + e.objectId = 3002; e.displayId = 172; + e.name = "Schools of Fish"; e.typeId = WoweeGameObject::FishingNode; + e.castBarCaption = "Fishing"; + e.requiredSkill = 356; // SkillLine fishing + e.requiredSkillValue = 1; + e.flags = WoweeGameObject::Despawn; + c.entries.push_back(e); + } + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 980b947c..8bed7605 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -43,6 +43,8 @@ const char* const kArgRequired[] = { "--export-wcrt-json", "--import-wcrt-json", "--gen-quests", "--gen-quests-chain", "--gen-quests-daily", "--info-wqt", "--validate-wqt", + "--gen-objects", "--gen-objects-dungeon", "--gen-objects-gather", + "--info-wgot", "--validate-wgot", "--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 1f0dde8a..a0bb0d3e 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -43,6 +43,7 @@ #include "cli_loot_catalog.hpp" #include "cli_creatures_catalog.hpp" #include "cli_quests_catalog.hpp" +#include "cli_objects_catalog.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -127,6 +128,7 @@ constexpr DispatchFn kDispatchTable[] = { handleLootCatalog, handleCreaturesCatalog, handleQuestsCatalog, + handleObjectsCatalog, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index 5f2d12fe..53650272 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -919,6 +919,16 @@ void printUsage(const char* argv0) { std::printf(" Print WQT entries (questId / level / giver / objectives / rewards / chain links)\n"); std::printf(" --validate-wqt [--json]\n"); std::printf(" Static checks: questId>0+unique, level>0+min<=max, title not empty, no rewards warning, daily needs repeatable\n"); + std::printf(" --gen-objects [name]\n"); + std::printf(" Emit .wgot starter object catalog: 1 chest + 1 mailbox + 1 sign\n"); + std::printf(" --gen-objects-dungeon [name]\n"); + std::printf(" Emit .wgot dungeon set: door + button + 2 chests (bandit + boss) + spike trap (cross-refs WLOT)\n"); + std::printf(" --gen-objects-gather [name]\n"); + std::printf(" Emit .wgot gathering nodes: Peacebloom (herb) + Tin Vein (ore) + Schools of Fish, with skill reqs\n"); + std::printf(" --info-wgot [--json]\n"); + std::printf(" Print WGOT entries (objectId / type / lock / loot / required skill / name)\n"); + std::printf(" --validate-wgot [--json]\n"); + std::printf(" Static checks: objectId>0+unique, size>0, time min<=max, gathering needs skill, chest warns on no loot\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_objects_catalog.cpp b/tools/editor/cli_objects_catalog.cpp new file mode 100644 index 00000000..5258a8ab --- /dev/null +++ b/tools/editor/cli_objects_catalog.cpp @@ -0,0 +1,255 @@ +#include "cli_objects_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_objects.hpp" +#include + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWgotExt(std::string base) { + stripExt(base, ".wgot"); + return base; +} + +void appendObjFlagsStr(std::string& s, uint32_t flags) { + if (flags & wowee::pipeline::WoweeGameObject::Disabled) s += "disabled "; + if (flags & wowee::pipeline::WoweeGameObject::ScriptOnly) s += "script-only "; + if (flags & wowee::pipeline::WoweeGameObject::UsableFromMount) s += "from-mount "; + if (flags & wowee::pipeline::WoweeGameObject::Despawn) s += "despawn "; + if (flags & wowee::pipeline::WoweeGameObject::Frozen) s += "frozen "; + if (flags & wowee::pipeline::WoweeGameObject::QuestGated) s += "quest-gated "; + if (s.empty()) s = "-"; + else if (s.back() == ' ') s.pop_back(); +} + +bool saveOrError(const wowee::pipeline::WoweeGameObject& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeGameObjectLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wgot\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeGameObject& c, + const std::string& base) { + std::printf("Wrote %s.wgot\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" objects : %zu\n", c.entries.size()); +} + +int handleGenStarter(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "StarterObjects"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWgotExt(base); + auto c = wowee::pipeline::WoweeGameObjectLoader::makeStarter(name); + if (!saveOrError(c, base, "gen-objects")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenDungeon(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "DungeonObjects"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWgotExt(base); + auto c = wowee::pipeline::WoweeGameObjectLoader::makeDungeon(name); + if (!saveOrError(c, base, "gen-objects-dungeon")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenGather(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "GatheringNodes"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWgotExt(base); + auto c = wowee::pipeline::WoweeGameObjectLoader::makeGather(name); + if (!saveOrError(c, base, "gen-objects-gather")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleInfo(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWgotExt(base); + if (!wowee::pipeline::WoweeGameObjectLoader::exists(base)) { + std::fprintf(stderr, "WGOT not found: %s.wgot\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeGameObjectLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wgot"] = base + ".wgot"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + std::string fs; + appendObjFlagsStr(fs, e.flags); + arr.push_back({ + {"objectId", e.objectId}, + {"displayId", e.displayId}, + {"name", e.name}, + {"typeId", e.typeId}, + {"typeName", wowee::pipeline::WoweeGameObject::typeName(e.typeId)}, + {"size", e.size}, + {"castBarCaption", e.castBarCaption}, + {"requiredSkill", e.requiredSkill}, + {"requiredSkillValue", e.requiredSkillValue}, + {"lockId", e.lockId}, + {"lootTableId", e.lootTableId}, + {"minOpenTimeMs", e.minOpenTimeMs}, + {"maxOpenTimeMs", e.maxOpenTimeMs}, + {"flags", e.flags}, + {"flagsStr", fs}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WGOT: %s.wgot\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" objects : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id type lock loot skill name\n"); + for (const auto& e : c.entries) { + std::printf(" %4u %-13s %4u %4u %4u/%-3u %s\n", + e.objectId, + wowee::pipeline::WoweeGameObject::typeName(e.typeId), + e.lockId, e.lootTableId, + e.requiredSkill, e.requiredSkillValue, + 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 = stripWgotExt(base); + if (!wowee::pipeline::WoweeGameObjectLoader::exists(base)) { + std::fprintf(stderr, + "validate-wgot: WGOT not found: %s.wgot\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeGameObjectLoader::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.objectId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.objectId == 0) { + errors.push_back(ctx + ": objectId is 0"); + } + if (e.size <= 0.0f) { + errors.push_back(ctx + ": size <= 0"); + } + if (e.minOpenTimeMs > e.maxOpenTimeMs) { + errors.push_back(ctx + ": minOpenTimeMs > maxOpenTimeMs"); + } + // Gathering nodes need a skill requirement to be useful. + if ((e.typeId == wowee::pipeline::WoweeGameObject::HerbNode || + e.typeId == wowee::pipeline::WoweeGameObject::MineralNode || + e.typeId == wowee::pipeline::WoweeGameObject::FishingNode) && + e.requiredSkill == 0) { + warnings.push_back(ctx + + ": gathering node has no required skill (anyone can harvest)"); + } + // Chest with no loot table is rare but possible (event-spawn + // chests fill via script). + if (e.typeId == wowee::pipeline::WoweeGameObject::Chest && + e.lootTableId == 0) { + warnings.push_back(ctx + + ": chest has no lootTableId (script must populate)"); + } + // requiredSkillValue without requiredSkill is incoherent. + if (e.requiredSkill == 0 && e.requiredSkillValue > 0) { + errors.push_back(ctx + + ": requiredSkillValue > 0 but requiredSkill is 0"); + } + for (uint32_t prev : idsSeen) { + if (prev == e.objectId) { + errors.push_back(ctx + ": duplicate objectId"); + break; + } + } + idsSeen.push_back(e.objectId); + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wgot"] = base + ".wgot"; + 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-wgot: %s.wgot\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu objects, all objectIds 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 handleObjectsCatalog(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--gen-objects") == 0 && i + 1 < argc) { + outRc = handleGenStarter(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-objects-dungeon") == 0 && i + 1 < argc) { + outRc = handleGenDungeon(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-objects-gather") == 0 && i + 1 < argc) { + outRc = handleGenGather(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wgot") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wgot") == 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_objects_catalog.hpp b/tools/editor/cli_objects_catalog.hpp new file mode 100644 index 00000000..1ff4b9af --- /dev/null +++ b/tools/editor/cli_objects_catalog.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleObjectsCatalog(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee