mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-10 02:53:51 +00:00
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:
parent
efc27ba7d2
commit
3b107459b2
8 changed files with 752 additions and 0 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
113
include/pipeline/wowee_taxi.hpp
Normal file
113
include/pipeline/wowee_taxi.hpp
Normal 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
310
src/pipeline/wowee_taxi.cpp
Normal 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
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
301
tools/editor/cli_taxi_catalog.cpp
Normal file
301
tools/editor/cli_taxi_catalog.cpp
Normal 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
|
||||
11
tools/editor/cli_taxi_catalog.hpp
Normal file
11
tools/editor/cli_taxi_catalog.hpp
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue