diff --git a/CMakeLists.txt b/CMakeLists.txt index 62cafdb7..23c30d2f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -598,6 +598,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_weather.cpp src/pipeline/wowee_world_map.cpp src/pipeline/wowee_sound.cpp + src/pipeline/wowee_spawns.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1342,6 +1343,7 @@ add_executable(wowee_editor tools/editor/cli_world_info.cpp tools/editor/cli_world_map.cpp tools/editor/cli_sound_catalog.cpp + tools/editor/cli_spawns_catalog.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp @@ -1418,6 +1420,7 @@ add_executable(wowee_editor src/pipeline/wowee_weather.cpp src/pipeline/wowee_world_map.cpp src/pipeline/wowee_sound.cpp + src/pipeline/wowee_spawns.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_spawns.hpp b/include/pipeline/wowee_spawns.hpp new file mode 100644 index 00000000..be0a4bd3 --- /dev/null +++ b/include/pipeline/wowee_spawns.hpp @@ -0,0 +1,104 @@ +#pragma once + +#include +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Spawn Catalog (.wspn) — novel replacement for +// the scattered creature_template / gameobject SQL tables +// AzerothCore-style servers use, plus the static doodad +// placements that ADT MDDF / MODF chunks encode. One file +// holds all spawn points for a zone, regardless of kind: +// creatures (NPCs, monsters), game objects (chests, doors, +// signs), and pure-render doodads (trees, rocks, grass). +// +// Server runtimes read it to know what to spawn; the editor +// reads it to draw spawn markers; the renderer reads the +// doodad subset to know what static props to draw without +// going through the server roundtrip. +// +// Binary layout (little-endian): +// magic[4] = "WSPN" +// version (uint32) = current 1 +// nameLen (uint32) + name bytes -- catalog label +// entryCount (uint32) +// entries (each): +// kind (uint8) +// pad[3] +// entryId (uint32) -- creature_template/gameobject/displayId +// position[3] (3*float) +// rotation[3] (3*float) -- euler XYZ in radians +// scale (float) +// flags (uint32) +// respawnSec (uint32) -- 0 = static (doodad) +// factionId (uint32) -- 0 if N/A (game objects, doodads) +// questIdRequired (uint32) -- 0 = no quest gating +// wanderRadius (float) -- creatures only; 0 for objects +// labelLen (uint32) + label bytes +struct WoweeSpawns { + enum Kind : uint8_t { + Creature = 0, + GameObject = 1, + Doodad = 2, + }; + + enum Flags : uint32_t { + Disabled = 0x01, // present in catalog but not spawned + EventOnly = 0x02, // spawned only during a world event + QuestPhased = 0x04, // visible only to players with quest state + }; + + struct Entry { + uint8_t kind = Creature; + uint32_t entryId = 0; + glm::vec3 position{0}; + glm::vec3 rotation{0}; + float scale = 1.0f; + uint32_t flags = 0; + uint32_t respawnSec = 300; // 5 minutes default + uint32_t factionId = 0; + uint32_t questIdRequired = 0; + float wanderRadius = 5.0f; + std::string label; + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + // Per-kind counts — useful for the editor info display. + uint32_t countByKind(uint8_t k) const; + + static const char* kindName(uint8_t k); +}; + +class WoweeSpawnsLoader { +public: + static bool save(const WoweeSpawns& cat, + const std::string& basePath); + static WoweeSpawns load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-spawns* variants. + // + // makeStarter — one entry per kind: 1 creature + 1 game + // object + 1 doodad, all near (0,0,0). + // Useful as a template for hand-edit. + // makeCamp — bandit-camp spawn cluster: 4 creatures + // in a wander ring + 1 chest (game object) + // + 2 tent doodads. + // makeVillage — small village: 6 NPC creatures spread + // out + 2 game-object signs + 4 tree + // doodads. + static WoweeSpawns makeStarter(const std::string& catalogName); + static WoweeSpawns makeCamp(const std::string& catalogName); + static WoweeSpawns makeVillage(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_spawns.cpp b/src/pipeline/wowee_spawns.cpp new file mode 100644 index 00000000..142b3fd8 --- /dev/null +++ b/src/pipeline/wowee_spawns.cpp @@ -0,0 +1,286 @@ +#include "pipeline/wowee_spawns.hpp" + +#include +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'S', 'P', 'N'}; +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() < 5 || base.substr(base.size() - 5) != ".wspn") { + base += ".wspn"; + } + return base; +} + +} // namespace + +uint32_t WoweeSpawns::countByKind(uint8_t k) const { + uint32_t n = 0; + for (const auto& e : entries) if (e.kind == k) ++n; + return n; +} + +const char* WoweeSpawns::kindName(uint8_t k) { + switch (k) { + case Creature: return "creature"; + case GameObject: return "object"; + case Doodad: return "doodad"; + default: return "unknown"; + } +} + +bool WoweeSpawnsLoader::save(const WoweeSpawns& 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.kind); + uint8_t pad[3] = {0, 0, 0}; + os.write(reinterpret_cast(pad), 3); + writePOD(os, e.entryId); + writePOD(os, e.position.x); + writePOD(os, e.position.y); + writePOD(os, e.position.z); + writePOD(os, e.rotation.x); + writePOD(os, e.rotation.y); + writePOD(os, e.rotation.z); + writePOD(os, e.scale); + writePOD(os, e.flags); + writePOD(os, e.respawnSec); + writePOD(os, e.factionId); + writePOD(os, e.questIdRequired); + writePOD(os, e.wanderRadius); + writeStr(os, e.label); + } + return os.good(); +} + +WoweeSpawns WoweeSpawnsLoader::load(const std::string& basePath) { + WoweeSpawns 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; // 1M cap + out.entries.resize(entryCount); + for (auto& e : out.entries) { + if (!readPOD(is, e.kind)) { 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.entryId)) { out.entries.clear(); return out; } + if (!readPOD(is, e.position.x) || + !readPOD(is, e.position.y) || + !readPOD(is, e.position.z) || + !readPOD(is, e.rotation.x) || + !readPOD(is, e.rotation.y) || + !readPOD(is, e.rotation.z) || + !readPOD(is, e.scale) || + !readPOD(is, e.flags) || + !readPOD(is, e.respawnSec) || + !readPOD(is, e.factionId) || + !readPOD(is, e.questIdRequired) || + !readPOD(is, e.wanderRadius)) { + out.entries.clear(); + return out; + } + if (!readStr(is, e.label)) { out.entries.clear(); return out; } + } + return out; +} + +bool WoweeSpawnsLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeSpawns WoweeSpawnsLoader::makeStarter(const std::string& catalogName) { + WoweeSpawns c; + c.name = catalogName; + { + WoweeSpawns::Entry e; + e.kind = WoweeSpawns::Creature; + e.entryId = 1; e.position = {0, 0, 0}; + e.factionId = 35; e.respawnSec = 300; e.wanderRadius = 5.0f; + e.label = "starter creature"; + c.entries.push_back(e); + } + { + WoweeSpawns::Entry e; + e.kind = WoweeSpawns::GameObject; + e.entryId = 2; e.position = {3, 0, 0}; + e.respawnSec = 600; + e.wanderRadius = 0.0f; // game objects don't wander + e.label = "starter chest"; + c.entries.push_back(e); + } + { + WoweeSpawns::Entry e; + e.kind = WoweeSpawns::Doodad; + e.entryId = 100; e.position = {-3, 0, 0}; + e.respawnSec = 0; + e.wanderRadius = 0.0f; // doodads don't wander + e.label = "starter tree"; + c.entries.push_back(e); + } + return c; +} + +WoweeSpawns WoweeSpawnsLoader::makeCamp(const std::string& catalogName) { + WoweeSpawns c; + c.name = catalogName; + // 4 bandits spaced around a wander ring, all sharing + // the same template entry id and the same wander radius. + const float ringR = 4.0f; + for (int k = 0; k < 4; ++k) { + float a = (k / 4.0f) * 6.2831853f; + WoweeSpawns::Entry e; + e.kind = WoweeSpawns::Creature; + e.entryId = 1000; + e.position = {std::cos(a) * ringR, 0.0f, std::sin(a) * ringR}; + e.rotation = {0.0f, a + 3.14159265f, 0.0f}; // facing inward + e.factionId = 14; // hostile + e.respawnSec = 240; + e.wanderRadius = 3.0f; + e.label = std::string("bandit ") + std::to_string(k + 1); + c.entries.push_back(e); + } + { + WoweeSpawns::Entry e; + e.kind = WoweeSpawns::GameObject; + e.entryId = 2000; + e.position = {0, 0, 0}; + e.respawnSec = 1800; + e.wanderRadius = 0.0f; + e.label = "bandit chest"; + c.entries.push_back(e); + } + { + WoweeSpawns::Entry e; + e.kind = WoweeSpawns::Doodad; + e.entryId = 3000; + e.position = {2.0f, 0.0f, 2.0f}; + e.respawnSec = 0; + e.wanderRadius = 0.0f; + e.label = "tent A"; + c.entries.push_back(e); + } + { + WoweeSpawns::Entry e; + e.kind = WoweeSpawns::Doodad; + e.entryId = 3000; + e.position = {-2.0f, 0.0f, -2.0f}; + e.respawnSec = 0; + e.wanderRadius = 0.0f; + e.label = "tent B"; + c.entries.push_back(e); + } + return c; +} + +WoweeSpawns WoweeSpawnsLoader::makeVillage(const std::string& catalogName) { + WoweeSpawns c; + c.name = catalogName; + // 6 friendly NPCs (different roles) spread over a ~30 m + // square plus 2 signpost game objects + 4 tree doodads. + struct Npc { float x; float z; uint32_t id; const char* label; }; + Npc npcs[6] = { + { 0.0f, 0.0f, 4001, "innkeeper" }, + { 12.0f, -5.0f, 4002, "smith" }, + {-10.0f, 8.0f, 4003, "alchemist" }, + { 6.0f, 10.0f, 4004, "scribe" }, + { -8.0f, -7.0f, 4005, "guard captain" }, + { 15.0f, 3.0f, 4006, "stable master" }, + }; + for (const auto& n : npcs) { + WoweeSpawns::Entry e; + e.kind = WoweeSpawns::Creature; + e.entryId = n.id; + e.position = {n.x, 0.0f, n.z}; + e.factionId = 35; // friendly + e.respawnSec = 600; + e.wanderRadius = 1.0f; + e.label = n.label; + c.entries.push_back(e); + } + for (int k = 0; k < 2; ++k) { + WoweeSpawns::Entry e; + e.kind = WoweeSpawns::GameObject; + e.entryId = 5000 + k; + e.position = {k == 0 ? -15.0f : 15.0f, 0.0f, 0.0f}; + e.respawnSec = 0; + e.wanderRadius = 0.0f; + e.label = (k == 0 ? "north sign" : "south sign"); + c.entries.push_back(e); + } + struct Tree { float x; float z; }; + Tree trees[4] = { + { -18.0f, -12.0f }, { 18.0f, -12.0f }, + { -18.0f, 12.0f }, { 18.0f, 12.0f }, + }; + for (const auto& t : trees) { + WoweeSpawns::Entry e; + e.kind = WoweeSpawns::Doodad; + e.entryId = 6000; + e.position = {t.x, 0.0f, t.z}; + e.respawnSec = 0; + e.wanderRadius = 0.0f; + e.label = "village tree"; + 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 c22ae6d5..5ccf536f 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -29,6 +29,8 @@ const char* const kArgRequired[] = { "--gen-sound-catalog-tavern", "--info-wsnd", "--validate-wsnd", "--export-wsnd-json", "--import-wsnd-json", + "--gen-spawns", "--gen-spawns-camp", "--gen-spawns-village", + "--info-wspn", "--validate-wspn", "--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 8aa8e206..6ad4bacd 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -38,6 +38,7 @@ #include "cli_world_info.hpp" #include "cli_world_map.hpp" #include "cli_sound_catalog.hpp" +#include "cli_spawns_catalog.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -117,6 +118,7 @@ constexpr DispatchFn kDispatchTable[] = { handleWorldInfo, handleWorldMap, handleSoundCatalog, + handleSpawnsCatalog, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index ca9484e6..457227a4 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -851,6 +851,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wsnd to a human-editable JSON sidecar (defaults to .wsnd.json)\n"); std::printf(" --import-wsnd-json [out-base]\n"); std::printf(" Import a .wsnd.json sidecar back into binary .wsnd (accepts either kind int OR kindName string)\n"); + std::printf(" --gen-spawns [name]\n"); + std::printf(" Emit .wspn starter spawn catalog: 1 creature + 1 game object + 1 doodad near origin\n"); + std::printf(" --gen-spawns-camp [name]\n"); + std::printf(" Emit .wspn bandit-camp catalog: 4 creatures around a wander ring + 1 chest + 2 tents\n"); + std::printf(" --gen-spawns-village [name]\n"); + std::printf(" Emit .wspn village catalog: 6 friendly NPCs (mixed roles) + 2 signs + 4 corner trees\n"); + std::printf(" --info-wspn [--json]\n"); + std::printf(" Print WSPN spawn entries (kind / entryId / position / respawn / wander radius / label)\n"); + std::printf(" --validate-wspn [--json]\n"); + std::printf(" Static checks: kind 0..2, finite position/scale/wander, doodads have respawn=0, no orphan entryId=0\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_spawns_catalog.cpp b/tools/editor/cli_spawns_catalog.cpp new file mode 100644 index 00000000..8b66d33c --- /dev/null +++ b/tools/editor/cli_spawns_catalog.cpp @@ -0,0 +1,254 @@ +#include "cli_spawns_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_spawns.hpp" +#include + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWspnExt(std::string base) { + stripExt(base, ".wspn"); + return base; +} + +bool saveOrError(const wowee::pipeline::WoweeSpawns& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeSpawnsLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wspn\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeSpawns& c, + const std::string& base) { + std::printf("Wrote %s.wspn\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" entries : %zu (creature=%u object=%u doodad=%u)\n", + c.entries.size(), + c.countByKind(wowee::pipeline::WoweeSpawns::Creature), + c.countByKind(wowee::pipeline::WoweeSpawns::GameObject), + c.countByKind(wowee::pipeline::WoweeSpawns::Doodad)); +} + +int handleGenStarter(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "StarterSpawns"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWspnExt(base); + auto c = wowee::pipeline::WoweeSpawnsLoader::makeStarter(name); + if (!saveOrError(c, base, "gen-spawns")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenCamp(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "BanditCamp"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWspnExt(base); + auto c = wowee::pipeline::WoweeSpawnsLoader::makeCamp(name); + if (!saveOrError(c, base, "gen-spawns-camp")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenVillage(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "VillageSpawns"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWspnExt(base); + auto c = wowee::pipeline::WoweeSpawnsLoader::makeVillage(name); + if (!saveOrError(c, base, "gen-spawns-village")) 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 = stripWspnExt(base); + if (!wowee::pipeline::WoweeSpawnsLoader::exists(base)) { + std::fprintf(stderr, "WSPN not found: %s.wspn\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeSpawnsLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wspn"] = base + ".wspn"; + j["name"] = c.name; + j["count"] = c.entries.size(); + j["countCreature"] = + c.countByKind(wowee::pipeline::WoweeSpawns::Creature); + j["countObject"] = + c.countByKind(wowee::pipeline::WoweeSpawns::GameObject); + j["countDoodad"] = + c.countByKind(wowee::pipeline::WoweeSpawns::Doodad); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + arr.push_back({ + {"kind", e.kind}, + {"kindName", wowee::pipeline::WoweeSpawns::kindName(e.kind)}, + {"entryId", e.entryId}, + {"position", {e.position.x, e.position.y, e.position.z}}, + {"rotation", {e.rotation.x, e.rotation.y, e.rotation.z}}, + {"scale", e.scale}, + {"flags", e.flags}, + {"respawnSec", e.respawnSec}, + {"factionId", e.factionId}, + {"questIdRequired", e.questIdRequired}, + {"wanderRadius", e.wanderRadius}, + {"label", e.label}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WSPN: %s.wspn\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" entries : %zu (creature=%u object=%u doodad=%u)\n", + c.entries.size(), + c.countByKind(wowee::pipeline::WoweeSpawns::Creature), + c.countByKind(wowee::pipeline::WoweeSpawns::GameObject), + c.countByKind(wowee::pipeline::WoweeSpawns::Doodad)); + if (c.entries.empty()) return 0; + std::printf(" kind entry pos (x, y, z) respawn rad label\n"); + for (const auto& e : c.entries) { + std::printf(" %-9s %5u (%6.1f,%6.1f,%6.1f) %5us %4.1f %s\n", + wowee::pipeline::WoweeSpawns::kindName(e.kind), + e.entryId, + e.position.x, e.position.y, e.position.z, + e.respawnSec, e.wanderRadius, + e.label.c_str()); + } + return 0; +} + +int handleValidate(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWspnExt(base); + if (!wowee::pipeline::WoweeSpawnsLoader::exists(base)) { + std::fprintf(stderr, + "validate-wspn: WSPN not found: %s.wspn\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeSpawnsLoader::load(base); + std::vector errors; + std::vector warnings; + if (c.entries.empty()) { + warnings.push_back("catalog has zero entries"); + } + for (size_t k = 0; k < c.entries.size(); ++k) { + const auto& e = c.entries[k]; + std::string ctx = "entry " + std::to_string(k); + if (!e.label.empty()) ctx += " (" + e.label + ")"; + if (e.kind > wowee::pipeline::WoweeSpawns::Doodad) { + errors.push_back(ctx + ": kind " + std::to_string(e.kind) + + " not in known range 0..2"); + } + if (!std::isfinite(e.position.x) || + !std::isfinite(e.position.y) || + !std::isfinite(e.position.z) || + !std::isfinite(e.rotation.x) || + !std::isfinite(e.rotation.y) || + !std::isfinite(e.rotation.z)) { + errors.push_back(ctx + ": position/rotation not finite"); + } + if (!std::isfinite(e.scale) || e.scale <= 0) { + errors.push_back(ctx + ": scale not finite or <= 0"); + } + if (!std::isfinite(e.wanderRadius) || e.wanderRadius < 0) { + errors.push_back(ctx + ": wanderRadius not finite or < 0"); + } + // Doodads should not have a respawn timer (they are + // permanent visual props). Catch the common misuse. + if (e.kind == wowee::pipeline::WoweeSpawns::Doodad && + e.respawnSec != 0) { + warnings.push_back(ctx + + ": doodad has non-zero respawnSec — doodads are static"); + } + // Creatures with respawn 0 will spawn once and never + // come back; flag as a warning since it's almost + // always a mistake. + if (e.kind == wowee::pipeline::WoweeSpawns::Creature && + e.respawnSec == 0 && + !(e.flags & wowee::pipeline::WoweeSpawns::EventOnly)) { + warnings.push_back(ctx + + ": creature with respawnSec=0 will not respawn after kill"); + } + if (e.entryId == 0) { + warnings.push_back(ctx + + ": entryId is 0 (no template referenced)"); + } + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wspn"] = base + ".wspn"; + 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-wspn: %s.wspn\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu entries (creature=%u object=%u doodad=%u)\n", + c.entries.size(), + c.countByKind(wowee::pipeline::WoweeSpawns::Creature), + c.countByKind(wowee::pipeline::WoweeSpawns::GameObject), + c.countByKind(wowee::pipeline::WoweeSpawns::Doodad)); + 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 handleSpawnsCatalog(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--gen-spawns") == 0 && i + 1 < argc) { + outRc = handleGenStarter(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-spawns-camp") == 0 && i + 1 < argc) { + outRc = handleGenCamp(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-spawns-village") == 0 && i + 1 < argc) { + outRc = handleGenVillage(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wspn") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wspn") == 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_spawns_catalog.hpp b/tools/editor/cli_spawns_catalog.hpp new file mode 100644 index 00000000..ba47216d --- /dev/null +++ b/tools/editor/cli_spawns_catalog.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleSpawnsCatalog(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee