mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-10 19:13:52 +00:00
feat(pipeline): add WTRN (Wowee Trainer / Vendor catalog) format
Novel open replacement for AzerothCore-style npc_trainer +
npc_vendor SQL tables PLUS the Blizzard TrainerSpells.dbc
family. The 22nd open format added to the editor.
Unifies trainer spell lists and vendor item inventories
into one per-NPC entry. A creature flagged Trainer or
Vendor in WCRT references a WTRN entry that lists what they
teach / sell. The same NPC can be both — kindMask is a
bitmask covering the Trainer (0x01) and Vendor (0x02) kinds.
This format closes a major cross-format gap: WCRT.npcFlags
already had Vendor / Trainer bits, but until now there was
no format defining what a vendor sells or what a trainer
teaches. Now an NPC marked Vendor in WCRT has a real
inventory, and an NPC marked Trainer has a real spell list.
Cross-references — every WTRN field has a real format target:
WTRN.entry.npcId -> WCRT.entry.creatureId
WTRN.spell.spellId -> WSPL.entry.spellId
WTRN.spell.requiredSkillId -> WSKL.entry.skillId
WTRN.item.itemId -> WIT.entry.itemId
Format:
• magic "WTRN", version 1, little-endian
• per NPC: npcId / kindMask / greeting + spells[] + items[]
• per spell offer: spellId / moneyCostCopper /
requiredSkillId / requiredSkillRank / requiredLevel
• per item offer: itemId / stockCount (0xFFFFFFFF =
unlimited) / restockSec / extendedCost / moneyCostCopper
(0 = inherit from WIT.buyPrice)
API: WoweeTrainerLoader::save / load / exists / findByNpc;
presets makeStarter (innkeeper 4001 as both trainer +
vendor: teaches First Aid + sells starter items),
makeMageTrainer (NPC 4003 teaches the WSPL mage spells
at scaling cost), makeWeaponVendor (NPC 4002 sells WIT
weapons with mixed unlimited/finite stock + restock timers).
CLI added (5 flags, 551 documented total now):
--gen-trainers / --gen-trainers-mage / --gen-trainers-weapons
--info-wtrn / --validate-wtrn
Validator catches: npcId=0 + duplicates, kindMask=0 (NPC
offers nothing), Trainer flag without spells, Vendor flag
without items, spells/items present without the matching
kind bit (silently ignored at runtime), spellId=0 / itemId=0
in offers, finite stock with restockSec=0 (single-fill —
usually intentional but worth surfacing).
The 3 presets deliberately use npcIds matching WCRT village
merchants (4001/4002/4003) so the demo content stack is
self-consistent: WCRT 4001 has the Vendor + Trainer flag,
and WTRN 4001 actually defines what they sell and teach.
This commit is contained in:
parent
89871c171c
commit
d2ca3ea22b
8 changed files with 698 additions and 0 deletions
|
|
@ -609,6 +609,7 @@ set(WOWEE_SOURCES
|
|||
src/pipeline/wowee_skills.cpp
|
||||
src/pipeline/wowee_spells.cpp
|
||||
src/pipeline/wowee_achievements.cpp
|
||||
src/pipeline/wowee_trainers.cpp
|
||||
src/pipeline/custom_zone_discovery.cpp
|
||||
src/pipeline/dbc_layout.cpp
|
||||
|
||||
|
|
@ -1364,6 +1365,7 @@ add_executable(wowee_editor
|
|||
tools/editor/cli_skills_catalog.cpp
|
||||
tools/editor/cli_spells_catalog.cpp
|
||||
tools/editor/cli_achievements_catalog.cpp
|
||||
tools/editor/cli_trainers_catalog.cpp
|
||||
tools/editor/cli_quest_objective.cpp
|
||||
tools/editor/cli_quest_reward.cpp
|
||||
tools/editor/cli_clone.cpp
|
||||
|
|
@ -1451,6 +1453,7 @@ add_executable(wowee_editor
|
|||
src/pipeline/wowee_skills.cpp
|
||||
src/pipeline/wowee_spells.cpp
|
||||
src/pipeline/wowee_achievements.cpp
|
||||
src/pipeline/wowee_trainers.cpp
|
||||
src/pipeline/custom_zone_discovery.cpp
|
||||
src/pipeline/terrain_mesh.cpp
|
||||
|
||||
|
|
|
|||
120
include/pipeline/wowee_trainers.hpp
Normal file
120
include/pipeline/wowee_trainers.hpp
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline {
|
||||
|
||||
// Wowee Open Trainer / Vendor catalog (.wtrn) — novel
|
||||
// replacement for AzerothCore-style npc_trainer + npc_vendor
|
||||
// SQL tables PLUS the Blizzard TrainerSpells.dbc family.
|
||||
// The 22nd open format added to the editor.
|
||||
//
|
||||
// Unifies trainer spell lists and vendor inventories into one
|
||||
// per-NPC entry. A creature flagged Trainer or Vendor in WCRT
|
||||
// references a WTRN entry that lists what they teach / sell.
|
||||
// The same NPC can be both — kindMask is a bitmask covering
|
||||
// the Trainer (0x01) and Vendor (0x02) kinds.
|
||||
//
|
||||
// Cross-references with previously-added formats:
|
||||
// WTRN.entry.npcId → WCRT.entry.creatureId
|
||||
// WTRN.spell.spellId → WSPL.entry.spellId
|
||||
// WTRN.spell.requiredSkillId → WSKL.entry.skillId
|
||||
// WTRN.item.itemId → WIT.entry.itemId
|
||||
//
|
||||
// Binary layout (little-endian):
|
||||
// magic[4] = "WTRN"
|
||||
// version (uint32) = current 1
|
||||
// nameLen + name (catalog label)
|
||||
// entryCount (uint32)
|
||||
// entries (each):
|
||||
// npcId (uint32)
|
||||
// kindMask (uint8) + pad[3]
|
||||
// greetingLen + greeting
|
||||
// spellCount (uint16) + itemCount (uint16)
|
||||
// spells (spellCount × {
|
||||
// spellId (uint32)
|
||||
// moneyCostCopper (uint32)
|
||||
// requiredSkillId (uint32)
|
||||
// requiredSkillRank (uint16)
|
||||
// requiredLevel (uint16)
|
||||
// })
|
||||
// items (itemCount × {
|
||||
// itemId (uint32)
|
||||
// stockCount (uint32) -- 0xFFFFFFFF = unlimited
|
||||
// restockSec (uint32)
|
||||
// extendedCost (uint32)
|
||||
// moneyCostCopper (uint32) -- 0 = use WIT.buyPrice
|
||||
// })
|
||||
struct WoweeTrainer {
|
||||
enum KindMask : uint8_t {
|
||||
Trainer = 0x01,
|
||||
Vendor = 0x02,
|
||||
};
|
||||
|
||||
static constexpr uint32_t kUnlimitedStock = 0xFFFFFFFFu;
|
||||
|
||||
struct SpellOffer {
|
||||
uint32_t spellId = 0;
|
||||
uint32_t moneyCostCopper = 0;
|
||||
uint32_t requiredSkillId = 0; // 0 = no skill prerequisite
|
||||
uint16_t requiredSkillRank = 0;
|
||||
uint16_t requiredLevel = 1;
|
||||
};
|
||||
|
||||
struct ItemOffer {
|
||||
uint32_t itemId = 0;
|
||||
uint32_t stockCount = kUnlimitedStock;
|
||||
uint32_t restockSec = 0;
|
||||
uint32_t extendedCost = 0; // 0 = copper-only
|
||||
uint32_t moneyCostCopper = 0; // 0 = inherit from WIT
|
||||
};
|
||||
|
||||
struct Entry {
|
||||
uint32_t npcId = 0;
|
||||
uint8_t kindMask = 0;
|
||||
std::string greeting;
|
||||
std::vector<SpellOffer> spells;
|
||||
std::vector<ItemOffer> items;
|
||||
};
|
||||
|
||||
std::string name;
|
||||
std::vector<Entry> entries;
|
||||
|
||||
bool isValid() const { return !entries.empty(); }
|
||||
|
||||
// Lookup by npcId — nullptr if not present.
|
||||
const Entry* findByNpc(uint32_t npcId) const;
|
||||
|
||||
// Decode the kindMask into a short string (e.g.
|
||||
// "trainer+vendor" or just "vendor").
|
||||
static std::string kindMaskName(uint8_t k);
|
||||
};
|
||||
|
||||
class WoweeTrainerLoader {
|
||||
public:
|
||||
static bool save(const WoweeTrainer& cat,
|
||||
const std::string& basePath);
|
||||
static WoweeTrainer load(const std::string& basePath);
|
||||
static bool exists(const std::string& basePath);
|
||||
|
||||
// Preset emitters used by --gen-trainers* variants.
|
||||
//
|
||||
// makeStarter — 1 NPC (Bartleby innkeeper, npcId=4001)
|
||||
// acting as both vendor + trainer: sells
|
||||
// 3 starter items + teaches First Aid.
|
||||
// makeMageTrainer — npcId=4003 (alchemist) becomes a
|
||||
// mage trainer offering Frostbolt /
|
||||
// Fireball / Arcane Intellect / Blink
|
||||
// at appropriate ranks.
|
||||
// makeWeaponVendor — npcId=4002 (smith) sells 5 weapons
|
||||
// across the WIT weapon catalog.
|
||||
static WoweeTrainer makeStarter(const std::string& catalogName);
|
||||
static WoweeTrainer makeMageTrainer(const std::string& catalogName);
|
||||
static WoweeTrainer makeWeaponVendor(const std::string& catalogName);
|
||||
};
|
||||
|
||||
} // namespace pipeline
|
||||
} // namespace wowee
|
||||
234
src/pipeline/wowee_trainers.cpp
Normal file
234
src/pipeline/wowee_trainers.cpp
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
#include "pipeline/wowee_trainers.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr char kMagic[4] = {'W', 'T', 'R', 'N'};
|
||||
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) != ".wtrn") {
|
||||
base += ".wtrn";
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const WoweeTrainer::Entry* WoweeTrainer::findByNpc(uint32_t npcId) const {
|
||||
for (const auto& e : entries) {
|
||||
if (e.npcId == npcId) return &e;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::string WoweeTrainer::kindMaskName(uint8_t k) {
|
||||
std::string s;
|
||||
if (k & Trainer) s += "trainer";
|
||||
if (k & Vendor) { if (!s.empty()) s += "+"; s += "vendor"; }
|
||||
if (s.empty()) s = "-";
|
||||
return s;
|
||||
}
|
||||
|
||||
bool WoweeTrainerLoader::save(const WoweeTrainer& 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.npcId);
|
||||
writePOD(os, e.kindMask);
|
||||
uint8_t pad[3] = {0, 0, 0};
|
||||
os.write(reinterpret_cast<const char*>(pad), 3);
|
||||
writeStr(os, e.greeting);
|
||||
uint16_t spellCount = static_cast<uint16_t>(
|
||||
e.spells.size() > 0xFFFF ? 0xFFFF : e.spells.size());
|
||||
uint16_t itemCount = static_cast<uint16_t>(
|
||||
e.items.size() > 0xFFFF ? 0xFFFF : e.items.size());
|
||||
writePOD(os, spellCount);
|
||||
writePOD(os, itemCount);
|
||||
for (uint16_t k = 0; k < spellCount; ++k) {
|
||||
const auto& s = e.spells[k];
|
||||
writePOD(os, s.spellId);
|
||||
writePOD(os, s.moneyCostCopper);
|
||||
writePOD(os, s.requiredSkillId);
|
||||
writePOD(os, s.requiredSkillRank);
|
||||
writePOD(os, s.requiredLevel);
|
||||
}
|
||||
for (uint16_t k = 0; k < itemCount; ++k) {
|
||||
const auto& it = e.items[k];
|
||||
writePOD(os, it.itemId);
|
||||
writePOD(os, it.stockCount);
|
||||
writePOD(os, it.restockSec);
|
||||
writePOD(os, it.extendedCost);
|
||||
writePOD(os, it.moneyCostCopper);
|
||||
}
|
||||
}
|
||||
return os.good();
|
||||
}
|
||||
|
||||
WoweeTrainer WoweeTrainerLoader::load(const std::string& basePath) {
|
||||
WoweeTrainer 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.npcId) || !readPOD(is, e.kindMask)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
uint8_t pad[3];
|
||||
is.read(reinterpret_cast<char*>(pad), 3);
|
||||
if (is.gcount() != 3) { out.entries.clear(); return out; }
|
||||
if (!readStr(is, e.greeting)) { out.entries.clear(); return out; }
|
||||
uint16_t spellCount = 0, itemCount = 0;
|
||||
if (!readPOD(is, spellCount) || !readPOD(is, itemCount)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
e.spells.resize(spellCount);
|
||||
for (uint16_t k = 0; k < spellCount; ++k) {
|
||||
auto& s = e.spells[k];
|
||||
if (!readPOD(is, s.spellId) ||
|
||||
!readPOD(is, s.moneyCostCopper) ||
|
||||
!readPOD(is, s.requiredSkillId) ||
|
||||
!readPOD(is, s.requiredSkillRank) ||
|
||||
!readPOD(is, s.requiredLevel)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
}
|
||||
e.items.resize(itemCount);
|
||||
for (uint16_t k = 0; k < itemCount; ++k) {
|
||||
auto& it = e.items[k];
|
||||
if (!readPOD(is, it.itemId) ||
|
||||
!readPOD(is, it.stockCount) ||
|
||||
!readPOD(is, it.restockSec) ||
|
||||
!readPOD(is, it.extendedCost) ||
|
||||
!readPOD(is, it.moneyCostCopper)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool WoweeTrainerLoader::exists(const std::string& basePath) {
|
||||
std::ifstream is(normalizePath(basePath), std::ios::binary);
|
||||
return is.good();
|
||||
}
|
||||
|
||||
WoweeTrainer WoweeTrainerLoader::makeStarter(const std::string& catalogName) {
|
||||
WoweeTrainer c;
|
||||
c.name = catalogName;
|
||||
{
|
||||
// npcId 4001 matches WCRT.makeStarter / makeMerchants
|
||||
// (Bartleby innkeeper).
|
||||
WoweeTrainer::Entry e;
|
||||
e.npcId = 4001;
|
||||
e.kindMask = WoweeTrainer::Trainer | WoweeTrainer::Vendor;
|
||||
e.greeting = "Welcome to the inn, traveler. What can I do for you?";
|
||||
// Train First Aid (skillId 129 in WSKL.makeProfessions).
|
||||
e.spells.push_back({4001, 100, 129, 1, 1}); // teaches First Aid
|
||||
// Sell starter items (itemIds match WIT.makeStarter:
|
||||
// 2=Linen Vest, 3=Healing Potion). Use moneyCost=0 to
|
||||
// mean "use WIT.buyPrice".
|
||||
e.items.push_back({2, WoweeTrainer::kUnlimitedStock, 0, 0, 0});
|
||||
e.items.push_back({3, WoweeTrainer::kUnlimitedStock, 0, 0, 0});
|
||||
e.items.push_back({4, 1, 86400, 0, 0}); // 1 unique item / 24h
|
||||
c.entries.push_back(e);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
WoweeTrainer WoweeTrainerLoader::makeMageTrainer(const std::string& catalogName) {
|
||||
WoweeTrainer c;
|
||||
c.name = catalogName;
|
||||
{
|
||||
// npcId 4003 = alchemist NPC repurposed as a mage
|
||||
// trainer for the demo. Spell IDs match WSPL.makeMage.
|
||||
WoweeTrainer::Entry e;
|
||||
e.npcId = 4003;
|
||||
e.kindMask = WoweeTrainer::Trainer;
|
||||
e.greeting = "Magic is a craft. Will you learn?";
|
||||
// Each spell costs scaling copper, requires reagent
|
||||
// skill (none here), and a minimum character level.
|
||||
e.spells.push_back({116, 100, 0, 0, 4}); // Frostbolt @ lvl 4
|
||||
e.spells.push_back({133, 100, 0, 0, 1}); // Fireball @ lvl 1
|
||||
e.spells.push_back({1459, 1000, 0, 0, 10}); // Arcane Int @ lvl 10
|
||||
e.spells.push_back({1953, 5000, 0, 0, 20}); // Blink @ lvl 20
|
||||
c.entries.push_back(e);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
WoweeTrainer WoweeTrainerLoader::makeWeaponVendor(const std::string& catalogName) {
|
||||
WoweeTrainer c;
|
||||
c.name = catalogName;
|
||||
{
|
||||
// npcId 4002 = smith from WCRT.makeMerchants. Sells
|
||||
// weapons matching WIT.makeWeapons itemIds.
|
||||
WoweeTrainer::Entry e;
|
||||
e.npcId = 4002;
|
||||
e.kindMask = WoweeTrainer::Vendor;
|
||||
e.greeting = "Strong steel for sturdy folk. Take a look.";
|
||||
e.items.push_back({1001, WoweeTrainer::kUnlimitedStock, 0, 0, 0}); // Apprentice Sword
|
||||
e.items.push_back({1002, WoweeTrainer::kUnlimitedStock, 0, 0, 0}); // Journeyman Blade
|
||||
e.items.push_back({1003, 3, 3600, 0, 0}); // Steelthorn Edge: 3 in stock, refresh 1h
|
||||
e.items.push_back({1004, 1, 7200, 0, 0}); // Bloodforged: 1 in stock, refresh 2h
|
||||
e.items.push_back({1005, 0, 0, 0, 0}); // Doombringer: out of stock by default
|
||||
c.entries.push_back(e);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
} // namespace pipeline
|
||||
} // namespace wowee
|
||||
|
|
@ -62,6 +62,8 @@ const char* const kArgRequired[] = {
|
|||
"--gen-achievements", "--gen-achievements-bandit", "--gen-achievements-meta",
|
||||
"--info-wach", "--validate-wach",
|
||||
"--export-wach-json", "--import-wach-json",
|
||||
"--gen-trainers", "--gen-trainers-mage", "--gen-trainers-weapons",
|
||||
"--info-wtrn", "--validate-wtrn",
|
||||
"--gen-weather-temperate", "--gen-weather-arctic",
|
||||
"--gen-weather-desert", "--gen-weather-stormy",
|
||||
"--gen-zone-atmosphere",
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@
|
|||
#include "cli_skills_catalog.hpp"
|
||||
#include "cli_spells_catalog.hpp"
|
||||
#include "cli_achievements_catalog.hpp"
|
||||
#include "cli_trainers_catalog.hpp"
|
||||
#include "cli_quest_objective.hpp"
|
||||
#include "cli_quest_reward.hpp"
|
||||
#include "cli_clone.hpp"
|
||||
|
|
@ -139,6 +140,7 @@ constexpr DispatchFn kDispatchTable[] = {
|
|||
handleSkillsCatalog,
|
||||
handleSpellsCatalog,
|
||||
handleAchievementsCatalog,
|
||||
handleTrainersCatalog,
|
||||
handleQuestObjective,
|
||||
handleQuestReward,
|
||||
handleClone,
|
||||
|
|
|
|||
|
|
@ -1007,6 +1007,16 @@ void printUsage(const char* argv0) {
|
|||
std::printf(" Export binary .wach to a human-editable JSON sidecar (defaults to <base>.wach.json)\n");
|
||||
std::printf(" --import-wach-json <json-path> [out-base]\n");
|
||||
std::printf(" Import a .wach.json sidecar back into binary .wach (accepts kind/faction/flag int OR name forms)\n");
|
||||
std::printf(" --gen-trainers <wtrn-base> [name]\n");
|
||||
std::printf(" Emit .wtrn starter: 1 NPC (innkeeper 4001) acting as both vendor + trainer with WSKL/WIT cross-refs\n");
|
||||
std::printf(" --gen-trainers-mage <wtrn-base> [name]\n");
|
||||
std::printf(" Emit .wtrn mage trainer (npcId=4003): teaches Frostbolt/Fireball/Arcane Intellect/Blink at scaling cost\n");
|
||||
std::printf(" --gen-trainers-weapons <wtrn-base> [name]\n");
|
||||
std::printf(" Emit .wtrn weapon vendor (npcId=4002): 5 weapons with mixed unlimited/finite stock + restock timers\n");
|
||||
std::printf(" --info-wtrn <wtrn-base> [--json]\n");
|
||||
std::printf(" Print WTRN entries (npc / kind / spells with skill+level reqs / items with stock + restock)\n");
|
||||
std::printf(" --validate-wtrn <wtrn-base> [--json]\n");
|
||||
std::printf(" Static checks: npcId>0+unique, kindMask>0, Trainer needs spells, Vendor needs items, no orphan offers\n");
|
||||
std::printf(" --gen-weather-temperate <wow-base> [zoneName]\n");
|
||||
std::printf(" Emit .wow weather schedule: clear-dominant + occasional rain + fog (forest / grassland)\n");
|
||||
std::printf(" --gen-weather-arctic <wow-base> [zoneName]\n");
|
||||
|
|
|
|||
316
tools/editor/cli_trainers_catalog.cpp
Normal file
316
tools/editor/cli_trainers_catalog.cpp
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
#include "cli_trainers_catalog.hpp"
|
||||
#include "cli_arg_parse.hpp"
|
||||
#include "cli_box_emitter.hpp"
|
||||
|
||||
#include "pipeline/wowee_trainers.hpp"
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace wowee {
|
||||
namespace editor {
|
||||
namespace cli {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string stripWtrnExt(std::string base) {
|
||||
stripExt(base, ".wtrn");
|
||||
return base;
|
||||
}
|
||||
|
||||
bool saveOrError(const wowee::pipeline::WoweeTrainer& c,
|
||||
const std::string& base, const char* cmd) {
|
||||
if (!wowee::pipeline::WoweeTrainerLoader::save(c, base)) {
|
||||
std::fprintf(stderr, "%s: failed to save %s.wtrn\n",
|
||||
cmd, base.c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t totalSpellOffers(const wowee::pipeline::WoweeTrainer& c) {
|
||||
uint32_t n = 0;
|
||||
for (const auto& e : c.entries) n += static_cast<uint32_t>(e.spells.size());
|
||||
return n;
|
||||
}
|
||||
|
||||
uint32_t totalItemOffers(const wowee::pipeline::WoweeTrainer& c) {
|
||||
uint32_t n = 0;
|
||||
for (const auto& e : c.entries) n += static_cast<uint32_t>(e.items.size());
|
||||
return n;
|
||||
}
|
||||
|
||||
void printGenSummary(const wowee::pipeline::WoweeTrainer& c,
|
||||
const std::string& base) {
|
||||
std::printf("Wrote %s.wtrn\n", base.c_str());
|
||||
std::printf(" catalog : %s\n", c.name.c_str());
|
||||
std::printf(" npcs : %zu (%u spells offered, %u items offered)\n",
|
||||
c.entries.size(),
|
||||
totalSpellOffers(c),
|
||||
totalItemOffers(c));
|
||||
}
|
||||
|
||||
int handleGenStarter(int& i, int argc, char** argv) {
|
||||
std::string base = argv[++i];
|
||||
std::string name = "StarterTrainers";
|
||||
if (parseOptArg(i, argc, argv)) name = argv[++i];
|
||||
base = stripWtrnExt(base);
|
||||
auto c = wowee::pipeline::WoweeTrainerLoader::makeStarter(name);
|
||||
if (!saveOrError(c, base, "gen-trainers")) return 1;
|
||||
printGenSummary(c, base);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int handleGenMage(int& i, int argc, char** argv) {
|
||||
std::string base = argv[++i];
|
||||
std::string name = "MageTrainer";
|
||||
if (parseOptArg(i, argc, argv)) name = argv[++i];
|
||||
base = stripWtrnExt(base);
|
||||
auto c = wowee::pipeline::WoweeTrainerLoader::makeMageTrainer(name);
|
||||
if (!saveOrError(c, base, "gen-trainers-mage")) return 1;
|
||||
printGenSummary(c, base);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int handleGenWeaponVendor(int& i, int argc, char** argv) {
|
||||
std::string base = argv[++i];
|
||||
std::string name = "WeaponVendor";
|
||||
if (parseOptArg(i, argc, argv)) name = argv[++i];
|
||||
base = stripWtrnExt(base);
|
||||
auto c = wowee::pipeline::WoweeTrainerLoader::makeWeaponVendor(name);
|
||||
if (!saveOrError(c, base, "gen-trainers-weapons")) 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 = stripWtrnExt(base);
|
||||
if (!wowee::pipeline::WoweeTrainerLoader::exists(base)) {
|
||||
std::fprintf(stderr, "WTRN not found: %s.wtrn\n", base.c_str());
|
||||
return 1;
|
||||
}
|
||||
auto c = wowee::pipeline::WoweeTrainerLoader::load(base);
|
||||
if (jsonOut) {
|
||||
nlohmann::json j;
|
||||
j["wtrn"] = base + ".wtrn";
|
||||
j["name"] = c.name;
|
||||
j["count"] = c.entries.size();
|
||||
nlohmann::json arr = nlohmann::json::array();
|
||||
for (const auto& e : c.entries) {
|
||||
nlohmann::json je;
|
||||
je["npcId"] = e.npcId;
|
||||
je["kindMask"] = e.kindMask;
|
||||
je["kindMaskName"] = wowee::pipeline::WoweeTrainer::kindMaskName(e.kindMask);
|
||||
je["greeting"] = e.greeting;
|
||||
nlohmann::json sa = nlohmann::json::array();
|
||||
for (const auto& s : e.spells) {
|
||||
sa.push_back({
|
||||
{"spellId", s.spellId},
|
||||
{"moneyCostCopper", s.moneyCostCopper},
|
||||
{"requiredSkillId", s.requiredSkillId},
|
||||
{"requiredSkillRank", s.requiredSkillRank},
|
||||
{"requiredLevel", s.requiredLevel},
|
||||
});
|
||||
}
|
||||
je["spells"] = sa;
|
||||
nlohmann::json ia = nlohmann::json::array();
|
||||
for (const auto& it : e.items) {
|
||||
ia.push_back({
|
||||
{"itemId", it.itemId},
|
||||
{"stockCount", it.stockCount},
|
||||
{"restockSec", it.restockSec},
|
||||
{"extendedCost", it.extendedCost},
|
||||
{"moneyCostCopper", it.moneyCostCopper},
|
||||
});
|
||||
}
|
||||
je["items"] = ia;
|
||||
arr.push_back(je);
|
||||
}
|
||||
j["entries"] = arr;
|
||||
std::printf("%s\n", j.dump(2).c_str());
|
||||
return 0;
|
||||
}
|
||||
std::printf("WTRN: %s.wtrn\n", base.c_str());
|
||||
std::printf(" catalog : %s\n", c.name.c_str());
|
||||
std::printf(" npcs : %zu (%u spells offered, %u items offered)\n",
|
||||
c.entries.size(),
|
||||
totalSpellOffers(c),
|
||||
totalItemOffers(c));
|
||||
if (c.entries.empty()) return 0;
|
||||
for (const auto& e : c.entries) {
|
||||
std::printf("\n npcId=%u kind=%s\n",
|
||||
e.npcId,
|
||||
wowee::pipeline::WoweeTrainer::kindMaskName(e.kindMask).c_str());
|
||||
if (!e.greeting.empty()) {
|
||||
std::printf(" greeting: \"%s\"\n", e.greeting.c_str());
|
||||
}
|
||||
if (!e.spells.empty()) {
|
||||
std::printf(" spells (%zu):\n", e.spells.size());
|
||||
std::printf(" spellId cost skill/rank minLvl\n");
|
||||
for (const auto& s : e.spells) {
|
||||
std::printf(" %5u %5uc %4u/%-4u %u\n",
|
||||
s.spellId, s.moneyCostCopper,
|
||||
s.requiredSkillId, s.requiredSkillRank,
|
||||
s.requiredLevel);
|
||||
}
|
||||
}
|
||||
if (!e.items.empty()) {
|
||||
std::printf(" items (%zu):\n", e.items.size());
|
||||
std::printf(" itemId stock restock override-cost\n");
|
||||
for (const auto& it : e.items) {
|
||||
std::string stockStr =
|
||||
it.stockCount == wowee::pipeline::WoweeTrainer::kUnlimitedStock
|
||||
? std::string("unlimited") : std::to_string(it.stockCount);
|
||||
std::printf(" %5u %-9s %5us %s%uc\n",
|
||||
it.itemId, stockStr.c_str(),
|
||||
it.restockSec,
|
||||
it.moneyCostCopper == 0 ? "(WIT) " : "",
|
||||
it.moneyCostCopper);
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int handleValidate(int& i, int argc, char** argv) {
|
||||
std::string base = argv[++i];
|
||||
bool jsonOut = consumeJsonFlag(i, argc, argv);
|
||||
base = stripWtrnExt(base);
|
||||
if (!wowee::pipeline::WoweeTrainerLoader::exists(base)) {
|
||||
std::fprintf(stderr,
|
||||
"validate-wtrn: WTRN not found: %s.wtrn\n", base.c_str());
|
||||
return 1;
|
||||
}
|
||||
auto c = wowee::pipeline::WoweeTrainerLoader::load(base);
|
||||
std::vector<std::string> errors;
|
||||
std::vector<std::string> warnings;
|
||||
if (c.entries.empty()) {
|
||||
warnings.push_back("catalog has zero entries");
|
||||
}
|
||||
std::vector<uint32_t> idsSeen;
|
||||
idsSeen.reserve(c.entries.size());
|
||||
for (size_t k = 0; k < c.entries.size(); ++k) {
|
||||
const auto& e = c.entries[k];
|
||||
std::string ctx = "entry " + std::to_string(k) +
|
||||
" (npcId=" + std::to_string(e.npcId) + ")";
|
||||
if (e.npcId == 0) {
|
||||
errors.push_back(ctx + ": npcId is 0");
|
||||
}
|
||||
if (e.kindMask == 0) {
|
||||
errors.push_back(ctx + ": kindMask is 0 (NPC offers nothing)");
|
||||
}
|
||||
// Trainer kind needs spells; vendor kind needs items.
|
||||
if ((e.kindMask & wowee::pipeline::WoweeTrainer::Trainer) &&
|
||||
e.spells.empty()) {
|
||||
warnings.push_back(ctx +
|
||||
": flagged Trainer but has no spells");
|
||||
}
|
||||
if ((e.kindMask & wowee::pipeline::WoweeTrainer::Vendor) &&
|
||||
e.items.empty()) {
|
||||
warnings.push_back(ctx +
|
||||
": flagged Vendor but has no items");
|
||||
}
|
||||
// Items / spells with kindMask not matching are dead config.
|
||||
if (!(e.kindMask & wowee::pipeline::WoweeTrainer::Trainer) &&
|
||||
!e.spells.empty()) {
|
||||
warnings.push_back(ctx +
|
||||
": has " + std::to_string(e.spells.size()) +
|
||||
" spells but Trainer bit not set (spells will be ignored)");
|
||||
}
|
||||
if (!(e.kindMask & wowee::pipeline::WoweeTrainer::Vendor) &&
|
||||
!e.items.empty()) {
|
||||
warnings.push_back(ctx +
|
||||
": has " + std::to_string(e.items.size()) +
|
||||
" items but Vendor bit not set (items will be ignored)");
|
||||
}
|
||||
for (size_t si = 0; si < e.spells.size(); ++si) {
|
||||
const auto& s = e.spells[si];
|
||||
std::string sctx = ctx + " spell " + std::to_string(si);
|
||||
if (s.spellId == 0) {
|
||||
errors.push_back(sctx + ": spellId is 0");
|
||||
}
|
||||
}
|
||||
for (size_t ii = 0; ii < e.items.size(); ++ii) {
|
||||
const auto& it = e.items[ii];
|
||||
std::string ictx = ctx + " item " + std::to_string(ii);
|
||||
if (it.itemId == 0) {
|
||||
errors.push_back(ictx + ": itemId is 0");
|
||||
}
|
||||
// Finite stock with restockSec=0 means "single fill"
|
||||
// — usually intentional but worth surfacing.
|
||||
if (it.stockCount != wowee::pipeline::WoweeTrainer::kUnlimitedStock &&
|
||||
it.restockSec == 0 && it.stockCount > 0) {
|
||||
warnings.push_back(ictx +
|
||||
": finite stock with restockSec=0 (no automatic refresh)");
|
||||
}
|
||||
}
|
||||
for (uint32_t prev : idsSeen) {
|
||||
if (prev == e.npcId) {
|
||||
errors.push_back(ctx + ": duplicate npcId");
|
||||
break;
|
||||
}
|
||||
}
|
||||
idsSeen.push_back(e.npcId);
|
||||
}
|
||||
bool ok = errors.empty();
|
||||
if (jsonOut) {
|
||||
nlohmann::json j;
|
||||
j["wtrn"] = base + ".wtrn";
|
||||
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-wtrn: %s.wtrn\n", base.c_str());
|
||||
if (ok && warnings.empty()) {
|
||||
std::printf(" OK — %zu npcs, %u spell offers, %u item offers\n",
|
||||
c.entries.size(),
|
||||
totalSpellOffers(c),
|
||||
totalItemOffers(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 handleTrainersCatalog(int& i, int argc, char** argv, int& outRc) {
|
||||
if (std::strcmp(argv[i], "--gen-trainers") == 0 && i + 1 < argc) {
|
||||
outRc = handleGenStarter(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--gen-trainers-mage") == 0 && i + 1 < argc) {
|
||||
outRc = handleGenMage(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--gen-trainers-weapons") == 0 && i + 1 < argc) {
|
||||
outRc = handleGenWeaponVendor(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--info-wtrn") == 0 && i + 1 < argc) {
|
||||
outRc = handleInfo(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--validate-wtrn") == 0 && i + 1 < argc) {
|
||||
outRc = handleValidate(i, argc, argv); return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace cli
|
||||
} // namespace editor
|
||||
} // namespace wowee
|
||||
11
tools/editor/cli_trainers_catalog.hpp
Normal file
11
tools/editor/cli_trainers_catalog.hpp
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#pragma once
|
||||
|
||||
namespace wowee {
|
||||
namespace editor {
|
||||
namespace cli {
|
||||
|
||||
bool handleTrainersCatalog(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