feat(pipeline): WTSC transit schedule catalog (128th open format)

Novel replacement for the implicit taxi/zeppelin/boat scheduling
that vanilla WoW drove from a tangle of TaxiNodes.dbc +
TaxiPath.dbc + per-zeppelin GameObject scripts + hard-coded
transport interval timers in the server's MapManager. Each WTSC
entry binds one scheduled passenger route to its origin /
destination coords, vehicle type (Taxi/Zeppelin/Boat/Mount),
departure interval, in-flight duration, capacity, and faction-
access gate.

Initially designed with magic 'WTRN' but discovered collision
with existing trainers catalog (also WTRN) — renamed to 'WTSC'
(Transit SChedule) and updated all CLI flags.

Three presets:
  --gen-trn-zeppelins  3 vanilla Horde zeppelin routes
                       (OG<->UC 240s interval, OG<->Grom'Gol,
                       UC<->Grom'Gol)
  --gen-trn-boats      3 vanilla boat routes (Auberdine<->
                       Stormwind Alliance, Menethil<->Theramore
                       Alliance, BootyBay<->Ratchet Neutral
                       cross-faction)
  --gen-trn-taxis      3 taxi gryphon/wyvern routes — capacity=0
                       indicates solo gryphon ride

CRITICAL scheduling invariant validator catches: when capacity > 0
the departureInterval MUST be >= travelDuration. A zeppelin with
interval=60s + travel=90s with capacity=40 would overflow the
vehicle pool — next zeppelin departs before prior arrives. Solo
gryphon (capacity=0) is exempt because each ride is independent.

Validator also catches: id+name+origin+destination required,
vehicleType/factionAccess range, zero intervals/travel, duplicate
routeIds, duplicate route names. Warns on same-map routes
(originMapId == destinationMapId) — preset taxi route Crossroads
to Razor Hill triggered this warning in smoke-test (both in
Kalimdor mapId=1, intentional).

Format count 127 -> 128. CLI flag count 1346 -> 1353.
This commit is contained in:
Kelsi 2026-05-10 03:54:39 -07:00
parent 2a28d3c1cd
commit 12e77e69ce
10 changed files with 838 additions and 0 deletions

View file

@ -716,6 +716,7 @@ set(WOWEE_SOURCES
src/pipeline/wowee_addon_manifest.cpp
src/pipeline/wowee_spell_pack.cpp
src/pipeline/wowee_player_movement_anim.cpp
src/pipeline/wowee_transit_schedule.cpp
src/pipeline/custom_zone_discovery.cpp
src/pipeline/dbc_layout.cpp
@ -1595,6 +1596,7 @@ add_executable(wowee_editor
tools/editor/cli_addon_manifest_catalog.cpp
tools/editor/cli_spell_pack_catalog.cpp
tools/editor/cli_player_movement_anim_catalog.cpp
tools/editor/cli_transit_schedule_catalog.cpp
tools/editor/cli_catalog_pluck.cpp
tools/editor/cli_catalog_find.cpp
tools/editor/cli_catalog_by_name.cpp
@ -1793,6 +1795,7 @@ add_executable(wowee_editor
src/pipeline/wowee_addon_manifest.cpp
src/pipeline/wowee_spell_pack.cpp
src/pipeline/wowee_player_movement_anim.cpp
src/pipeline/wowee_transit_schedule.cpp
src/pipeline/custom_zone_discovery.cpp
src/pipeline/terrain_mesh.cpp

View file

@ -0,0 +1,158 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace wowee {
namespace pipeline {
// Wowee Open Transit Schedule catalog (.wtsc) —
// novel replacement for the implicit
// taxi/zeppelin/boat scheduling that vanilla WoW
// drove from a tangle of TaxiNodes.dbc +
// TaxiPath.dbc + per-zeppelin GameObject scripts +
// hard-coded transport interval timers in the
// server's MapManager. Each WTRN entry binds one
// scheduled passenger route to its origin /
// destination coordinates, vehicle type
// (Taxi/Zeppelin/Boat/Mount), departure interval,
// in-flight duration, capacity, and faction-access
// gate.
//
// Cross-references with previously-added formats:
// WMS: originMapId / destinationMapId reference
// the WMS map catalog.
// WTAX: vehicleType=Taxi routes are derived from
// (and extend) WTAX taxi-node catalog —
// WTRN adds the scheduling layer that WTAX
// lacked.
//
// Binary layout (little-endian):
// magic[4] = "WTSC"
// version (uint32) = current 1
// nameLen + name (catalog label)
// entryCount (uint32)
// entries (each):
// routeId (uint32)
// nameLen + name
// vehicleType (uint8) — 0=Taxi /
// 1=Zeppelin /
// 2=Boat /
// 3=Mount
// factionAccess (uint8) — 0=Both /
// 1=Alliance /
// 2=Horde /
// 3=Neutral
// pad0 (uint16)
// originLen + originName
// originX (float)
// originY (float)
// originMapId (uint32)
// destinationLen + destinationName
// destinationX (float)
// destinationY (float)
// destinationMapId (uint32)
// departureIntervalSec (uint32) — period between
// successive
// departures from
// origin
// travelDurationSec (uint32) — in-flight time
// origin->dest
// capacity (uint16) — max simultaneous
// riders (0 =
// unlimited, e.g.
// solo gryphon)
// pad1 (uint16)
struct WoweeTransitSchedule {
enum VehicleType : uint8_t {
Taxi = 0,
Zeppelin = 1,
Boat = 2,
Mount = 3, // hired riding-mount
// (e.g., kodo
// caravan in vanilla
// Barrens)
};
enum FactionAccess : uint8_t {
Both = 0,
Alliance = 1,
Horde = 2,
Neutral = 3, // Booty Bay-style
// cross-faction routes
};
struct Entry {
uint32_t routeId = 0;
std::string name;
uint8_t vehicleType = Taxi;
uint8_t factionAccess = Both;
uint16_t pad0 = 0;
std::string originName;
float originX = 0.f;
float originY = 0.f;
uint32_t originMapId = 0;
std::string destinationName;
float destinationX = 0.f;
float destinationY = 0.f;
uint32_t destinationMapId = 0;
uint32_t departureIntervalSec = 0;
uint32_t travelDurationSec = 0;
uint16_t capacity = 0;
uint16_t pad1 = 0;
};
std::string name;
std::vector<Entry> entries;
bool isValid() const { return !entries.empty(); }
const Entry* findById(uint32_t routeId) const;
// Returns all routes accessible by a given faction
// mask (Alliance/Horde/Neutral all see Both routes;
// a faction-specific call also includes that
// faction's exclusive routes).
std::vector<const Entry*> findAccessibleByFaction(
uint8_t faction) const;
// Returns all routes departing from a given
// origin map. Used by the boat-dock UI to
// populate the "next departure" widget.
std::vector<const Entry*> findDeparturesFromMap(
uint32_t mapId) const;
};
class WoweeTransitScheduleLoader {
public:
static bool save(const WoweeTransitSchedule& cat,
const std::string& basePath);
static WoweeTransitSchedule load(const std::string& basePath);
static bool exists(const std::string& basePath);
// Preset emitters used by --gen-trn* variants.
//
// makeZeppelins — 3 vanilla zeppelin routes
// (Orgrimmar<->Undercity 240s
// interval / OG<->Grom'gol /
// UC<->Grom'gol). All Horde-only.
// makeBoats — 3 vanilla boat routes
// (Auberdine<->Stormwind /
// Menethil<->Theramore /
// BootyBay<->Ratchet — last is
// Neutral, both factions can
// board).
// makeTaxis — 3 taxi gryphon/wyvern routes
// (Stormwind<->Ironforge
// Alliance / Crossroads<->
// Razor Hill Horde /
// Booty Bay<->Stormwind
// Neutral).
static WoweeTransitSchedule makeZeppelins(const std::string& catalogName);
static WoweeTransitSchedule makeBoats(const std::string& catalogName);
static WoweeTransitSchedule makeTaxis(const std::string& catalogName);
};
} // namespace pipeline
} // namespace wowee

View file

@ -0,0 +1,328 @@
#include "pipeline/wowee_transit_schedule.hpp"
#include <cstdio>
#include <cstring>
#include <fstream>
namespace wowee {
namespace pipeline {
namespace {
constexpr char kMagic[4] = {'W', 'T', 'S', 'C'};
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) != ".wtsc") {
base += ".wtsc";
}
return base;
}
} // namespace
const WoweeTransitSchedule::Entry*
WoweeTransitSchedule::findById(uint32_t routeId) const {
for (const auto& e : entries)
if (e.routeId == routeId) return &e;
return nullptr;
}
std::vector<const WoweeTransitSchedule::Entry*>
WoweeTransitSchedule::findAccessibleByFaction(uint8_t faction) const {
std::vector<const Entry*> out;
for (const auto& e : entries) {
if (e.factionAccess == Both) {
out.push_back(&e);
} else if (faction != Both && e.factionAccess == faction) {
out.push_back(&e);
} else if (e.factionAccess == Neutral) {
// Neutral routes are accessible to ALL
// factions including Both — Booty Bay /
// Ratchet style.
out.push_back(&e);
}
}
return out;
}
std::vector<const WoweeTransitSchedule::Entry*>
WoweeTransitSchedule::findDeparturesFromMap(uint32_t mapId) const {
std::vector<const Entry*> out;
for (const auto& e : entries) {
if (e.originMapId == mapId) out.push_back(&e);
}
return out;
}
bool WoweeTransitScheduleLoader::save(
const WoweeTransitSchedule& 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<uint32_t>(cat.entries.size());
writePOD(os, entryCount);
for (const auto& e : cat.entries) {
writePOD(os, e.routeId);
writeStr(os, e.name);
writePOD(os, e.vehicleType);
writePOD(os, e.factionAccess);
writePOD(os, e.pad0);
writeStr(os, e.originName);
writePOD(os, e.originX);
writePOD(os, e.originY);
writePOD(os, e.originMapId);
writeStr(os, e.destinationName);
writePOD(os, e.destinationX);
writePOD(os, e.destinationY);
writePOD(os, e.destinationMapId);
writePOD(os, e.departureIntervalSec);
writePOD(os, e.travelDurationSec);
writePOD(os, e.capacity);
writePOD(os, e.pad1);
}
return os.good();
}
WoweeTransitSchedule WoweeTransitScheduleLoader::load(
const std::string& basePath) {
WoweeTransitSchedule out;
std::ifstream is(normalizePath(basePath), std::ios::binary);
if (!is) return out;
char magic[4];
is.read(magic, 4);
if (std::memcmp(magic, kMagic, 4) != 0) return out;
uint32_t version = 0;
if (!readPOD(is, version) || version != kVersion) return out;
if (!readStr(is, out.name)) return out;
uint32_t entryCount = 0;
if (!readPOD(is, entryCount)) return out;
if (entryCount > (1u << 20)) return out;
out.entries.resize(entryCount);
for (auto& e : out.entries) {
if (!readPOD(is, e.routeId)) {
out.entries.clear(); return out;
}
if (!readStr(is, e.name)) {
out.entries.clear(); return out;
}
if (!readPOD(is, e.vehicleType) ||
!readPOD(is, e.factionAccess) ||
!readPOD(is, e.pad0)) {
out.entries.clear(); return out;
}
if (!readStr(is, e.originName)) {
out.entries.clear(); return out;
}
if (!readPOD(is, e.originX) ||
!readPOD(is, e.originY) ||
!readPOD(is, e.originMapId)) {
out.entries.clear(); return out;
}
if (!readStr(is, e.destinationName)) {
out.entries.clear(); return out;
}
if (!readPOD(is, e.destinationX) ||
!readPOD(is, e.destinationY) ||
!readPOD(is, e.destinationMapId)) {
out.entries.clear(); return out;
}
if (!readPOD(is, e.departureIntervalSec) ||
!readPOD(is, e.travelDurationSec) ||
!readPOD(is, e.capacity) ||
!readPOD(is, e.pad1)) {
out.entries.clear(); return out;
}
}
return out;
}
bool WoweeTransitScheduleLoader::exists(
const std::string& basePath) {
std::ifstream is(normalizePath(basePath), std::ios::binary);
return is.good();
}
WoweeTransitSchedule WoweeTransitScheduleLoader::makeZeppelins(
const std::string& catalogName) {
using T = WoweeTransitSchedule;
WoweeTransitSchedule c;
c.name = catalogName;
auto add = [&](uint32_t id, const char* name,
const char* origin, float ox, float oy,
uint32_t omap,
const char* dest, float dx, float dy,
uint32_t dmap,
uint32_t intervalSec,
uint32_t travelSec,
uint16_t capacity) {
T::Entry e;
e.routeId = id; e.name = name;
e.vehicleType = T::Zeppelin;
e.factionAccess = T::Horde;
e.originName = origin;
e.originX = ox; e.originY = oy;
e.originMapId = omap;
e.destinationName = dest;
e.destinationX = dx; e.destinationY = dy;
e.destinationMapId = dmap;
e.departureIntervalSec = intervalSec;
e.travelDurationSec = travelSec;
e.capacity = capacity;
c.entries.push_back(e);
};
// Vanilla zeppelin tower coordinates (Orgrimmar
// map=1, Eastern Kingdoms via UC map=0,
// Stranglethorn via Grom'Gol map=0). Capacity 40
// approximates the rim platform headcount.
add(1, "Orgrimmar to Undercity",
"Orgrimmar Zeppelin Tower", 1843.f, -4416.f, 1,
"Tirisfal Zeppelin Tower", 2055.f, 273.f, 0,
240, 60, 40);
add(2, "Orgrimmar to Grom'Gol",
"Orgrimmar Zeppelin Tower", 1843.f, -4416.f, 1,
"Grom'Gol Zeppelin Tower", -12422.f, 110.f, 0,
240, 90, 40);
add(3, "Undercity to Grom'Gol",
"Tirisfal Zeppelin Tower", 2055.f, 273.f, 0,
"Grom'Gol Zeppelin Tower", -12422.f, 110.f, 0,
240, 90, 40);
return c;
}
WoweeTransitSchedule WoweeTransitScheduleLoader::makeBoats(
const std::string& catalogName) {
using T = WoweeTransitSchedule;
WoweeTransitSchedule c;
c.name = catalogName;
auto add = [&](uint32_t id, const char* name,
uint8_t faction,
const char* origin, float ox, float oy,
uint32_t omap,
const char* dest, float dx, float dy,
uint32_t dmap,
uint32_t intervalSec,
uint32_t travelSec,
uint16_t capacity) {
T::Entry e;
e.routeId = id; e.name = name;
e.vehicleType = T::Boat;
e.factionAccess = faction;
e.originName = origin;
e.originX = ox; e.originY = oy;
e.originMapId = omap;
e.destinationName = dest;
e.destinationX = dx; e.destinationY = dy;
e.destinationMapId = dmap;
e.departureIntervalSec = intervalSec;
e.travelDurationSec = travelSec;
e.capacity = capacity;
c.entries.push_back(e);
};
add(10, "Auberdine to Stormwind Harbor",
T::Alliance,
"Auberdine Dock", 6577.f, 769.f, 1,
"Stormwind Harbor", -8713.f, 1281.f, 0,
300, 90, 30);
add(11, "Menethil Harbor to Theramore",
T::Alliance,
"Menethil Harbor", -3814.f, -616.f, 0,
"Theramore Isle", -3870.f, -4533.f, 1,
300, 90, 30);
add(12, "Booty Bay to Ratchet",
T::Neutral, // both factions
// may board
"Booty Bay Dock", -14305.f, 570.f, 0,
"Ratchet Dock", -984.f, -3835.f, 1,
180, 75, 35);
return c;
}
WoweeTransitSchedule WoweeTransitScheduleLoader::makeTaxis(
const std::string& catalogName) {
using T = WoweeTransitSchedule;
WoweeTransitSchedule c;
c.name = catalogName;
auto add = [&](uint32_t id, const char* name,
uint8_t faction,
const char* origin, float ox, float oy,
uint32_t omap,
const char* dest, float dx, float dy,
uint32_t dmap,
uint32_t intervalSec,
uint32_t travelSec,
uint16_t capacity) {
T::Entry e;
e.routeId = id; e.name = name;
e.vehicleType = T::Taxi;
e.factionAccess = faction;
e.originName = origin;
e.originX = ox; e.originY = oy;
e.originMapId = omap;
e.destinationName = dest;
e.destinationX = dx; e.destinationY = dy;
e.destinationMapId = dmap;
e.departureIntervalSec = intervalSec;
e.travelDurationSec = travelSec;
e.capacity = capacity;
c.entries.push_back(e);
};
// Capacity=0 for taxis: each gryphon/wyvern is a
// solo ride, no shared seating — interval matters
// only for the visual gryphon respawn timer at the
// taxi master.
add(20, "Stormwind to Ironforge",
T::Alliance,
"Stormwind, Eastvale", -8836.f, 490.f, 0,
"Ironforge, Tinkertown", -4815.f, -1170.f, 0,
30, 200, 0);
add(21, "Crossroads to Razor Hill",
T::Horde,
"The Crossroads", -445.f, -2598.f, 1,
"Razor Hill", 314.f, -4748.f, 1,
30, 130, 0);
add(22, "Booty Bay to Stormwind",
T::Neutral,
"Booty Bay Gryphon Master", -14373.f, 555.f, 0,
"Stormwind, Eastvale", -8836.f, 490.f, 0,
30, 320, 0);
return c;
}
} // namespace pipeline
} // namespace wowee

View file

@ -392,6 +392,8 @@ const char* const kArgRequired[] = {
"--gen-phm-human", "--gen-phm-orc", "--gen-phm-undead",
"--info-wphm", "--validate-wphm",
"--export-wphm-json", "--import-wphm-json",
"--gen-trn-zeppelins", "--gen-trn-boats", "--gen-trn-taxis",
"--info-wtsc", "--validate-wtsc",
"--gen-weather-temperate", "--gen-weather-arctic",
"--gen-weather-desert", "--gen-weather-stormy",
"--gen-zone-atmosphere",

View file

@ -172,6 +172,7 @@
#include "cli_addon_manifest_catalog.hpp"
#include "cli_spell_pack_catalog.hpp"
#include "cli_player_movement_anim_catalog.hpp"
#include "cli_transit_schedule_catalog.hpp"
#include "cli_catalog_pluck.hpp"
#include "cli_catalog_find.hpp"
#include "cli_catalog_by_name.hpp"
@ -389,6 +390,7 @@ constexpr DispatchFn kDispatchTable[] = {
handleAddonManifestCatalog,
handleSpellPackCatalog,
handlePlayerMovementAnimCatalog,
handleTransitScheduleCatalog,
handleCatalogPluck,
handleCatalogFind,
handleCatalogByName,

View file

@ -130,6 +130,7 @@ constexpr FormatMagicEntry kFormats[] = {
{{'W','M','O','D'}, ".wmod", "addons", "--info-wmod", "Addon manifest catalog"},
{{'W','S','P','K'}, ".wspk", "spells", "--info-wspk", "Spell pack catalog"},
{{'W','P','H','M'}, ".wphm", "anim", "--info-wphm", "Player movement-to-animation map"},
{{'W','T','S','C'}, ".wtsc", "transit", "--info-wtsc", "Transit schedule catalog"},
{{'W','F','A','C'}, ".wfac", "factions", nullptr, "Faction catalog"},
{{'W','L','C','K'}, ".wlck", "locks", nullptr, "Lock catalog"},
{{'W','S','K','L'}, ".wskl", "skills", nullptr, "Skill catalog"},

View file

@ -2545,6 +2545,16 @@ void printUsage(const char* argv0) {
std::printf(" Export binary .wphm to a human-editable JSON sidecar (defaults to <base>.wphm.json; emits movementState as int + name string)\n");
std::printf(" --import-wphm-json <json-path> [out-base]\n");
std::printf(" Import a .wphm.json sidecar back into binary .wphm (movementState int OR \"idle\"/\"walk\"/\"run\"/\"swim\"/\"fly\"/\"sit\"/\"mount\"/\"death\")\n");
std::printf(" --gen-trn-zeppelins <wtsc-base> [name]\n");
std::printf(" Emit .wtsc 3 vanilla Horde zeppelin routes (Orgrimmar<->Undercity 240s interval, OG<->Grom'Gol, UC<->Grom'Gol)\n");
std::printf(" --gen-trn-boats <wtsc-base> [name]\n");
std::printf(" Emit .wtsc 3 vanilla boat routes (Auberdine<->Stormwind Alliance, Menethil<->Theramore Alliance, Booty Bay<->Ratchet Neutral cross-faction)\n");
std::printf(" --gen-trn-taxis <wtsc-base> [name]\n");
std::printf(" Emit .wtsc 3 taxi gryphon/wyvern routes (Stormwind<->Ironforge Alliance, Crossroads<->Razor Hill Horde, Booty Bay<->Stormwind Neutral) — capacity=0 indicates solo gryphon ride\n");
std::printf(" --info-wtsc <wtsc-base> [--json]\n");
std::printf(" Print WTSC entries (id / vehicleType / factionAccess / departureIntervalSec / travelDurationSec / capacity / name)\n");
std::printf(" --validate-wtsc <wtsc-base> [--json]\n");
std::printf(" Static checks: id+name+origin+destination required, vehicleType 0..3, factionAccess 0..3, no zero intervals/travel, no duplicate routeIds, no duplicate route names; CRITICAL scheduling invariant: when capacity > 0 the departureInterval >= travelDuration (else vehicle pool overflow — next zeppelin departs before prior arrives). Warns on same-map routes (originMapId == destinationMapId) — verify intentional\n");
std::printf(" --catalog-pluck <wXXX-file> <id> [--json]\n");
std::printf(" Extract one entry by id from any registered catalog format. Auto-detects magic, dispatches to the per-format --info-* handler internally, then prints just the matching entry. Primary-key field is auto-detected (first *Id field, or first numeric)\n");
std::printf(" --catalog-find <directory> <id> [--magic <WXXX>] [--json]\n");

View file

@ -152,6 +152,7 @@ constexpr FormatRow kFormats[] = {
{"WMOD", ".wmod", "addons", "per-addon TOC text + load-order rules","Addon manifest catalog (deps + cycle detection)"},
{"WSPK", ".wspk", "spells", "SkillLineAbility + per-spec tab order","Spell pack catalog (per-class spellbook tab layout)"},
{"WPHM", ".wphm", "anim", "implicit M2 movementState->anim map","Player movement-to-animation map (per race/gender/state)"},
{"WTSC", ".wtsc", "transit", "TaxiNodes + zeppelin GO scripts", "Transit schedule catalog (taxi/zeppelin/boat scheduled departures)"},
// Additional pipeline catalogs without the alternating
// gen/info/validate CLI surface (loaded by the engine

View file

@ -0,0 +1,321 @@
#include "cli_transit_schedule_catalog.hpp"
#include "cli_arg_parse.hpp"
#include "cli_box_emitter.hpp"
#include "pipeline/wowee_transit_schedule.hpp"
#include <nlohmann/json.hpp>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <set>
#include <string>
#include <vector>
namespace wowee {
namespace editor {
namespace cli {
namespace {
std::string stripWtscExt(std::string base) {
stripExt(base, ".wtsc");
return base;
}
const char* vehicleTypeName(uint8_t v) {
using T = wowee::pipeline::WoweeTransitSchedule;
switch (v) {
case T::Taxi: return "taxi";
case T::Zeppelin: return "zeppelin";
case T::Boat: return "boat";
case T::Mount: return "mount";
default: return "?";
}
}
const char* factionAccessName(uint8_t f) {
using T = wowee::pipeline::WoweeTransitSchedule;
switch (f) {
case T::Both: return "both";
case T::Alliance: return "alliance";
case T::Horde: return "horde";
case T::Neutral: return "neutral";
default: return "?";
}
}
bool saveOrError(const wowee::pipeline::WoweeTransitSchedule& c,
const std::string& base, const char* cmd) {
if (!wowee::pipeline::WoweeTransitScheduleLoader::save(c, base)) {
std::fprintf(stderr, "%s: failed to save %s.wtsc\n",
cmd, base.c_str());
return false;
}
return true;
}
void printGenSummary(const wowee::pipeline::WoweeTransitSchedule& c,
const std::string& base) {
std::printf("Wrote %s.wtsc\n", base.c_str());
std::printf(" catalog : %s\n", c.name.c_str());
std::printf(" routes : %zu\n", c.entries.size());
}
int handleGenZeppelins(int& i, int argc, char** argv) {
std::string base = argv[++i];
std::string name = "VanillaZeppelins";
if (parseOptArg(i, argc, argv)) name = argv[++i];
base = stripWtscExt(base);
auto c = wowee::pipeline::WoweeTransitScheduleLoader::
makeZeppelins(name);
if (!saveOrError(c, base, "gen-trn-zeppelins")) return 1;
printGenSummary(c, base);
return 0;
}
int handleGenBoats(int& i, int argc, char** argv) {
std::string base = argv[++i];
std::string name = "VanillaBoats";
if (parseOptArg(i, argc, argv)) name = argv[++i];
base = stripWtscExt(base);
auto c = wowee::pipeline::WoweeTransitScheduleLoader::
makeBoats(name);
if (!saveOrError(c, base, "gen-trn-boats")) return 1;
printGenSummary(c, base);
return 0;
}
int handleGenTaxis(int& i, int argc, char** argv) {
std::string base = argv[++i];
std::string name = "VanillaTaxis";
if (parseOptArg(i, argc, argv)) name = argv[++i];
base = stripWtscExt(base);
auto c = wowee::pipeline::WoweeTransitScheduleLoader::
makeTaxis(name);
if (!saveOrError(c, base, "gen-trn-taxis")) 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 = stripWtscExt(base);
if (!wowee::pipeline::WoweeTransitScheduleLoader::exists(base)) {
std::fprintf(stderr, "WTSC not found: %s.wtsc\n",
base.c_str());
return 1;
}
auto c = wowee::pipeline::WoweeTransitScheduleLoader::load(base);
if (jsonOut) {
nlohmann::json j;
j["wtsc"] = base + ".wtsc";
j["name"] = c.name;
j["count"] = c.entries.size();
nlohmann::json arr = nlohmann::json::array();
for (const auto& e : c.entries) {
arr.push_back({
{"routeId", e.routeId},
{"name", e.name},
{"vehicleType", e.vehicleType},
{"vehicleTypeName",
vehicleTypeName(e.vehicleType)},
{"factionAccess", e.factionAccess},
{"factionAccessName",
factionAccessName(e.factionAccess)},
{"originName", e.originName},
{"originX", e.originX},
{"originY", e.originY},
{"originMapId", e.originMapId},
{"destinationName", e.destinationName},
{"destinationX", e.destinationX},
{"destinationY", e.destinationY},
{"destinationMapId", e.destinationMapId},
{"departureIntervalSec",
e.departureIntervalSec},
{"travelDurationSec", e.travelDurationSec},
{"capacity", e.capacity},
});
}
j["entries"] = arr;
std::printf("%s\n", j.dump(2).c_str());
return 0;
}
std::printf("WTSC: %s.wtsc\n", base.c_str());
std::printf(" catalog : %s\n", c.name.c_str());
std::printf(" routes : %zu\n", c.entries.size());
if (c.entries.empty()) return 0;
std::printf(" id vehicle fact intv travel cap name\n");
for (const auto& e : c.entries) {
std::printf(" %4u %-9s %-8s %4us %4us %4u %s\n",
e.routeId,
vehicleTypeName(e.vehicleType),
factionAccessName(e.factionAccess),
e.departureIntervalSec,
e.travelDurationSec,
e.capacity, e.name.c_str());
}
return 0;
}
int handleValidate(int& i, int argc, char** argv) {
std::string base = argv[++i];
bool jsonOut = consumeJsonFlag(i, argc, argv);
base = stripWtscExt(base);
if (!wowee::pipeline::WoweeTransitScheduleLoader::exists(base)) {
std::fprintf(stderr,
"validate-wtsc: WTSC not found: %s.wtsc\n",
base.c_str());
return 1;
}
auto c = wowee::pipeline::WoweeTransitScheduleLoader::load(base);
std::vector<std::string> errors;
std::vector<std::string> warnings;
if (c.entries.empty()) {
warnings.push_back("catalog has zero entries");
}
std::set<uint32_t> idsSeen;
std::set<std::string> namesSeen;
for (size_t k = 0; k < c.entries.size(); ++k) {
const auto& e = c.entries[k];
std::string ctx = "entry " + std::to_string(k) +
" (id=" + std::to_string(e.routeId);
if (!e.name.empty()) ctx += " " + e.name;
ctx += ")";
if (e.routeId == 0)
errors.push_back(ctx + ": routeId is 0");
if (e.name.empty())
errors.push_back(ctx + ": name is empty");
if (e.originName.empty())
errors.push_back(ctx + ": originName is empty");
if (e.destinationName.empty())
errors.push_back(ctx +
": destinationName is empty");
if (e.vehicleType > 3) {
errors.push_back(ctx + ": vehicleType " +
std::to_string(e.vehicleType) +
" out of range (0..3)");
}
if (e.factionAccess > 3) {
errors.push_back(ctx + ": factionAccess " +
std::to_string(e.factionAccess) +
" out of range (0..3)");
}
// Critical scheduling invariant: a new
// departure cannot leave before the previous
// one has arrived if capacity is finite — an
// interval shorter than travel would
// overflow the route's vehicle pool. (This
// doesn't apply to capacity==0 = solo
// gryphon, where each ride is independent.)
if (e.capacity > 0 &&
e.departureIntervalSec > 0 &&
e.travelDurationSec > 0 &&
e.departureIntervalSec < e.travelDurationSec) {
errors.push_back(ctx +
": departureIntervalSec=" +
std::to_string(e.departureIntervalSec) +
" < travelDurationSec=" +
std::to_string(e.travelDurationSec) +
" with finite capacity — vehicle pool "
"overflow (next zeppelin departs "
"before prior arrives)");
}
if (e.departureIntervalSec == 0) {
errors.push_back(ctx +
": departureIntervalSec is 0 (route "
"would never depart)");
}
if (e.travelDurationSec == 0) {
errors.push_back(ctx +
": travelDurationSec is 0 (route would "
"instant-teleport, not a transit)");
}
// Same-map vehicle: not an error (some
// vanilla flightpaths cross only intra-zone)
// but is worth flagging — the reader may want
// to verify this is intentional.
if (e.originMapId == e.destinationMapId &&
e.originMapId != 0) {
warnings.push_back(ctx +
": originMapId == destinationMapId=" +
std::to_string(e.originMapId) +
" — same-map route, verify intentional");
}
// No identical (origin, destination) pair within
// a single catalog — would be a duplicate route.
if (!e.name.empty() &&
!namesSeen.insert(e.name).second) {
errors.push_back(ctx +
": duplicate route name '" + e.name +
"' — UI dispatch would route ambiguously");
}
if (!idsSeen.insert(e.routeId).second) {
errors.push_back(ctx + ": duplicate routeId");
}
}
bool ok = errors.empty();
if (jsonOut) {
nlohmann::json j;
j["wtsc"] = base + ".wtsc";
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-wtsc: %s.wtsc\n", base.c_str());
if (ok && warnings.empty()) {
std::printf(" OK — %zu routes, all routeIds + "
"names unique, vehicleType 0..3, "
"factionAccess 0..3, no zero "
"intervals/travel, no scheduling "
"overflow (interval >= travel where "
"capacity is finite)\n",
c.entries.size());
return 0;
}
if (!warnings.empty()) {
std::printf(" warnings (%zu):\n", warnings.size());
for (const auto& w : warnings)
std::printf(" - %s\n", w.c_str());
}
if (!errors.empty()) {
std::printf(" ERRORS (%zu):\n", errors.size());
for (const auto& e : errors)
std::printf(" - %s\n", e.c_str());
}
return ok ? 0 : 1;
}
} // namespace
bool handleTransitScheduleCatalog(int& i, int argc, char** argv,
int& outRc) {
if (std::strcmp(argv[i], "--gen-trn-zeppelins") == 0 &&
i + 1 < argc) {
outRc = handleGenZeppelins(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--gen-trn-boats") == 0 &&
i + 1 < argc) {
outRc = handleGenBoats(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--gen-trn-taxis") == 0 &&
i + 1 < argc) {
outRc = handleGenTaxis(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--info-wtsc") == 0 && i + 1 < argc) {
outRc = handleInfo(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--validate-wtsc") == 0 &&
i + 1 < argc) {
outRc = handleValidate(i, argc, argv); return true;
}
return false;
}
} // namespace cli
} // namespace editor
} // namespace wowee

View file

@ -0,0 +1,12 @@
#pragma once
namespace wowee {
namespace editor {
namespace cli {
bool handleTransitScheduleCatalog(int& i, int argc, char** argv,
int& outRc);
} // namespace cli
} // namespace editor
} // namespace wowee