feat(pipeline): add WTAX (Wowee Taxi catalog) format

Novel open 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, with intra-format
references from path.fromNodeId / toNodeId to node.nodeId.

Cross-references:
  WCRT.entry (with FlightMaster npcFlag) ~= WTAX.nodeId
                                            (matched by world
                                             position; flight
                                             master NPCs stand
                                             at their nodes)
  WTAX.path.fromNodeId / toNodeId -> WTAX.entry.nodeId
                                     (intra-format graph)

Format:
  • magic "WTAX", version 1, little-endian
  • nodes (each): nodeId / mapId / name / iconPath /
    position / faction restrictions
  • paths (each): pathId / from+toNodeId / moneyCostCopper /
    waypoints[] each with position + per-waypoint delaySec

API: WoweeTaxiLoader::save / load / exists +
WoweeTaxi::findNode / findPath / findPathBetween.

Three preset emitters showcase different graph shapes:
  • makeStarter  — 2 nodes + 2 paths (round-trip)
  • makeRegion   — 4 nodes at a 500m square + 4-path
                    directed ring (NW->NE->SE->SW->NW)
  • makeContinent — 6 nodes hub-spoke + 3 perimeter
                     shortcuts; intermediate waypoints
                     climb to altitude 120m for visual
                     arc effect

CLI added (5 flags, 564 documented total now):
  --gen-taxi / --gen-taxi-region / --gen-taxi-continent
  --info-wtax / --validate-wtax

Validator catches: nodeId/pathId=0 + duplicates, empty node
name, non-finite positions, fromNodeId == toNodeId
(self-loop path), path references to non-existent nodes
(intra-format cross-reference resolution), negative
waypoint delays.
This commit is contained in:
Kelsi 2026-05-09 16:26:27 -07:00
parent efc27ba7d2
commit 3b107459b2
8 changed files with 752 additions and 0 deletions

View file

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

View file

@ -0,0 +1,113 @@
#pragma once
#include <glm/glm.hpp>
#include <cstdint>
#include <string>
#include <vector>
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<Waypoint> waypoints;
};
std::string name;
std::vector<Node> nodes;
std::vector<Path> 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

310
src/pipeline/wowee_taxi.cpp Normal file
View file

@ -0,0 +1,310 @@
#include "pipeline/wowee_taxi.hpp"
#include <cstdio>
#include <cstring>
#include <fstream>
namespace wowee {
namespace pipeline {
namespace {
constexpr char kMagic[4] = {'W', 'T', 'A', 'X'};
constexpr uint32_t kVersion = 1;
template <typename T>
void writePOD(std::ofstream& os, const T& v) {
os.write(reinterpret_cast<const char*>(&v), sizeof(T));
}
template <typename T>
bool readPOD(std::ifstream& is, T& v) {
is.read(reinterpret_cast<char*>(&v), sizeof(T));
return is.gcount() == static_cast<std::streamsize>(sizeof(T));
}
void writeStr(std::ofstream& os, const std::string& s) {
uint32_t n = static_cast<uint32_t>(s.size());
writePOD(os, n);
if (n > 0) os.write(s.data(), n);
}
bool readStr(std::ifstream& is, std::string& s) {
uint32_t n = 0;
if (!readPOD(is, n)) return false;
if (n > (1u << 20)) return false;
s.resize(n);
if (n > 0) {
is.read(s.data(), n);
if (is.gcount() != static_cast<std::streamsize>(n)) {
s.clear();
return false;
}
}
return true;
}
std::string normalizePath(std::string base) {
if (base.size() < 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<uint32_t>(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<uint32_t>(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<uint32_t>(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

View file

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

View file

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

View file

@ -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 <wgsp-base> [--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 <wtax-base> [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 <wtax-base> [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 <wtax-base> [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 <wtax-base> [--json]\n");
std::printf(" Print WTAX nodes (id / map / position / name) + paths (id / from->to / cost / waypoint count)\n");
std::printf(" --validate-wtax <wtax-base> [--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 <wow-base> [zoneName]\n");
std::printf(" Emit .wow weather schedule: clear-dominant + occasional rain + fog (forest / grassland)\n");
std::printf(" --gen-weather-arctic <wow-base> [zoneName]\n");

View file

@ -0,0 +1,301 @@
#include "cli_taxi_catalog.hpp"
#include "cli_arg_parse.hpp"
#include "cli_box_emitter.hpp"
#include "pipeline/wowee_taxi.hpp"
#include <nlohmann/json.hpp>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <string>
#include <vector>
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<uint32_t>(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<std::string> errors;
std::vector<std::string> warnings;
if (c.nodes.empty()) {
warnings.push_back("catalog has zero nodes");
}
std::vector<uint32_t> 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<uint32_t> 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

View file

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