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:
Kelsi 2026-05-09 16:12:58 -07:00
parent 89871c171c
commit d2ca3ea22b
8 changed files with 698 additions and 0 deletions

View 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