mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-10 19:13:52 +00:00
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:
parent
2a28d3c1cd
commit
12e77e69ce
10 changed files with 838 additions and 0 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
158
include/pipeline/wowee_transit_schedule.hpp
Normal file
158
include/pipeline/wowee_transit_schedule.hpp
Normal 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
|
||||
328
src/pipeline/wowee_transit_schedule.cpp
Normal file
328
src/pipeline/wowee_transit_schedule.cpp
Normal 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
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
321
tools/editor/cli_transit_schedule_catalog.cpp
Normal file
321
tools/editor/cli_transit_schedule_catalog.cpp
Normal 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
|
||||
12
tools/editor/cli_transit_schedule_catalog.hpp
Normal file
12
tools/editor/cli_transit_schedule_catalog.hpp
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue