mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-10 02:53:51 +00:00
feat(pipeline): add WGSP (Wowee Gossip Menu) format
Novel open replacement for AzerothCore-style gossip_menu +
gossip_menu_option + npc_text SQL tables PLUS the Blizzard
NpcText.dbc family. The 23rd open format added to the
editor.
An NPC's dialogue tree: a menu of options the player can
pick from when right-clicking the NPC. Each option may
bridge to another menu, trigger a vendor / trainer
interaction, offer a quest, etc. The simplified per-option
model (kind + actionTarget + flags + moneyCost) covers the
common cases without needing separate npc_text condition
tables.
Closes a major cross-format gap: WCRT.entry.gossipId has
existed since batch 116 (when WCRT was added) but pointed
to a format that didn't exist yet. The innkeeper preset's
menuId=4001 deliberately matches WCRT's Bartleby NPC so
the demo content stack can wire WCRT.gossipId = 4001 once
that field is plumbed through the runtime.
Cross-references:
WCRT.entry.gossipId -> WGSP.entry.menuId
WGSP.option.actionTarget (Submenu) -> WGSP.entry.menuId
WGSP.option.actionTarget (Vendor / Trainer)
-> WTRN.entry.npcId
WGSP.option.actionTarget (Quest) -> WQT.entry.questId
Format:
• magic "WGSP", version 1, little-endian
• per menu: menuId / titleText + options[]
• per option: optionId / text / kind / actionTarget /
requiredFlags / moneyCostCopper
Enums:
• OptionKind (13): Close / Submenu / Vendor / Trainer /
Quest / Tabard / Banker / Innkeeper /
FlightMaster / TextOnly / Script /
Battlemaster / Auctioneer
• OptionFlags: AllianceOnly / HordeOnly / Coinpouch /
QuestGated / Closes
API: WoweeGossipLoader::save / load / exists / findById;
presets makeStarter (1 menu with vendor + trainer + close),
makeInnkeeper (2-menu tree: main menu 4001 with hearth /
vendor / flight / submenu options + lore submenu 4002 that
links back), makeQuestGiver (1 menu with 2 quest options
referencing WQT 1 and 100, plus a paid respec script
exercising the Coinpouch flag with a 10g cost).
CLI added (5 flags, 558 documented total now):
--gen-gossip / --gen-gossip-innkeeper / --gen-gossip-questgiver
--info-wgsp / --validate-wgsp
Validator catches: menuId=0 + duplicates, empty title /
options, unknown option kind, empty option text, Submenu
options pointing at non-existent menuIds (intra-format
cross-reference resolution), Coinpouch flag without
moneyCost (misleading UI), AllianceOnly+HordeOnly conflict.
This commit is contained in:
parent
c3f7286d4a
commit
2de08a3fd0
8 changed files with 710 additions and 0 deletions
|
|
@ -610,6 +610,7 @@ set(WOWEE_SOURCES
|
|||
src/pipeline/wowee_spells.cpp
|
||||
src/pipeline/wowee_achievements.cpp
|
||||
src/pipeline/wowee_trainers.cpp
|
||||
src/pipeline/wowee_gossip.cpp
|
||||
src/pipeline/custom_zone_discovery.cpp
|
||||
src/pipeline/dbc_layout.cpp
|
||||
|
||||
|
|
@ -1366,6 +1367,7 @@ add_executable(wowee_editor
|
|||
tools/editor/cli_spells_catalog.cpp
|
||||
tools/editor/cli_achievements_catalog.cpp
|
||||
tools/editor/cli_trainers_catalog.cpp
|
||||
tools/editor/cli_gossip_catalog.cpp
|
||||
tools/editor/cli_quest_objective.cpp
|
||||
tools/editor/cli_quest_reward.cpp
|
||||
tools/editor/cli_clone.cpp
|
||||
|
|
@ -1454,6 +1456,7 @@ add_executable(wowee_editor
|
|||
src/pipeline/wowee_spells.cpp
|
||||
src/pipeline/wowee_achievements.cpp
|
||||
src/pipeline/wowee_trainers.cpp
|
||||
src/pipeline/wowee_gossip.cpp
|
||||
src/pipeline/custom_zone_discovery.cpp
|
||||
src/pipeline/terrain_mesh.cpp
|
||||
|
||||
|
|
|
|||
123
include/pipeline/wowee_gossip.hpp
Normal file
123
include/pipeline/wowee_gossip.hpp
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline {
|
||||
|
||||
// Wowee Open Gossip Menu catalog (.wgsp) — novel replacement
|
||||
// for AzerothCore-style gossip_menu + gossip_menu_option +
|
||||
// npc_text SQL tables PLUS the Blizzard NpcText.dbc family.
|
||||
// The 23rd open format added to the editor.
|
||||
//
|
||||
// An NPC's dialogue tree: a menu of options the player can
|
||||
// pick from when right-clicking the NPC. Each option may
|
||||
// bridge to another menu, trigger a vendor / trainer
|
||||
// interaction, offer a quest, etc.
|
||||
//
|
||||
// This format closes the WCRT.gossipId cross-reference gap
|
||||
// from batch 116 — until now WCRT had a gossipId field that
|
||||
// pointed to a format that didn't exist yet.
|
||||
//
|
||||
// Cross-references with previously-added formats:
|
||||
// WCRT.entry.gossipId → WGSP.entry.menuId
|
||||
// WGSP.option.actionTarget (kind=Submenu) → WGSP.entry.menuId
|
||||
// (intra-format chain)
|
||||
// WGSP.option.actionTarget (kind=Vendor / Trainer)
|
||||
// → WTRN.entry.npcId
|
||||
// WGSP.option.actionTarget (kind=Quest) → WQT.entry.questId
|
||||
//
|
||||
// Binary layout (little-endian):
|
||||
// magic[4] = "WGSP"
|
||||
// version (uint32) = current 1
|
||||
// nameLen + name (catalog label)
|
||||
// entryCount (uint32)
|
||||
// entries (each):
|
||||
// menuId (uint32)
|
||||
// titleLen + titleText
|
||||
// optionCount (uint8) + pad[3]
|
||||
// options (optionCount × {
|
||||
// optionId (uint32)
|
||||
// textLen + text
|
||||
// kind (uint8) + pad[3]
|
||||
// actionTarget (uint32)
|
||||
// requiredFlags (uint32)
|
||||
// moneyCostCopper (uint32)
|
||||
// })
|
||||
struct WoweeGossip {
|
||||
enum OptionKind : uint8_t {
|
||||
Close = 0, // closes the menu, no action
|
||||
Submenu = 1, // jumps to another menuId
|
||||
Vendor = 2, // opens vendor inventory window
|
||||
Trainer = 3, // opens trainer spell window
|
||||
Quest = 4, // opens quest dialog
|
||||
Tabard = 5, // opens tabard customization
|
||||
Banker = 6, // opens bank
|
||||
Innkeeper = 7, // sets hearth + opens vendor
|
||||
FlightMaster = 8, // opens taxi node
|
||||
TextOnly = 9, // dialogue only, no action
|
||||
Script = 10, // triggers a server-side script
|
||||
Battlemaster = 11,
|
||||
Auctioneer = 12,
|
||||
};
|
||||
|
||||
enum OptionFlags : uint32_t {
|
||||
AllianceOnly = 0x01,
|
||||
HordeOnly = 0x02,
|
||||
Coinpouch = 0x04, // shows the coin icon when paid
|
||||
QuestGated = 0x08, // visible only with matching quest
|
||||
Closes = 0x10, // closes the menu after the action
|
||||
};
|
||||
|
||||
struct Option {
|
||||
uint32_t optionId = 0;
|
||||
std::string text;
|
||||
uint8_t kind = TextOnly;
|
||||
uint32_t actionTarget = 0; // submenu / NPC / quest id
|
||||
uint32_t requiredFlags = 0;
|
||||
uint32_t moneyCostCopper = 0;
|
||||
};
|
||||
|
||||
struct Entry {
|
||||
uint32_t menuId = 0;
|
||||
std::string titleText;
|
||||
std::vector<Option> options;
|
||||
};
|
||||
|
||||
std::string name;
|
||||
std::vector<Entry> entries;
|
||||
|
||||
bool isValid() const { return !entries.empty(); }
|
||||
|
||||
// Lookup by menuId — nullptr if not present.
|
||||
const Entry* findById(uint32_t menuId) const;
|
||||
|
||||
static const char* optionKindName(uint8_t k);
|
||||
};
|
||||
|
||||
class WoweeGossipLoader {
|
||||
public:
|
||||
static bool save(const WoweeGossip& cat,
|
||||
const std::string& basePath);
|
||||
static WoweeGossip load(const std::string& basePath);
|
||||
static bool exists(const std::string& basePath);
|
||||
|
||||
// Preset emitters used by --gen-gossip* variants.
|
||||
//
|
||||
// makeStarter — single menu with greeting + 3 options
|
||||
// (vendor / trainer / close).
|
||||
// makeInnkeeper — menu 4001 (matches WCRT.gossipId on
|
||||
// Bartleby): set hearth + browse goods
|
||||
// + bind to flight + close.
|
||||
// makeQuestGiver — branching menu: greeting + 2 quests +
|
||||
// submenu "tell me about the area"
|
||||
// leading to lore text + close.
|
||||
static WoweeGossip makeStarter(const std::string& catalogName);
|
||||
static WoweeGossip makeInnkeeper(const std::string& catalogName);
|
||||
static WoweeGossip makeQuestGiver(const std::string& catalogName);
|
||||
};
|
||||
|
||||
} // namespace pipeline
|
||||
} // namespace wowee
|
||||
266
src/pipeline/wowee_gossip.cpp
Normal file
266
src/pipeline/wowee_gossip.cpp
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
#include "pipeline/wowee_gossip.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr char kMagic[4] = {'W', 'G', 'S', 'P'};
|
||||
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) != ".wgsp") {
|
||||
base += ".wgsp";
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const WoweeGossip::Entry* WoweeGossip::findById(uint32_t menuId) const {
|
||||
for (const auto& e : entries) {
|
||||
if (e.menuId == menuId) return &e;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char* WoweeGossip::optionKindName(uint8_t k) {
|
||||
switch (k) {
|
||||
case Close: return "close";
|
||||
case Submenu: return "submenu";
|
||||
case Vendor: return "vendor";
|
||||
case Trainer: return "trainer";
|
||||
case Quest: return "quest";
|
||||
case Tabard: return "tabard";
|
||||
case Banker: return "banker";
|
||||
case Innkeeper: return "innkeeper";
|
||||
case FlightMaster: return "flight";
|
||||
case TextOnly: return "text";
|
||||
case Script: return "script";
|
||||
case Battlemaster: return "battlemaster";
|
||||
case Auctioneer: return "auctioneer";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
bool WoweeGossipLoader::save(const WoweeGossip& 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.menuId);
|
||||
writeStr(os, e.titleText);
|
||||
uint8_t optCount = static_cast<uint8_t>(
|
||||
e.options.size() > 255 ? 255 : e.options.size());
|
||||
writePOD(os, optCount);
|
||||
uint8_t pad[3] = {0, 0, 0};
|
||||
os.write(reinterpret_cast<const char*>(pad), 3);
|
||||
for (uint8_t k = 0; k < optCount; ++k) {
|
||||
const auto& o = e.options[k];
|
||||
writePOD(os, o.optionId);
|
||||
writeStr(os, o.text);
|
||||
writePOD(os, o.kind);
|
||||
os.write(reinterpret_cast<const char*>(pad), 3);
|
||||
writePOD(os, o.actionTarget);
|
||||
writePOD(os, o.requiredFlags);
|
||||
writePOD(os, o.moneyCostCopper);
|
||||
}
|
||||
}
|
||||
return os.good();
|
||||
}
|
||||
|
||||
WoweeGossip WoweeGossipLoader::load(const std::string& basePath) {
|
||||
WoweeGossip 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.menuId)) { out.entries.clear(); return out; }
|
||||
if (!readStr(is, e.titleText)) { out.entries.clear(); return out; }
|
||||
uint8_t optCount = 0;
|
||||
if (!readPOD(is, optCount)) { 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; }
|
||||
e.options.resize(optCount);
|
||||
for (uint8_t k = 0; k < optCount; ++k) {
|
||||
auto& o = e.options[k];
|
||||
if (!readPOD(is, o.optionId)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
if (!readStr(is, o.text)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
if (!readPOD(is, o.kind)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
is.read(reinterpret_cast<char*>(pad), 3);
|
||||
if (is.gcount() != 3) { out.entries.clear(); return out; }
|
||||
if (!readPOD(is, o.actionTarget) ||
|
||||
!readPOD(is, o.requiredFlags) ||
|
||||
!readPOD(is, o.moneyCostCopper)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool WoweeGossipLoader::exists(const std::string& basePath) {
|
||||
std::ifstream is(normalizePath(basePath), std::ios::binary);
|
||||
return is.good();
|
||||
}
|
||||
|
||||
WoweeGossip WoweeGossipLoader::makeStarter(const std::string& catalogName) {
|
||||
WoweeGossip c;
|
||||
c.name = catalogName;
|
||||
{
|
||||
WoweeGossip::Entry e;
|
||||
e.menuId = 1;
|
||||
e.titleText = "Greetings, traveler. How can I help?";
|
||||
e.options.push_back({1, "I want to browse your goods.",
|
||||
WoweeGossip::Vendor, 0,
|
||||
WoweeGossip::Closes, 0});
|
||||
e.options.push_back({2, "Train me.",
|
||||
WoweeGossip::Trainer, 0,
|
||||
WoweeGossip::Closes, 0});
|
||||
e.options.push_back({3, "Goodbye.",
|
||||
WoweeGossip::Close, 0,
|
||||
WoweeGossip::Closes, 0});
|
||||
c.entries.push_back(e);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
WoweeGossip WoweeGossipLoader::makeInnkeeper(const std::string& catalogName) {
|
||||
WoweeGossip c;
|
||||
c.name = catalogName;
|
||||
{
|
||||
// menuId 4001 deliberately matches what WCRT.makeStarter
|
||||
// and WCRT.makeMerchants set as Bartleby's gossipId
|
||||
// (currently 0 — set this when the demo content stack
|
||||
// is updated to wire WCRT.gossipId = 4001).
|
||||
WoweeGossip::Entry e;
|
||||
e.menuId = 4001;
|
||||
e.titleText =
|
||||
"Welcome to the inn! What'll it be — a room, "
|
||||
"a meal, or directions?";
|
||||
e.options.push_back({1, "Make this inn my home.",
|
||||
WoweeGossip::Innkeeper, 0,
|
||||
WoweeGossip::Closes, 0});
|
||||
e.options.push_back({2, "Show me what you have for sale.",
|
||||
WoweeGossip::Vendor, 4001,
|
||||
WoweeGossip::Closes, 0});
|
||||
e.options.push_back({3, "I need to take a flight.",
|
||||
WoweeGossip::FlightMaster, 0,
|
||||
WoweeGossip::Closes, 0});
|
||||
e.options.push_back({4, "Tell me about the area.",
|
||||
WoweeGossip::Submenu, 4002,
|
||||
0, 0});
|
||||
e.options.push_back({5, "Goodbye.",
|
||||
WoweeGossip::Close, 0,
|
||||
WoweeGossip::Closes, 0});
|
||||
c.entries.push_back(e);
|
||||
}
|
||||
{
|
||||
// Submenu reached from the "tell me about the area" option.
|
||||
WoweeGossip::Entry e;
|
||||
e.menuId = 4002;
|
||||
e.titleText =
|
||||
"There's been bandit trouble of late. The Defias "
|
||||
"have a camp east of here. Mind your purse on the "
|
||||
"road.";
|
||||
e.options.push_back({1, "Back.",
|
||||
WoweeGossip::Submenu, 4001,
|
||||
0, 0});
|
||||
e.options.push_back({2, "Goodbye.",
|
||||
WoweeGossip::Close, 0,
|
||||
WoweeGossip::Closes, 0});
|
||||
c.entries.push_back(e);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
WoweeGossip WoweeGossipLoader::makeQuestGiver(const std::string& catalogName) {
|
||||
WoweeGossip c;
|
||||
c.name = catalogName;
|
||||
{
|
||||
WoweeGossip::Entry e;
|
||||
e.menuId = 5000;
|
||||
e.titleText =
|
||||
"I have work for someone of your obvious talent.";
|
||||
// Quest options reference WQT.questId values from
|
||||
// makeStarter/makeChain.
|
||||
e.options.push_back({1, "Tell me about Bandit Trouble.",
|
||||
WoweeGossip::Quest, 1,
|
||||
0, 0});
|
||||
e.options.push_back({2, "What's this about a camp?",
|
||||
WoweeGossip::Quest, 100,
|
||||
0, 0});
|
||||
e.options.push_back({3, "I have business with the bank.",
|
||||
WoweeGossip::Banker, 0,
|
||||
WoweeGossip::Closes, 0});
|
||||
e.options.push_back({4, "Pay 10 gold to respec my talents.",
|
||||
WoweeGossip::Script, 9001,
|
||||
WoweeGossip::Coinpouch | WoweeGossip::Closes,
|
||||
100000}); // 10g
|
||||
e.options.push_back({5, "Goodbye.",
|
||||
WoweeGossip::Close, 0,
|
||||
WoweeGossip::Closes, 0});
|
||||
c.entries.push_back(e);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
} // namespace pipeline
|
||||
} // namespace wowee
|
||||
|
|
@ -65,6 +65,8 @@ const char* const kArgRequired[] = {
|
|||
"--gen-trainers", "--gen-trainers-mage", "--gen-trainers-weapons",
|
||||
"--info-wtrn", "--validate-wtrn",
|
||||
"--export-wtrn-json", "--import-wtrn-json",
|
||||
"--gen-gossip", "--gen-gossip-innkeeper", "--gen-gossip-questgiver",
|
||||
"--info-wgsp", "--validate-wgsp",
|
||||
"--gen-weather-temperate", "--gen-weather-arctic",
|
||||
"--gen-weather-desert", "--gen-weather-stormy",
|
||||
"--gen-zone-atmosphere",
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@
|
|||
#include "cli_spells_catalog.hpp"
|
||||
#include "cli_achievements_catalog.hpp"
|
||||
#include "cli_trainers_catalog.hpp"
|
||||
#include "cli_gossip_catalog.hpp"
|
||||
#include "cli_quest_objective.hpp"
|
||||
#include "cli_quest_reward.hpp"
|
||||
#include "cli_clone.hpp"
|
||||
|
|
@ -141,6 +142,7 @@ constexpr DispatchFn kDispatchTable[] = {
|
|||
handleSpellsCatalog,
|
||||
handleAchievementsCatalog,
|
||||
handleTrainersCatalog,
|
||||
handleGossipCatalog,
|
||||
handleQuestObjective,
|
||||
handleQuestReward,
|
||||
handleClone,
|
||||
|
|
|
|||
293
tools/editor/cli_gossip_catalog.cpp
Normal file
293
tools/editor/cli_gossip_catalog.cpp
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
#include "cli_gossip_catalog.hpp"
|
||||
#include "cli_arg_parse.hpp"
|
||||
#include "cli_box_emitter.hpp"
|
||||
|
||||
#include "pipeline/wowee_gossip.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 stripWgspExt(std::string base) {
|
||||
stripExt(base, ".wgsp");
|
||||
return base;
|
||||
}
|
||||
|
||||
void appendOptFlagsStr(std::string& s, uint32_t flags) {
|
||||
if (flags & wowee::pipeline::WoweeGossip::AllianceOnly) s += "alliance ";
|
||||
if (flags & wowee::pipeline::WoweeGossip::HordeOnly) s += "horde ";
|
||||
if (flags & wowee::pipeline::WoweeGossip::Coinpouch) s += "coin ";
|
||||
if (flags & wowee::pipeline::WoweeGossip::QuestGated) s += "quest-gated ";
|
||||
if (flags & wowee::pipeline::WoweeGossip::Closes) s += "closes ";
|
||||
if (s.empty()) s = "-";
|
||||
else if (s.back() == ' ') s.pop_back();
|
||||
}
|
||||
|
||||
bool saveOrError(const wowee::pipeline::WoweeGossip& c,
|
||||
const std::string& base, const char* cmd) {
|
||||
if (!wowee::pipeline::WoweeGossipLoader::save(c, base)) {
|
||||
std::fprintf(stderr, "%s: failed to save %s.wgsp\n",
|
||||
cmd, base.c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t totalOptions(const wowee::pipeline::WoweeGossip& c) {
|
||||
uint32_t n = 0;
|
||||
for (const auto& e : c.entries) n += static_cast<uint32_t>(e.options.size());
|
||||
return n;
|
||||
}
|
||||
|
||||
void printGenSummary(const wowee::pipeline::WoweeGossip& c,
|
||||
const std::string& base) {
|
||||
std::printf("Wrote %s.wgsp\n", base.c_str());
|
||||
std::printf(" catalog : %s\n", c.name.c_str());
|
||||
std::printf(" menus : %zu (%u options total)\n",
|
||||
c.entries.size(), totalOptions(c));
|
||||
}
|
||||
|
||||
int handleGenStarter(int& i, int argc, char** argv) {
|
||||
std::string base = argv[++i];
|
||||
std::string name = "StarterGossip";
|
||||
if (parseOptArg(i, argc, argv)) name = argv[++i];
|
||||
base = stripWgspExt(base);
|
||||
auto c = wowee::pipeline::WoweeGossipLoader::makeStarter(name);
|
||||
if (!saveOrError(c, base, "gen-gossip")) return 1;
|
||||
printGenSummary(c, base);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int handleGenInnkeeper(int& i, int argc, char** argv) {
|
||||
std::string base = argv[++i];
|
||||
std::string name = "InnkeeperGossip";
|
||||
if (parseOptArg(i, argc, argv)) name = argv[++i];
|
||||
base = stripWgspExt(base);
|
||||
auto c = wowee::pipeline::WoweeGossipLoader::makeInnkeeper(name);
|
||||
if (!saveOrError(c, base, "gen-gossip-innkeeper")) return 1;
|
||||
printGenSummary(c, base);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int handleGenQuestGiver(int& i, int argc, char** argv) {
|
||||
std::string base = argv[++i];
|
||||
std::string name = "QuestGiverGossip";
|
||||
if (parseOptArg(i, argc, argv)) name = argv[++i];
|
||||
base = stripWgspExt(base);
|
||||
auto c = wowee::pipeline::WoweeGossipLoader::makeQuestGiver(name);
|
||||
if (!saveOrError(c, base, "gen-gossip-questgiver")) 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 = stripWgspExt(base);
|
||||
if (!wowee::pipeline::WoweeGossipLoader::exists(base)) {
|
||||
std::fprintf(stderr, "WGSP not found: %s.wgsp\n", base.c_str());
|
||||
return 1;
|
||||
}
|
||||
auto c = wowee::pipeline::WoweeGossipLoader::load(base);
|
||||
if (jsonOut) {
|
||||
nlohmann::json j;
|
||||
j["wgsp"] = base + ".wgsp";
|
||||
j["name"] = c.name;
|
||||
j["count"] = c.entries.size();
|
||||
j["totalOptions"] = totalOptions(c);
|
||||
nlohmann::json arr = nlohmann::json::array();
|
||||
for (const auto& e : c.entries) {
|
||||
nlohmann::json je;
|
||||
je["menuId"] = e.menuId;
|
||||
je["titleText"] = e.titleText;
|
||||
nlohmann::json opts = nlohmann::json::array();
|
||||
for (const auto& o : e.options) {
|
||||
std::string fs;
|
||||
appendOptFlagsStr(fs, o.requiredFlags);
|
||||
opts.push_back({
|
||||
{"optionId", o.optionId},
|
||||
{"text", o.text},
|
||||
{"kind", o.kind},
|
||||
{"kindName", wowee::pipeline::WoweeGossip::optionKindName(o.kind)},
|
||||
{"actionTarget", o.actionTarget},
|
||||
{"requiredFlags", o.requiredFlags},
|
||||
{"requiredFlagsStr", fs},
|
||||
{"moneyCostCopper", o.moneyCostCopper},
|
||||
});
|
||||
}
|
||||
je["options"] = opts;
|
||||
arr.push_back(je);
|
||||
}
|
||||
j["entries"] = arr;
|
||||
std::printf("%s\n", j.dump(2).c_str());
|
||||
return 0;
|
||||
}
|
||||
std::printf("WGSP: %s.wgsp\n", base.c_str());
|
||||
std::printf(" catalog : %s\n", c.name.c_str());
|
||||
std::printf(" menus : %zu (%u options total)\n",
|
||||
c.entries.size(), totalOptions(c));
|
||||
if (c.entries.empty()) return 0;
|
||||
for (const auto& e : c.entries) {
|
||||
std::printf("\n menuId=%u\n", e.menuId);
|
||||
std::printf(" title : \"%s\"\n", e.titleText.c_str());
|
||||
if (e.options.empty()) {
|
||||
std::printf(" *no options*\n");
|
||||
continue;
|
||||
}
|
||||
for (const auto& o : e.options) {
|
||||
std::string fs;
|
||||
appendOptFlagsStr(fs, o.requiredFlags);
|
||||
std::printf(" [%-9s] target=%-5u cost=%-6uc flags=%s\n",
|
||||
wowee::pipeline::WoweeGossip::optionKindName(o.kind),
|
||||
o.actionTarget, o.moneyCostCopper, fs.c_str());
|
||||
std::printf(" \"%s\"\n", o.text.c_str());
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int handleValidate(int& i, int argc, char** argv) {
|
||||
std::string base = argv[++i];
|
||||
bool jsonOut = consumeJsonFlag(i, argc, argv);
|
||||
base = stripWgspExt(base);
|
||||
if (!wowee::pipeline::WoweeGossipLoader::exists(base)) {
|
||||
std::fprintf(stderr,
|
||||
"validate-wgsp: WGSP not found: %s.wgsp\n", base.c_str());
|
||||
return 1;
|
||||
}
|
||||
auto c = wowee::pipeline::WoweeGossipLoader::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());
|
||||
// Build the set of menuIds present so we can verify
|
||||
// intra-format Submenu cross-references resolve.
|
||||
std::vector<uint32_t> menuIds;
|
||||
menuIds.reserve(c.entries.size());
|
||||
for (const auto& e : c.entries) menuIds.push_back(e.menuId);
|
||||
auto hasMenu = [&](uint32_t id) {
|
||||
for (uint32_t m : menuIds) if (m == id) return true;
|
||||
return false;
|
||||
};
|
||||
for (size_t k = 0; k < c.entries.size(); ++k) {
|
||||
const auto& e = c.entries[k];
|
||||
std::string ctx = "entry " + std::to_string(k) +
|
||||
" (menuId=" + std::to_string(e.menuId) + ")";
|
||||
if (e.menuId == 0) {
|
||||
errors.push_back(ctx + ": menuId is 0");
|
||||
}
|
||||
if (e.titleText.empty()) {
|
||||
warnings.push_back(ctx + ": titleText is empty");
|
||||
}
|
||||
if (e.options.empty()) {
|
||||
warnings.push_back(ctx +
|
||||
": no options (player has no way to dismiss the menu)");
|
||||
}
|
||||
// Alliance + Horde restriction is mutually exclusive.
|
||||
for (size_t oi = 0; oi < e.options.size(); ++oi) {
|
||||
const auto& o = e.options[oi];
|
||||
std::string octx = ctx + " option " + std::to_string(oi);
|
||||
if (o.kind > wowee::pipeline::WoweeGossip::Auctioneer) {
|
||||
errors.push_back(octx + ": kind " +
|
||||
std::to_string(o.kind) + " not in known range 0..12");
|
||||
}
|
||||
if (o.text.empty()) {
|
||||
errors.push_back(octx + ": text is empty");
|
||||
}
|
||||
// Submenu must point at a menuId that exists in the catalog.
|
||||
if (o.kind == wowee::pipeline::WoweeGossip::Submenu) {
|
||||
if (o.actionTarget == 0) {
|
||||
errors.push_back(octx + ": Submenu option has actionTarget=0");
|
||||
} else if (!hasMenu(o.actionTarget)) {
|
||||
errors.push_back(octx + ": Submenu actionTarget " +
|
||||
std::to_string(o.actionTarget) +
|
||||
" does not exist in this catalog");
|
||||
}
|
||||
}
|
||||
// Coinpouch flag without moneyCost is misleading — the
|
||||
// coin icon would show with no actual fee.
|
||||
if ((o.requiredFlags & wowee::pipeline::WoweeGossip::Coinpouch) &&
|
||||
o.moneyCostCopper == 0) {
|
||||
warnings.push_back(octx +
|
||||
": Coinpouch flag set but moneyCostCopper=0");
|
||||
}
|
||||
if ((o.requiredFlags & wowee::pipeline::WoweeGossip::AllianceOnly) &&
|
||||
(o.requiredFlags & wowee::pipeline::WoweeGossip::HordeOnly)) {
|
||||
errors.push_back(octx +
|
||||
": AllianceOnly and HordeOnly both set (incoherent)");
|
||||
}
|
||||
}
|
||||
for (uint32_t prev : idsSeen) {
|
||||
if (prev == e.menuId) {
|
||||
errors.push_back(ctx + ": duplicate menuId");
|
||||
break;
|
||||
}
|
||||
}
|
||||
idsSeen.push_back(e.menuId);
|
||||
}
|
||||
bool ok = errors.empty();
|
||||
if (jsonOut) {
|
||||
nlohmann::json j;
|
||||
j["wgsp"] = base + ".wgsp";
|
||||
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-wgsp: %s.wgsp\n", base.c_str());
|
||||
if (ok && warnings.empty()) {
|
||||
std::printf(" OK — %zu menus (%u options), all menuIds unique\n",
|
||||
c.entries.size(), totalOptions(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 handleGossipCatalog(int& i, int argc, char** argv, int& outRc) {
|
||||
if (std::strcmp(argv[i], "--gen-gossip") == 0 && i + 1 < argc) {
|
||||
outRc = handleGenStarter(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--gen-gossip-innkeeper") == 0 && i + 1 < argc) {
|
||||
outRc = handleGenInnkeeper(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--gen-gossip-questgiver") == 0 && i + 1 < argc) {
|
||||
outRc = handleGenQuestGiver(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--info-wgsp") == 0 && i + 1 < argc) {
|
||||
outRc = handleInfo(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--validate-wgsp") == 0 && i + 1 < argc) {
|
||||
outRc = handleValidate(i, argc, argv); return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace cli
|
||||
} // namespace editor
|
||||
} // namespace wowee
|
||||
11
tools/editor/cli_gossip_catalog.hpp
Normal file
11
tools/editor/cli_gossip_catalog.hpp
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#pragma once
|
||||
|
||||
namespace wowee {
|
||||
namespace editor {
|
||||
namespace cli {
|
||||
|
||||
bool handleGossipCatalog(int& i, int argc, char** argv, int& outRc);
|
||||
|
||||
} // namespace cli
|
||||
} // namespace editor
|
||||
} // namespace wowee
|
||||
|
|
@ -1021,6 +1021,16 @@ void printUsage(const char* argv0) {
|
|||
std::printf(" Export binary .wtrn to a human-editable JSON sidecar (defaults to <base>.wtrn.json)\n");
|
||||
std::printf(" --import-wtrn-json <json-path> [out-base]\n");
|
||||
std::printf(" Import a .wtrn.json sidecar back into binary .wtrn (accepts kindMask int OR kindList string array)\n");
|
||||
std::printf(" --gen-gossip <wgsp-base> [name]\n");
|
||||
std::printf(" Emit .wgsp starter: 1 menu with greeting + 3 options (vendor / trainer / close)\n");
|
||||
std::printf(" --gen-gossip-innkeeper <wgsp-base> [name]\n");
|
||||
std::printf(" Emit .wgsp 2-menu innkeeper tree (menuId=4001 closes WCRT.gossipId gap; submenu 4002 area lore)\n");
|
||||
std::printf(" --gen-gossip-questgiver <wgsp-base> [name]\n");
|
||||
std::printf(" Emit .wgsp questgiver menu: 2 quest options + bank + paid respec (Coinpouch flag, 10g)\n");
|
||||
std::printf(" --info-wgsp <wgsp-base> [--json]\n");
|
||||
std::printf(" Print WGSP entries (menuId / title / per-option kind / target / cost / flags)\n");
|
||||
std::printf(" --validate-wgsp <wgsp-base> [--json]\n");
|
||||
std::printf(" Static checks: menuId>0+unique, options non-empty, Submenu actionTarget exists, Coinpouch needs cost, faction conflict\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");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue