diff --git a/CMakeLists.txt b/CMakeLists.txt index 18281c23..1fe1fb8f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -630,6 +630,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_channels.cpp src/pipeline/wowee_cinematics.cpp src/pipeline/wowee_glyphs.cpp + src/pipeline/wowee_vehicles.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1406,6 +1407,7 @@ add_executable(wowee_editor tools/editor/cli_channels_catalog.cpp tools/editor/cli_cinematics_catalog.cpp tools/editor/cli_glyphs_catalog.cpp + tools/editor/cli_vehicles_catalog.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp @@ -1514,6 +1516,7 @@ add_executable(wowee_editor src/pipeline/wowee_channels.cpp src/pipeline/wowee_cinematics.cpp src/pipeline/wowee_glyphs.cpp + src/pipeline/wowee_vehicles.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_vehicles.hpp b/include/pipeline/wowee_vehicles.hpp new file mode 100644 index 00000000..1ffabffb --- /dev/null +++ b/include/pipeline/wowee_vehicles.hpp @@ -0,0 +1,153 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Vehicle catalog (.wvhc) — novel replacement for +// Blizzard's Vehicle.dbc + VehicleSeat.dbc + the +// AzerothCore-style vehicle_template SQL tables. The 43rd +// open format added to the editor. +// +// Defines drivable / rideable vehicles: tanks, demolishers, +// motorcycles, gryphon mounts with passengers, choppers, +// siege weapons, multi-passenger transports. Each vehicle +// pairs a creature template (the rendered model) with a +// fixed seat layout — driver / passenger seats with their +// own attachment points, control flags, and per-seat +// abilities mounted to the action bar. +// +// Cross-references with previously-added formats: +// WVHC.entry.creatureId → WCRT.creatureId +// (the rendered vehicle model) +// WVHC.entry.flightCapabilityId → WMNT.mountId (optional — +// shared fly-speed table for +// flying vehicles) +// WVHC.seat.controlSpellId → WSPL.spellId +// (action-bar slot for the seat) +// WVHC.seat.exitSpellId → WSPL.spellId +// (eject ability) +// +// Binary layout (little-endian): +// magic[4] = "WVHC" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// vehicleId (uint32) +// creatureId (uint32) +// nameLen + name +// descLen + description +// vehicleKind (uint8) / movementKind (uint8) / +// seatCount (uint8) / pad[1] +// turnSpeed (float) +// pitchSpeed (float) +// flightCapabilityId (uint32) — 0 = ground vehicle +// powerType (uint8) / pad[3] +// maxPower (uint32) +// seats (seatCount × VehicleSeat): +// seatIndex (uint8) / seatFlags (uint8) / +// attachmentId (uint8) / pad[1] +// controlSpellId (uint32) +// exitSpellId (uint32) +// passengerYaw (float) +struct WoweeVehicle { + enum VehicleKind : uint8_t { + Mount = 0, // 1-rider creature mount + Chopper = 1, // motorcycle / 2-seater hog + Tank = 2, // tracked siege tank + Demolisher = 3, // catapult / trebuchet + Gunship = 4, // multi-seat aerial / sea craft + FlyingMount = 5, // single-seat flying mount + TransportRail = 6, // tram / zeppelin (rail-bound) + SiegeWeapon = 7, // ballista / cannon emplacement + }; + + enum MovementKind : uint8_t { + Ground = 0, + Air = 1, + Water = 2, + Submerged = 3, // operates fully underwater + AmphibiousAW = 4, // air + water + AmphibiousGW = 5, // ground + water + }; + + enum PowerType : uint8_t { + Mana = 0, + Energy = 1, // chopper boost / standard energy bar + Pyrite = 2, // Ulduar-specific charge resource + Heat = 3, // tank / cannon overheat resource + None = 4, // simple mounts have no power bar + }; + + // Per-seat flags — seatFlags is a bitmask. + static constexpr uint8_t kSeatDriver = 0x01; + static constexpr uint8_t kSeatGunner = 0x02; + static constexpr uint8_t kSeatPassenger = 0x04; + static constexpr uint8_t kSeatHidesPlayer = 0x08; // model swap + static constexpr uint8_t kSeatNoEjectByCC = 0x10; // cc immune + + struct Seat { + uint8_t seatIndex = 0; + uint8_t seatFlags = kSeatPassenger; + uint8_t attachmentId = 0; // M2 attachment slot + uint32_t controlSpellId = 0; // WSPL cross-ref + uint32_t exitSpellId = 0; // WSPL cross-ref + float passengerYaw = 0.0f; // facing offset (radians) + }; + + struct Entry { + uint32_t vehicleId = 0; + uint32_t creatureId = 0; // WCRT cross-ref + std::string name; + std::string description; + uint8_t vehicleKind = Mount; + uint8_t movementKind = Ground; + float turnSpeed = 3.14f; // radians/sec + float pitchSpeed = 1.0f; // radians/sec + uint32_t flightCapabilityId = 0; // WMNT cross-ref or 0 + uint8_t powerType = None; + uint32_t maxPower = 100; + std::vector seats; + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t vehicleId) const; + + static const char* vehicleKindName(uint8_t k); + static const char* movementKindName(uint8_t m); + static const char* powerTypeName(uint8_t p); +}; + +class WoweeVehicleLoader { +public: + static bool save(const WoweeVehicle& cat, + const std::string& basePath); + static WoweeVehicle load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-vehicles* variants. + // + // makeStarter — 3 vehicles: 1-seat chopper, 2-seat + // flying mount, 1-seat ground tank with + // driver-only seat config. + // makeSiege — 3 siege weapons (Demolisher / Catapult / + // Cannon) with multi-seat layouts and + // per-seat control spells. + // makeFlying — 3 flying mounts (Wyrm / Drake / Gryphon) + // with FlightCapability cross-refs to + // WMNT entries. + static WoweeVehicle makeStarter(const std::string& catalogName); + static WoweeVehicle makeSiege(const std::string& catalogName); + static WoweeVehicle makeFlying(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_vehicles.cpp b/src/pipeline/wowee_vehicles.cpp new file mode 100644 index 00000000..abf2ae89 --- /dev/null +++ b/src/pipeline/wowee_vehicles.cpp @@ -0,0 +1,355 @@ +#include "pipeline/wowee_vehicles.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'V', 'H', 'C'}; +constexpr uint32_t kVersion = 1; + +template +void writePOD(std::ofstream& os, const T& v) { + os.write(reinterpret_cast(&v), sizeof(T)); +} + +template +bool readPOD(std::ifstream& is, T& v) { + is.read(reinterpret_cast(&v), sizeof(T)); + return is.gcount() == static_cast(sizeof(T)); +} + +void writeStr(std::ofstream& os, const std::string& s) { + uint32_t n = static_cast(s.size()); + writePOD(os, n); + if (n > 0) os.write(s.data(), n); +} + +bool readStr(std::ifstream& is, std::string& s) { + uint32_t n = 0; + if (!readPOD(is, n)) return false; + if (n > (1u << 20)) return false; + s.resize(n); + if (n > 0) { + is.read(s.data(), n); + if (is.gcount() != static_cast(n)) { + s.clear(); + return false; + } + } + return true; +} + +std::string normalizePath(std::string base) { + if (base.size() < 5 || base.substr(base.size() - 5) != ".wvhc") { + base += ".wvhc"; + } + return base; +} + +} // namespace + +const WoweeVehicle::Entry* +WoweeVehicle::findById(uint32_t vehicleId) const { + for (const auto& e : entries) if (e.vehicleId == vehicleId) return &e; + return nullptr; +} + +const char* WoweeVehicle::vehicleKindName(uint8_t k) { + switch (k) { + case Mount: return "mount"; + case Chopper: return "chopper"; + case Tank: return "tank"; + case Demolisher: return "demolisher"; + case Gunship: return "gunship"; + case FlyingMount: return "flying-mount"; + case TransportRail: return "transport-rail"; + case SiegeWeapon: return "siege-weapon"; + default: return "unknown"; + } +} + +const char* WoweeVehicle::movementKindName(uint8_t m) { + switch (m) { + case Ground: return "ground"; + case Air: return "air"; + case Water: return "water"; + case Submerged: return "submerged"; + case AmphibiousAW: return "air+water"; + case AmphibiousGW: return "ground+water"; + default: return "unknown"; + } +} + +const char* WoweeVehicle::powerTypeName(uint8_t p) { + switch (p) { + case Mana: return "mana"; + case Energy: return "energy"; + case Pyrite: return "pyrite"; + case Heat: return "heat"; + case None: return "none"; + default: return "unknown"; + } +} + +bool WoweeVehicleLoader::save(const WoweeVehicle& 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(cat.entries.size()); + writePOD(os, entryCount); + for (const auto& e : cat.entries) { + writePOD(os, e.vehicleId); + writePOD(os, e.creatureId); + writeStr(os, e.name); + writeStr(os, e.description); + writePOD(os, e.vehicleKind); + writePOD(os, e.movementKind); + uint8_t seatCount = static_cast(e.seats.size()); + writePOD(os, seatCount); + uint8_t pad = 0; + writePOD(os, pad); + writePOD(os, e.turnSpeed); + writePOD(os, e.pitchSpeed); + writePOD(os, e.flightCapabilityId); + writePOD(os, e.powerType); + uint8_t pad3[3] = {0, 0, 0}; + os.write(reinterpret_cast(pad3), 3); + writePOD(os, e.maxPower); + for (const auto& s : e.seats) { + writePOD(os, s.seatIndex); + writePOD(os, s.seatFlags); + writePOD(os, s.attachmentId); + uint8_t spad = 0; + writePOD(os, spad); + writePOD(os, s.controlSpellId); + writePOD(os, s.exitSpellId); + writePOD(os, s.passengerYaw); + } + } + return os.good(); +} + +WoweeVehicle WoweeVehicleLoader::load(const std::string& basePath) { + WoweeVehicle 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.vehicleId) || + !readPOD(is, e.creatureId)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.name) || !readStr(is, e.description)) { + out.entries.clear(); return out; + } + uint8_t seatCount = 0; + if (!readPOD(is, e.vehicleKind) || + !readPOD(is, e.movementKind) || + !readPOD(is, seatCount)) { + out.entries.clear(); return out; + } + uint8_t pad = 0; + if (!readPOD(is, pad)) { out.entries.clear(); return out; } + if (!readPOD(is, e.turnSpeed) || + !readPOD(is, e.pitchSpeed) || + !readPOD(is, e.flightCapabilityId) || + !readPOD(is, e.powerType)) { + out.entries.clear(); return out; + } + uint8_t pad3[3]; + is.read(reinterpret_cast(pad3), 3); + if (is.gcount() != 3) { out.entries.clear(); return out; } + if (!readPOD(is, e.maxPower)) { + out.entries.clear(); return out; + } + if (seatCount > 64) { out.entries.clear(); return out; } + e.seats.resize(seatCount); + for (auto& s : e.seats) { + if (!readPOD(is, s.seatIndex) || + !readPOD(is, s.seatFlags) || + !readPOD(is, s.attachmentId)) { + out.entries.clear(); return out; + } + uint8_t spad = 0; + if (!readPOD(is, spad)) { out.entries.clear(); return out; } + if (!readPOD(is, s.controlSpellId) || + !readPOD(is, s.exitSpellId) || + !readPOD(is, s.passengerYaw)) { + out.entries.clear(); return out; + } + } + } + return out; +} + +bool WoweeVehicleLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeVehicle WoweeVehicleLoader::makeStarter(const std::string& catalogName) { + WoweeVehicle c; + c.name = catalogName; + { + // Mechano-Hog: 2-seat ground chopper. + WoweeVehicle::Entry e; + e.vehicleId = 1; e.creatureId = 28829; + e.name = "Mechano-Hog"; + e.description = "Engineering chopper — driver + 1 passenger."; + e.vehicleKind = WoweeVehicle::Chopper; + e.movementKind = WoweeVehicle::Ground; + e.powerType = WoweeVehicle::Energy; + e.maxPower = 100; + WoweeVehicle::Seat driver; + driver.seatIndex = 0; driver.seatFlags = WoweeVehicle::kSeatDriver; + driver.attachmentId = 0; driver.controlSpellId = 0; + e.seats.push_back(driver); + WoweeVehicle::Seat passenger; + passenger.seatIndex = 1; + passenger.seatFlags = WoweeVehicle::kSeatPassenger; + passenger.attachmentId = 1; passenger.controlSpellId = 0; + e.seats.push_back(passenger); + c.entries.push_back(e); + } + { + // Wind Rider: 1-seat flying mount with WMNT cross-ref. + WoweeVehicle::Entry e; + e.vehicleId = 2; e.creatureId = 1908; + e.name = "Wind Rider"; + e.description = "Horde flying mount — single rider."; + e.vehicleKind = WoweeVehicle::FlyingMount; + e.movementKind = WoweeVehicle::Air; + // flightCapabilityId 1 matches WMNT.makeStarter mountId. + e.flightCapabilityId = 1; + e.powerType = WoweeVehicle::None; + e.maxPower = 0; + e.turnSpeed = 4.0f; e.pitchSpeed = 1.5f; + WoweeVehicle::Seat driver; + driver.seatIndex = 0; + driver.seatFlags = WoweeVehicle::kSeatDriver | + WoweeVehicle::kSeatHidesPlayer; + e.seats.push_back(driver); + c.entries.push_back(e); + } + { + // Salvaged Tank: 1-seat siege ground tank. + WoweeVehicle::Entry e; + e.vehicleId = 3; e.creatureId = 33060; + e.name = "Salvaged Tank"; + e.description = "Ulduar-style siege tank."; + e.vehicleKind = WoweeVehicle::Tank; + e.movementKind = WoweeVehicle::Ground; + e.powerType = WoweeVehicle::Heat; + e.maxPower = 100; + WoweeVehicle::Seat driver; + driver.seatIndex = 0; + driver.seatFlags = WoweeVehicle::kSeatDriver | + WoweeVehicle::kSeatNoEjectByCC; + driver.controlSpellId = 62286; // ram ability + e.seats.push_back(driver); + c.entries.push_back(e); + } + return c; +} + +WoweeVehicle WoweeVehicleLoader::makeSiege(const std::string& catalogName) { + WoweeVehicle c; + c.name = catalogName; + auto add = [&](uint32_t vid, uint32_t cid, const char* name, + uint8_t kind, uint8_t power, + uint32_t controlSp, uint32_t gunnerSp, + const char* desc) { + WoweeVehicle::Entry e; + e.vehicleId = vid; e.creatureId = cid; + e.name = name; e.description = desc; + e.vehicleKind = kind; + e.movementKind = WoweeVehicle::Ground; + e.powerType = power; + e.maxPower = (power == WoweeVehicle::Heat) ? 100 : 50; + WoweeVehicle::Seat driver; + driver.seatIndex = 0; + driver.seatFlags = WoweeVehicle::kSeatDriver; + driver.controlSpellId = controlSp; + e.seats.push_back(driver); + if (gunnerSp != 0) { + WoweeVehicle::Seat gunner; + gunner.seatIndex = 1; + gunner.seatFlags = WoweeVehicle::kSeatGunner; + gunner.attachmentId = 1; + gunner.controlSpellId = gunnerSp; + e.seats.push_back(gunner); + } + c.entries.push_back(e); + }; + add(100, 28593, "Demolisher", + WoweeVehicle::Demolisher, WoweeVehicle::Pyrite, + 50990, 50652, "2-seat catapult — driver steers, " + "gunner launches boulders."); + add(101, 28781, "Glaive Thrower", + WoweeVehicle::SiegeWeapon, WoweeVehicle::Pyrite, + 53908, 0, "Single-seat ballista — fires armor-piercing " + "glaives."); + add(102, 33113, "Salvaged Cannon", + WoweeVehicle::SiegeWeapon, WoweeVehicle::Heat, + 62307, 0, "Stationary cannon — overheats on rapid fire."); + return c; +} + +WoweeVehicle WoweeVehicleLoader::makeFlying(const std::string& catalogName) { + WoweeVehicle c; + c.name = catalogName; + auto add = [&](uint32_t vid, uint32_t cid, const char* name, + uint32_t flightCap, uint8_t passengerSeats, + const char* desc) { + WoweeVehicle::Entry e; + e.vehicleId = vid; e.creatureId = cid; + e.name = name; e.description = desc; + e.vehicleKind = WoweeVehicle::FlyingMount; + e.movementKind = WoweeVehicle::Air; + e.flightCapabilityId = flightCap; + e.powerType = WoweeVehicle::None; + e.turnSpeed = 4.0f; e.pitchSpeed = 1.5f; + WoweeVehicle::Seat driver; + driver.seatIndex = 0; + driver.seatFlags = WoweeVehicle::kSeatDriver | + WoweeVehicle::kSeatHidesPlayer; + e.seats.push_back(driver); + for (uint8_t k = 0; k < passengerSeats; ++k) { + WoweeVehicle::Seat p; + p.seatIndex = static_cast(k + 1); + p.seatFlags = WoweeVehicle::kSeatPassenger; + p.attachmentId = static_cast(k + 1); + e.seats.push_back(p); + } + c.entries.push_back(e); + }; + // flightCapabilityIds 1 / 2 / 3 match WMNT.makeStarter + // mountIds (Wind Rider / Gryphon / Drake). + add(200, 1908, "Wind Rider", 1, 0, + "Single-seat horde flying mount."); + add(201, 478, "Storm Gryphon", 2, 0, + "Single-seat alliance flying mount."); + add(202, 30414, "Twilight Drake", 3, 1, + "2-seat drake — driver + 1 passenger."); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 444fedbc..880d7008 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -124,6 +124,8 @@ const char* const kArgRequired[] = { "--export-wcms-json", "--import-wcms-json", "--gen-glyphs", "--gen-glyphs-warrior", "--gen-glyphs-universal", "--info-wgly", "--validate-wgly", + "--gen-vehicles", "--gen-vehicles-siege", "--gen-vehicles-flying", + "--info-wvhc", "--validate-wvhc", "--gen-weather-temperate", "--gen-weather-arctic", "--gen-weather-desert", "--gen-weather-stormy", "--gen-zone-atmosphere", diff --git a/tools/editor/cli_dispatch.cpp b/tools/editor/cli_dispatch.cpp index ddd339b3..34b885f4 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -70,6 +70,7 @@ #include "cli_channels_catalog.hpp" #include "cli_cinematics_catalog.hpp" #include "cli_glyphs_catalog.hpp" +#include "cli_vehicles_catalog.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -181,6 +182,7 @@ constexpr DispatchFn kDispatchTable[] = { handleChannelsCatalog, handleCinematicsCatalog, handleGlyphsCatalog, + handleVehiclesCatalog, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index d966508e..9b7cbc27 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1301,6 +1301,16 @@ void printUsage(const char* argv0) { std::printf(" Print WGLY entries (id / type / spellId / itemId / classMask / requiredLevel / name)\n"); std::printf(" --validate-wgly [--json]\n"); std::printf(" Static checks: id>0+unique, name+spellId not empty, type 0..2, classMask>0, level<25 warning, missing-itemId warning\n"); + std::printf(" --gen-vehicles [name]\n"); + std::printf(" Emit .wvhc starter: 3 vehicles (chopper / wind rider / salvaged tank) covering ground+air+siege roles\n"); + std::printf(" --gen-vehicles-siege [name]\n"); + std::printf(" Emit .wvhc 3 siege weapons (Demolisher 2-seat / Glaive Thrower / Salvaged Cannon) with control spellIds\n"); + std::printf(" --gen-vehicles-flying [name]\n"); + std::printf(" Emit .wvhc 3 flying mounts (Wind Rider / Storm Gryphon / Twilight Drake) cross-ref WMNT flightCapabilityIds\n"); + std::printf(" --info-wvhc [--json]\n"); + std::printf(" Print WVHC entries with seat layout (id / creature / kind / movement / power / seat count / name)\n"); + std::printf(" --validate-wvhc [--json]\n"); + std::printf(" Static checks: id>0+unique, creatureId>0, kind/movement/power in range, exactly 1 driver seat, no duplicate seatIndex\n"); std::printf(" --gen-weather-temperate [zoneName]\n"); std::printf(" Emit .wow weather schedule: clear-dominant + occasional rain + fog (forest / grassland)\n"); std::printf(" --gen-weather-arctic [zoneName]\n"); diff --git a/tools/editor/cli_vehicles_catalog.cpp b/tools/editor/cli_vehicles_catalog.cpp new file mode 100644 index 00000000..55e39612 --- /dev/null +++ b/tools/editor/cli_vehicles_catalog.cpp @@ -0,0 +1,289 @@ +#include "cli_vehicles_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_vehicles.hpp" +#include + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWvhcExt(std::string base) { + stripExt(base, ".wvhc"); + return base; +} + +bool saveOrError(const wowee::pipeline::WoweeVehicle& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeVehicleLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wvhc\n", + cmd, base.c_str()); + return false; + } + return true; +} + +size_t totalSeats(const wowee::pipeline::WoweeVehicle& c) { + size_t n = 0; + for (const auto& e : c.entries) n += e.seats.size(); + return n; +} + +void printGenSummary(const wowee::pipeline::WoweeVehicle& c, + const std::string& base) { + std::printf("Wrote %s.wvhc\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" vehicles : %zu (%zu seats total)\n", + c.entries.size(), totalSeats(c)); +} + +int handleGenStarter(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "StarterVehicles"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWvhcExt(base); + auto c = wowee::pipeline::WoweeVehicleLoader::makeStarter(name); + if (!saveOrError(c, base, "gen-vehicles")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenSiege(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "SiegeVehicles"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWvhcExt(base); + auto c = wowee::pipeline::WoweeVehicleLoader::makeSiege(name); + if (!saveOrError(c, base, "gen-vehicles-siege")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenFlying(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "FlyingVehicles"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWvhcExt(base); + auto c = wowee::pipeline::WoweeVehicleLoader::makeFlying(name); + if (!saveOrError(c, base, "gen-vehicles-flying")) 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 = stripWvhcExt(base); + if (!wowee::pipeline::WoweeVehicleLoader::exists(base)) { + std::fprintf(stderr, "WVHC not found: %s.wvhc\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeVehicleLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wvhc"] = base + ".wvhc"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + nlohmann::json seats = nlohmann::json::array(); + for (const auto& s : e.seats) { + seats.push_back({ + {"seatIndex", s.seatIndex}, + {"seatFlags", s.seatFlags}, + {"attachmentId", s.attachmentId}, + {"controlSpellId", s.controlSpellId}, + {"exitSpellId", s.exitSpellId}, + {"passengerYaw", s.passengerYaw}, + }); + } + arr.push_back({ + {"vehicleId", e.vehicleId}, + {"creatureId", e.creatureId}, + {"name", e.name}, + {"description", e.description}, + {"vehicleKind", e.vehicleKind}, + {"vehicleKindName", wowee::pipeline::WoweeVehicle::vehicleKindName(e.vehicleKind)}, + {"movementKind", e.movementKind}, + {"movementKindName", wowee::pipeline::WoweeVehicle::movementKindName(e.movementKind)}, + {"turnSpeed", e.turnSpeed}, + {"pitchSpeed", e.pitchSpeed}, + {"flightCapabilityId", e.flightCapabilityId}, + {"powerType", e.powerType}, + {"powerTypeName", wowee::pipeline::WoweeVehicle::powerTypeName(e.powerType)}, + {"maxPower", e.maxPower}, + {"seats", seats}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WVHC: %s.wvhc\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" vehicles : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id creature kind move power seats name\n"); + for (const auto& e : c.entries) { + std::printf(" %4u %5u %-13s %-7s %-7s %3zu %s\n", + e.vehicleId, e.creatureId, + wowee::pipeline::WoweeVehicle::vehicleKindName(e.vehicleKind), + wowee::pipeline::WoweeVehicle::movementKindName(e.movementKind), + wowee::pipeline::WoweeVehicle::powerTypeName(e.powerType), + e.seats.size(), 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 = stripWvhcExt(base); + if (!wowee::pipeline::WoweeVehicleLoader::exists(base)) { + std::fprintf(stderr, + "validate-wvhc: WVHC not found: %s.wvhc\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeVehicleLoader::load(base); + std::vector errors; + std::vector warnings; + if (c.entries.empty()) { + warnings.push_back("catalog has zero entries"); + } + std::vector idsSeen; + 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.vehicleId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.vehicleId == 0) errors.push_back(ctx + ": vehicleId is 0"); + if (e.name.empty()) errors.push_back(ctx + ": name is empty"); + if (e.creatureId == 0) + errors.push_back(ctx + ": creatureId is 0 " + "(no rendered model)"); + if (e.vehicleKind > wowee::pipeline::WoweeVehicle::SiegeWeapon) { + errors.push_back(ctx + ": vehicleKind " + + std::to_string(e.vehicleKind) + " not in 0..7"); + } + if (e.movementKind > wowee::pipeline::WoweeVehicle::AmphibiousGW) { + errors.push_back(ctx + ": movementKind " + + std::to_string(e.movementKind) + " not in 0..5"); + } + if (e.powerType > wowee::pipeline::WoweeVehicle::None) { + errors.push_back(ctx + ": powerType " + + std::to_string(e.powerType) + " not in 0..4"); + } + if (e.seats.empty()) { + errors.push_back(ctx + + ": no seats (vehicle has no rideable position)"); + } + // Flying vehicles MUST be on Air or AmphibiousAW + // movement, otherwise they fall through the world. + if ((e.vehicleKind == wowee::pipeline::WoweeVehicle::FlyingMount || + e.vehicleKind == wowee::pipeline::WoweeVehicle::Gunship) && + e.movementKind != wowee::pipeline::WoweeVehicle::Air && + e.movementKind != wowee::pipeline::WoweeVehicle::AmphibiousAW) { + errors.push_back(ctx + + ": flying vehicle without Air/AmphibiousAW movement " + "(would fall through world)"); + } + // Driver-flag exclusivity check. + int driverCount = 0; + std::vector seatIdxSeen; + for (size_t si = 0; si < e.seats.size(); ++si) { + const auto& s = e.seats[si]; + if (s.seatFlags & wowee::pipeline::WoweeVehicle::kSeatDriver) { + ++driverCount; + } + for (uint8_t prev : seatIdxSeen) { + if (prev == s.seatIndex) { + errors.push_back(ctx + ": seat[" + + std::to_string(si) + "] duplicate seatIndex=" + + std::to_string(s.seatIndex)); + break; + } + } + seatIdxSeen.push_back(s.seatIndex); + } + if (driverCount == 0) { + warnings.push_back(ctx + + ": no seat marked kSeatDriver " + "(no one can steer this vehicle)"); + } + if (driverCount > 1) { + errors.push_back(ctx + + ": multiple seats marked kSeatDriver (driverCount=" + + std::to_string(driverCount) + ")"); + } + for (uint32_t prev : idsSeen) { + if (prev == e.vehicleId) { + errors.push_back(ctx + ": duplicate vehicleId"); + break; + } + } + idsSeen.push_back(e.vehicleId); + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wvhc"] = base + ".wvhc"; + 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-wvhc: %s.wvhc\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu vehicles, %zu seats, all vehicleIds unique\n", + c.entries.size(), totalSeats(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 handleVehiclesCatalog(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--gen-vehicles") == 0 && i + 1 < argc) { + outRc = handleGenStarter(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-vehicles-siege") == 0 && i + 1 < argc) { + outRc = handleGenSiege(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-vehicles-flying") == 0 && i + 1 < argc) { + outRc = handleGenFlying(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wvhc") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wvhc") == 0 && i + 1 < argc) { + outRc = handleValidate(i, argc, argv); return true; + } + return false; +} + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_vehicles_catalog.hpp b/tools/editor/cli_vehicles_catalog.hpp new file mode 100644 index 00000000..b4730487 --- /dev/null +++ b/tools/editor/cli_vehicles_catalog.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleVehiclesCatalog(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee