diff --git a/CMakeLists.txt b/CMakeLists.txt index 905e83da..b21ed476 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -611,6 +611,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_achievements.cpp src/pipeline/wowee_trainers.cpp src/pipeline/wowee_gossip.cpp + src/pipeline/wowee_taxi.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1368,6 +1369,7 @@ add_executable(wowee_editor tools/editor/cli_achievements_catalog.cpp tools/editor/cli_trainers_catalog.cpp tools/editor/cli_gossip_catalog.cpp + tools/editor/cli_taxi_catalog.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp @@ -1457,6 +1459,7 @@ add_executable(wowee_editor src/pipeline/wowee_achievements.cpp src/pipeline/wowee_trainers.cpp src/pipeline/wowee_gossip.cpp + src/pipeline/wowee_taxi.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_taxi.hpp b/include/pipeline/wowee_taxi.hpp new file mode 100644 index 00000000..421a0fe0 --- /dev/null +++ b/include/pipeline/wowee_taxi.hpp @@ -0,0 +1,113 @@ +#pragma once + +#include +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Taxi catalog (.wtax) — novel replacement for +// Blizzard's TaxiNodes.dbc + TaxiPath.dbc + TaxiPathNode.dbc. +// The 24th open format added to the editor. +// +// Defines the flight-master network: a set of named nodes +// (positions on the world map) plus the paths between them +// (sequences of waypoints with per-segment delay and a +// per-path gold cost). The same file holds both node and +// path lists — flat arrays keyed by id. +// +// Cross-references with previously-added formats: +// WCRT.entry (with FlightMaster npcFlag) ≈ WTAX.entry.nodeId +// (matched by world +// position, not by +// direct ID — the +// flight-master NPC +// stands at the node) +// WTAX.path.fromNodeId / toNodeId → WTAX.entry.nodeId +// (intra-format graph) +// +// Binary layout (little-endian): +// magic[4] = "WTAX" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// nodeCount (uint32) +// nodes (each): +// nodeId (uint32) +// mapId (uint32) +// nameLen + name +// iconLen + iconPath +// position (3 × float) +// factionAlliance (uint32) / factionHorde (uint32) +// pathCount (uint32) +// paths (each): +// pathId (uint32) +// fromNodeId (uint32) / toNodeId (uint32) +// moneyCostCopper (uint32) +// waypointCount (uint32) +// waypoints (waypointCount × { +// position (3 × float) +// delaySec (float) +// }) +struct WoweeTaxi { + struct Node { + uint32_t nodeId = 0; + uint32_t mapId = 0; + std::string name; + std::string iconPath; + glm::vec3 position{0}; + uint32_t factionAlliance = 0; // 0 = available to all + uint32_t factionHorde = 0; + }; + + struct Waypoint { + glm::vec3 position{0}; + float delaySec = 0.0f; // pause at this waypoint + }; + + struct Path { + uint32_t pathId = 0; + uint32_t fromNodeId = 0; + uint32_t toNodeId = 0; + uint32_t moneyCostCopper = 0; + std::vector waypoints; + }; + + std::string name; + std::vector nodes; + std::vector paths; + + bool isValid() const { return !nodes.empty(); } + + // Lookup helpers. + const Node* findNode(uint32_t nodeId) const; + const Path* findPath(uint32_t pathId) const; + // First path matching a from→to pair, or nullptr. + const Path* findPathBetween(uint32_t fromNodeId, uint32_t toNodeId) const; +}; + +class WoweeTaxiLoader { +public: + static bool save(const WoweeTaxi& cat, + const std::string& basePath); + static WoweeTaxi load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-taxi* variants. + // + // makeStarter — 2 nodes + 1 path (round-trip 2 cities, + // 3 waypoints, 50 silver each way). + // makeRegion — 4 nodes around a square (~500m apart) + + // 4 paths forming a connected ring + // (each path is 2 waypoints). + // makeContinent — 6 nodes + 8 paths covering a small + // continent's flight network with + // cross-route shortcuts. + static WoweeTaxi makeStarter(const std::string& catalogName); + static WoweeTaxi makeRegion(const std::string& catalogName); + static WoweeTaxi makeContinent(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_taxi.cpp b/src/pipeline/wowee_taxi.cpp new file mode 100644 index 00000000..00cae8c8 --- /dev/null +++ b/src/pipeline/wowee_taxi.cpp @@ -0,0 +1,310 @@ +#include "pipeline/wowee_taxi.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'T', 'A', 'X'}; +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) != ".wtax") { + base += ".wtax"; + } + return base; +} + +} // namespace + +const WoweeTaxi::Node* WoweeTaxi::findNode(uint32_t nodeId) const { + for (const auto& n : nodes) if (n.nodeId == nodeId) return &n; + return nullptr; +} + +const WoweeTaxi::Path* WoweeTaxi::findPath(uint32_t pathId) const { + for (const auto& p : paths) if (p.pathId == pathId) return &p; + return nullptr; +} + +const WoweeTaxi::Path* WoweeTaxi::findPathBetween(uint32_t fromNodeId, + uint32_t toNodeId) const { + for (const auto& p : paths) { + if (p.fromNodeId == fromNodeId && p.toNodeId == toNodeId) return &p; + } + return nullptr; +} + +bool WoweeTaxiLoader::save(const WoweeTaxi& 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 nodeCount = static_cast(cat.nodes.size()); + writePOD(os, nodeCount); + for (const auto& n : cat.nodes) { + writePOD(os, n.nodeId); + writePOD(os, n.mapId); + writeStr(os, n.name); + writeStr(os, n.iconPath); + writePOD(os, n.position.x); + writePOD(os, n.position.y); + writePOD(os, n.position.z); + writePOD(os, n.factionAlliance); + writePOD(os, n.factionHorde); + } + uint32_t pathCount = static_cast(cat.paths.size()); + writePOD(os, pathCount); + for (const auto& p : cat.paths) { + writePOD(os, p.pathId); + writePOD(os, p.fromNodeId); + writePOD(os, p.toNodeId); + writePOD(os, p.moneyCostCopper); + uint32_t wpCount = static_cast(p.waypoints.size()); + writePOD(os, wpCount); + for (const auto& w : p.waypoints) { + writePOD(os, w.position.x); + writePOD(os, w.position.y); + writePOD(os, w.position.z); + writePOD(os, w.delaySec); + } + } + return os.good(); +} + +WoweeTaxi WoweeTaxiLoader::load(const std::string& basePath) { + WoweeTaxi 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 nodeCount = 0; + if (!readPOD(is, nodeCount)) return out; + if (nodeCount > (1u << 20)) return out; + out.nodes.resize(nodeCount); + for (auto& n : out.nodes) { + if (!readPOD(is, n.nodeId) || !readPOD(is, n.mapId)) { + out.nodes.clear(); return out; + } + if (!readStr(is, n.name) || !readStr(is, n.iconPath)) { + out.nodes.clear(); return out; + } + if (!readPOD(is, n.position.x) || + !readPOD(is, n.position.y) || + !readPOD(is, n.position.z) || + !readPOD(is, n.factionAlliance) || + !readPOD(is, n.factionHorde)) { + out.nodes.clear(); return out; + } + } + uint32_t pathCount = 0; + if (!readPOD(is, pathCount)) { + out.nodes.clear(); return out; + } + if (pathCount > (1u << 20)) { + out.nodes.clear(); return out; + } + out.paths.resize(pathCount); + for (auto& p : out.paths) { + if (!readPOD(is, p.pathId) || + !readPOD(is, p.fromNodeId) || + !readPOD(is, p.toNodeId) || + !readPOD(is, p.moneyCostCopper)) { + out.nodes.clear(); out.paths.clear(); return out; + } + uint32_t wpCount = 0; + if (!readPOD(is, wpCount)) { + out.nodes.clear(); out.paths.clear(); return out; + } + if (wpCount > (1u << 16)) { + out.nodes.clear(); out.paths.clear(); return out; + } + p.waypoints.resize(wpCount); + for (auto& w : p.waypoints) { + if (!readPOD(is, w.position.x) || + !readPOD(is, w.position.y) || + !readPOD(is, w.position.z) || + !readPOD(is, w.delaySec)) { + out.nodes.clear(); out.paths.clear(); return out; + } + } + } + return out; +} + +bool WoweeTaxiLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeTaxi WoweeTaxiLoader::makeStarter(const std::string& catalogName) { + WoweeTaxi c; + c.name = catalogName; + { + WoweeTaxi::Node n; + n.nodeId = 1; n.mapId = 0; + n.name = "Stormwind Gryphon Master"; + n.position = {-9000.0f, 100.0f, 50.0f}; + c.nodes.push_back(n); + } + { + WoweeTaxi::Node n; + n.nodeId = 2; n.mapId = 0; + n.name = "Goldshire Gryphon Master"; + n.position = {-9460.0f, 60.0f, 56.0f}; + c.nodes.push_back(n); + } + { + WoweeTaxi::Path p; + p.pathId = 1; p.fromNodeId = 1; p.toNodeId = 2; + p.moneyCostCopper = 5000; // 50 silver + // 3 waypoints carving a gentle arc between the cities. + p.waypoints.push_back({{-9100.0f, 90.0f, 80.0f}, 0.0f}); + p.waypoints.push_back({{-9250.0f, 70.0f, 90.0f}, 0.0f}); + p.waypoints.push_back({{-9460.0f, 60.0f, 56.0f}, 0.0f}); + c.paths.push_back(p); + } + { + WoweeTaxi::Path p; + p.pathId = 2; p.fromNodeId = 2; p.toNodeId = 1; + p.moneyCostCopper = 5000; + p.waypoints.push_back({{-9250.0f, 70.0f, 90.0f}, 0.0f}); + p.waypoints.push_back({{-9100.0f, 90.0f, 80.0f}, 0.0f}); + p.waypoints.push_back({{-9000.0f, 100.0f, 50.0f}, 0.0f}); + c.paths.push_back(p); + } + return c; +} + +WoweeTaxi WoweeTaxiLoader::makeRegion(const std::string& catalogName) { + WoweeTaxi c; + c.name = catalogName; + // 4 nodes at corners of a 500m square at y=80 altitude. + struct Pos { float x; float z; const char* name; }; + Pos posns[4] = { + { -250.0f, -250.0f, "Northwest Outpost" }, + { 250.0f, -250.0f, "Northeast Outpost" }, + { 250.0f, 250.0f, "Southeast Outpost" }, + { -250.0f, 250.0f, "Southwest Outpost" }, + }; + for (int k = 0; k < 4; ++k) { + WoweeTaxi::Node n; + n.nodeId = 100 + k; + n.mapId = 0; + n.name = posns[k].name; + n.position = {posns[k].x, 60.0f, posns[k].z}; + c.nodes.push_back(n); + } + // 4 paths forming a directed ring NW -> NE -> SE -> SW -> NW. + for (int k = 0; k < 4; ++k) { + int from = 100 + k; + int to = 100 + ((k + 1) % 4); + WoweeTaxi::Path p; + p.pathId = 100 + k; + p.fromNodeId = from; p.toNodeId = to; + p.moneyCostCopper = 2500; + // 2 intermediate waypoints at altitude 90 (climb + + // descend pattern). + const auto& a = c.nodes[k].position; + const auto& b = c.nodes[(k + 1) % 4].position; + glm::vec3 mid1 = a + (b - a) * 0.33f; mid1.y = 90.0f; + glm::vec3 mid2 = a + (b - a) * 0.67f; mid2.y = 90.0f; + p.waypoints.push_back({mid1, 0.0f}); + p.waypoints.push_back({mid2, 0.0f}); + p.waypoints.push_back({b, 0.0f}); + c.paths.push_back(p); + } + return c; +} + +WoweeTaxi WoweeTaxiLoader::makeContinent(const std::string& catalogName) { + WoweeTaxi c; + c.name = catalogName; + // 6 nodes spread across a continent — a hub-and-spoke + // network with 1 central node connected to 5 outliers. + struct Pos { float x; float z; const char* name; }; + Pos posns[6] = { + { 0.0f, 0.0f, "Crossroads (hub)" }, + { -1500.0f, -1500.0f, "Stormwind" }, + { 1500.0f, -1500.0f, "Stranglethorn" }, + { 1500.0f, 1500.0f, "Lordaeron" }, + { -1500.0f, 1500.0f, "Westfall" }, + { 0.0f, 3000.0f, "Tirisfal" }, + }; + for (int k = 0; k < 6; ++k) { + WoweeTaxi::Node n; + n.nodeId = 200 + k; + n.mapId = 0; + n.name = posns[k].name; + n.position = {posns[k].x, 80.0f, posns[k].z}; + c.nodes.push_back(n); + } + // 8 paths: 5 hub-spoke (out + return) plus 3 cross-route + // shortcuts on the perimeter. + auto addPath = [&](uint32_t pid, uint32_t from, uint32_t to, + uint32_t cost) { + WoweeTaxi::Path p; + p.pathId = pid; p.fromNodeId = from; p.toNodeId = to; + p.moneyCostCopper = cost; + const auto& a = c.findNode(from)->position; + const auto& b = c.findNode(to)->position; + glm::vec3 mid1 = a + (b - a) * 0.5f; mid1.y = 120.0f; + p.waypoints.push_back({mid1, 0.0f}); + p.waypoints.push_back({b, 0.0f}); + c.paths.push_back(p); + }; + addPath(200, 200, 201, 8000); // hub -> Stormwind + addPath(201, 200, 202, 12000); // hub -> Stranglethorn + addPath(202, 200, 203, 10000); // hub -> Lordaeron + addPath(203, 200, 204, 6000); // hub -> Westfall + addPath(204, 200, 205, 15000); // hub -> Tirisfal + addPath(205, 201, 204, 4000); // Stormwind -> Westfall (perimeter) + addPath(206, 202, 203, 18000); // Stranglethorn -> Lordaeron + addPath(207, 203, 205, 6000); // Lordaeron -> Tirisfal + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 9d0cce31..a2d25fe9 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -67,6 +67,8 @@ const char* const kArgRequired[] = { "--export-wtrn-json", "--import-wtrn-json", "--gen-gossip", "--gen-gossip-innkeeper", "--gen-gossip-questgiver", "--info-wgsp", "--validate-wgsp", + "--gen-taxi", "--gen-taxi-region", "--gen-taxi-continent", + "--info-wtax", "--validate-wtax", "--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 7fc3ffe6..0c83f03b 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -51,6 +51,7 @@ #include "cli_achievements_catalog.hpp" #include "cli_trainers_catalog.hpp" #include "cli_gossip_catalog.hpp" +#include "cli_taxi_catalog.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -143,6 +144,7 @@ constexpr DispatchFn kDispatchTable[] = { handleAchievementsCatalog, handleTrainersCatalog, handleGossipCatalog, + handleTaxiCatalog, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index 0d4c12bc..f3ed14dc 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1033,6 +1033,16 @@ void printUsage(const char* argv0) { std::printf(" Print WGSP entries (menuId / title / per-option kind / target / cost / flags)\n"); std::printf(" --validate-wgsp [--json]\n"); std::printf(" Static checks: menuId>0+unique, options non-empty, Submenu actionTarget exists, Coinpouch needs cost, faction conflict\n"); + std::printf(" --gen-taxi [name]\n"); + std::printf(" Emit .wtax starter: 2 nodes (Stormwind / Goldshire) + 2 paths (round-trip, 50s each, 3 waypoints)\n"); + std::printf(" --gen-taxi-region [name]\n"); + std::printf(" Emit .wtax 4-node region: NW/NE/SE/SW outposts on a 500m square + 4-path directed ring\n"); + std::printf(" --gen-taxi-continent [name]\n"); + std::printf(" Emit .wtax 6-node hub-spoke continent: central crossroads + 5 outliers + 3 perimeter shortcuts (8 paths)\n"); + std::printf(" --info-wtax [--json]\n"); + std::printf(" Print WTAX nodes (id / map / position / name) + paths (id / from->to / cost / waypoint count)\n"); + std::printf(" --validate-wtax [--json]\n"); + std::printf(" Static checks: ids>0+unique, finite positions, paths reference real nodes, no self-loop, non-negative delays\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_taxi_catalog.cpp b/tools/editor/cli_taxi_catalog.cpp new file mode 100644 index 00000000..de9e2653 --- /dev/null +++ b/tools/editor/cli_taxi_catalog.cpp @@ -0,0 +1,301 @@ +#include "cli_taxi_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_taxi.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWtaxExt(std::string base) { + stripExt(base, ".wtax"); + return base; +} + +bool saveOrError(const wowee::pipeline::WoweeTaxi& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeTaxiLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wtax\n", + cmd, base.c_str()); + return false; + } + return true; +} + +uint32_t totalWaypoints(const wowee::pipeline::WoweeTaxi& c) { + uint32_t n = 0; + for (const auto& p : c.paths) n += static_cast(p.waypoints.size()); + return n; +} + +void printGenSummary(const wowee::pipeline::WoweeTaxi& c, + const std::string& base) { + std::printf("Wrote %s.wtax\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" nodes : %zu\n", c.nodes.size()); + std::printf(" paths : %zu (%u waypoints total)\n", + c.paths.size(), totalWaypoints(c)); +} + +int handleGenStarter(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "StarterTaxi"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWtaxExt(base); + auto c = wowee::pipeline::WoweeTaxiLoader::makeStarter(name); + if (!saveOrError(c, base, "gen-taxi")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenRegion(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "RegionTaxi"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWtaxExt(base); + auto c = wowee::pipeline::WoweeTaxiLoader::makeRegion(name); + if (!saveOrError(c, base, "gen-taxi-region")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenContinent(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "ContinentTaxi"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWtaxExt(base); + auto c = wowee::pipeline::WoweeTaxiLoader::makeContinent(name); + if (!saveOrError(c, base, "gen-taxi-continent")) 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 = stripWtaxExt(base); + if (!wowee::pipeline::WoweeTaxiLoader::exists(base)) { + std::fprintf(stderr, "WTAX not found: %s.wtax\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeTaxiLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wtax"] = base + ".wtax"; + j["name"] = c.name; + j["nodeCount"] = c.nodes.size(); + j["pathCount"] = c.paths.size(); + j["totalWaypoints"] = totalWaypoints(c); + nlohmann::json na = nlohmann::json::array(); + for (const auto& n : c.nodes) { + na.push_back({ + {"nodeId", n.nodeId}, + {"mapId", n.mapId}, + {"name", n.name}, + {"iconPath", n.iconPath}, + {"position", {n.position.x, n.position.y, n.position.z}}, + {"factionAlliance", n.factionAlliance}, + {"factionHorde", n.factionHorde}, + }); + } + j["nodes"] = na; + nlohmann::json pa = nlohmann::json::array(); + for (const auto& p : c.paths) { + nlohmann::json wpa = nlohmann::json::array(); + for (const auto& w : p.waypoints) { + wpa.push_back({ + {"position", {w.position.x, w.position.y, w.position.z}}, + {"delaySec", w.delaySec}, + }); + } + pa.push_back({ + {"pathId", p.pathId}, + {"fromNodeId", p.fromNodeId}, + {"toNodeId", p.toNodeId}, + {"moneyCostCopper", p.moneyCostCopper}, + {"waypoints", wpa}, + }); + } + j["paths"] = pa; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WTAX: %s.wtax\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" nodes : %zu\n", c.nodes.size()); + std::printf(" paths : %zu (%u waypoints total)\n", + c.paths.size(), totalWaypoints(c)); + if (!c.nodes.empty()) { + std::printf("\n Nodes:\n"); + std::printf(" id map pos (x, y, z) name\n"); + for (const auto& n : c.nodes) { + std::printf(" %4u %3u (%7.1f,%6.1f,%7.1f) %s\n", + n.nodeId, n.mapId, + n.position.x, n.position.y, n.position.z, + n.name.c_str()); + } + } + if (!c.paths.empty()) { + std::printf("\n Paths:\n"); + std::printf(" id from -> to cost waypoints\n"); + for (const auto& p : c.paths) { + std::printf(" %4u %4u -> %-4u %5uc %zu\n", + p.pathId, p.fromNodeId, p.toNodeId, + p.moneyCostCopper, p.waypoints.size()); + } + } + return 0; +} + +int handleValidate(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWtaxExt(base); + if (!wowee::pipeline::WoweeTaxiLoader::exists(base)) { + std::fprintf(stderr, + "validate-wtax: WTAX not found: %s.wtax\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeTaxiLoader::load(base); + std::vector errors; + std::vector warnings; + if (c.nodes.empty()) { + warnings.push_back("catalog has zero nodes"); + } + std::vector nodeIdsSeen; + for (size_t k = 0; k < c.nodes.size(); ++k) { + const auto& n = c.nodes[k]; + std::string ctx = "node " + std::to_string(k) + + " (id=" + std::to_string(n.nodeId); + if (!n.name.empty()) ctx += " " + n.name; + ctx += ")"; + if (n.nodeId == 0) { + errors.push_back(ctx + ": nodeId is 0"); + } + if (n.name.empty()) { + errors.push_back(ctx + ": name is empty"); + } + if (!std::isfinite(n.position.x) || + !std::isfinite(n.position.y) || + !std::isfinite(n.position.z)) { + errors.push_back(ctx + ": position not finite"); + } + for (uint32_t prev : nodeIdsSeen) { + if (prev == n.nodeId) { + errors.push_back(ctx + ": duplicate nodeId"); + break; + } + } + nodeIdsSeen.push_back(n.nodeId); + } + std::vector pathIdsSeen; + for (size_t k = 0; k < c.paths.size(); ++k) { + const auto& p = c.paths[k]; + std::string ctx = "path " + std::to_string(k) + + " (id=" + std::to_string(p.pathId) + ")"; + if (p.pathId == 0) { + errors.push_back(ctx + ": pathId is 0"); + } + if (p.fromNodeId == p.toNodeId) { + errors.push_back(ctx + + ": fromNodeId == toNodeId (path goes nowhere)"); + } + if (!c.findNode(p.fromNodeId)) { + errors.push_back(ctx + ": fromNodeId " + + std::to_string(p.fromNodeId) + " does not exist"); + } + if (!c.findNode(p.toNodeId)) { + errors.push_back(ctx + ": toNodeId " + + std::to_string(p.toNodeId) + " does not exist"); + } + if (p.waypoints.empty()) { + warnings.push_back(ctx + + ": no waypoints (gryphon teleports instantly)"); + } + for (size_t wi = 0; wi < p.waypoints.size(); ++wi) { + const auto& w = p.waypoints[wi]; + if (!std::isfinite(w.position.x) || + !std::isfinite(w.position.y) || + !std::isfinite(w.position.z) || + !std::isfinite(w.delaySec)) { + errors.push_back(ctx + " waypoint " + + std::to_string(wi) + ": position/delay not finite"); + } + if (w.delaySec < 0) { + errors.push_back(ctx + " waypoint " + + std::to_string(wi) + ": delaySec is negative"); + } + } + for (uint32_t prev : pathIdsSeen) { + if (prev == p.pathId) { + errors.push_back(ctx + ": duplicate pathId"); + break; + } + } + pathIdsSeen.push_back(p.pathId); + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wtax"] = base + ".wtax"; + 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-wtax: %s.wtax\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu nodes, %zu paths, %u waypoints, all IDs unique\n", + c.nodes.size(), c.paths.size(), totalWaypoints(c)); + 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 handleTaxiCatalog(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--gen-taxi") == 0 && i + 1 < argc) { + outRc = handleGenStarter(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-taxi-region") == 0 && i + 1 < argc) { + outRc = handleGenRegion(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-taxi-continent") == 0 && i + 1 < argc) { + outRc = handleGenContinent(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wtax") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wtax") == 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_taxi_catalog.hpp b/tools/editor/cli_taxi_catalog.hpp new file mode 100644 index 00000000..8e2d0155 --- /dev/null +++ b/tools/editor/cli_taxi_catalog.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleTaxiCatalog(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee