From 5f5a696495507da1ef5e8062c51ee0d0d6ac9c44 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 10 May 2026 05:47:23 -0700 Subject: [PATCH] feat(pipeline): WCMD chat slash command catalog (143rd open format) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Novel replacement for the implicit slash-command registry vanilla WoW carried in the client's ChatFrame.lua + server-side per-command CommandHandler hooks (no formal data-driven catalog; commands were registered ad-hoc with hard-coded security checks scattered across LevelMgr / WorldMgr / CharacterMgr). Each WCMD entry binds one command name to its aliases, minimum security level required, argument schema string, help text, per-player throttle in ms, hidden flag (for debug-only commands), and category. Three presets covering the security-tier spectrum: --gen-cmd-basic 4 standard player Info commands (/who /played /time /ginfo) at Player security with no throttle --gen-cmd-movement 3 emote-style Movement commands (/sit /stand /sleep) with short typing-speed aliases ("sd" / "su" / "sd") --gen-cmd-admin 3 GameMaster-only Admin commands (/announce 5s throttle / /kick 2s throttle / /ban 10s throttle — demonstrating per-command rate-limiting to prevent admin-spam abuse) Validator catches: id+command required, minSecurityLevel 0..4, category 0..4, no duplicate cmdIds. CRITICAL: command names AND aliases share one flat namespace (chat parser dispatches uniformly by typed string) — duplicate name across canonical+ aliases errors. Warns on uppercase chars in names (parser is case-insensitive but convention is lowercase), Admin-category command at Player/Helper security level (likely security misconfiguration — admin commands usually require GameMaster+), throttleMs > 60000 (likely ms-vs-s units typo — 60+ second throttle is nearly unusable), self-alias (canonical already matches), and empty helpText (/help would show the command without description). Format count 142 -> 143. CLI flag count 1481 -> 1488. --- CMakeLists.txt | 3 + include/pipeline/wowee_chat_commands.hpp | 134 ++++++++ src/pipeline/wowee_chat_commands.cpp | 293 ++++++++++++++++++ tools/editor/cli_arg_required.cpp | 2 + tools/editor/cli_chat_commands_catalog.cpp | 337 +++++++++++++++++++++ tools/editor/cli_chat_commands_catalog.hpp | 12 + tools/editor/cli_dispatch.cpp | 2 + tools/editor/cli_format_table.cpp | 1 + tools/editor/cli_help.cpp | 10 + tools/editor/cli_list_formats.cpp | 1 + 10 files changed, 795 insertions(+) create mode 100644 include/pipeline/wowee_chat_commands.hpp create mode 100644 src/pipeline/wowee_chat_commands.cpp create mode 100644 tools/editor/cli_chat_commands_catalog.cpp create mode 100644 tools/editor/cli_chat_commands_catalog.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ef2cc246..b23166f7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -731,6 +731,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_battleground_rewards.cpp src/pipeline/wowee_sound_swap.cpp src/pipeline/wowee_tutorial_steps.cpp + src/pipeline/wowee_chat_commands.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1625,6 +1626,7 @@ add_executable(wowee_editor tools/editor/cli_battleground_rewards_catalog.cpp tools/editor/cli_sound_swap_catalog.cpp tools/editor/cli_tutorial_steps_catalog.cpp + tools/editor/cli_chat_commands_catalog.cpp tools/editor/cli_catalog_pluck.cpp tools/editor/cli_catalog_find.cpp tools/editor/cli_catalog_by_name.cpp @@ -1838,6 +1840,7 @@ add_executable(wowee_editor src/pipeline/wowee_battleground_rewards.cpp src/pipeline/wowee_sound_swap.cpp src/pipeline/wowee_tutorial_steps.cpp + src/pipeline/wowee_chat_commands.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_chat_commands.hpp b/include/pipeline/wowee_chat_commands.hpp new file mode 100644 index 00000000..9f251226 --- /dev/null +++ b/include/pipeline/wowee_chat_commands.hpp @@ -0,0 +1,134 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Chat Slash Commands catalog (.wcmd) +// — novel replacement for the implicit slash- +// command registry vanilla WoW carried in the +// client's ChatFrame.lua + server-side per-command +// CommandHandler hooks (no formal data-driven +// catalog; commands were registered ad-hoc with +// hard-coded security checks scattered across +// LevelMgr / WorldMgr / CharacterMgr). Each WCMD +// entry binds one command name (e.g. "who", +// "played", "announce") to its aliases, minimum +// security level required, argument schema string, +// help text, per-player throttle, hidden flag (for +// debug-only commands), and category. +// +// Cross-references with previously-added formats: +// None directly — commands are dispatched by +// handlerKey hash to server-side handlers, but the +// handler code itself lives outside the catalog. +// +// Binary layout (little-endian): +// magic[4] = "WCMD" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// cmdId (uint32) +// commandLen + command (the canonical /name) +// minSecurityLevel (uint8) — 0=Player / +// 1=Helper / +// 2=Moderator / +// 3=GameMaster / +// 4=Admin +// category (uint8) — 0=Info / +// 1=Movement / +// 2=Communication +// /3=Admin / +// 4=Debug +// isHidden (uint8) — 0/1 — debug- +// only commands +// hidden from +// /help listing +// pad0 (uint8) +// throttleMs (uint32) — per-player rate +// limit (0 = no +// throttle) +// argSchemaLen + argSchema +// helpTextLen + helpText +// aliasesCount (uint32) +// aliases (each: stringLen + string) +struct WoweeChatCommands { + enum SecurityLevel : uint8_t { + Player = 0, + Helper = 1, + Moderator = 2, + GameMaster = 3, + Admin = 4, + }; + + enum Category : uint8_t { + Info = 0, + Movement = 1, + Communication = 2, + AdminCmd = 3, + Debug = 4, + }; + + struct Entry { + uint32_t cmdId = 0; + std::string command; + uint8_t minSecurityLevel = Player; + uint8_t category = Info; + uint8_t isHidden = 0; + uint8_t pad0 = 0; + uint32_t throttleMs = 0; + std::string argSchema; + std::string helpText; + std::vector aliases; + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t cmdId) const; + + // Resolves a chat command by typed string — + // matches against canonical command name OR + // any alias. Used by the chat parser hot path. + const Entry* findByCommand(const std::string& cmd) const; + + // Returns all commands a player at the given + // security level can use. The /help UI calls + // this with the player's security level filter. + std::vector findByMinSecurity(uint8_t playerSec) const; +}; + +class WoweeChatCommandsLoader { +public: + static bool save(const WoweeChatCommands& cat, + const std::string& basePath); + static WoweeChatCommands load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-cmd* variants. + // + // makeBasicCommands — 4 standard player-info + // commands (/who /played + // /time /ginfo) all at + // Player security level, + // no throttle. + // makeMovementCommands — 3 emote-style commands + // (/sit /stand /sleep) + // with short aliases. + // makeAdminCommands — 3 admin-only commands + // (/announce /kick /ban) + // at GameMaster security + // level with throttling. + static WoweeChatCommands makeBasicCommands(const std::string& catalogName); + static WoweeChatCommands makeMovementCommands(const std::string& catalogName); + static WoweeChatCommands makeAdminCommands(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_chat_commands.cpp b/src/pipeline/wowee_chat_commands.cpp new file mode 100644 index 00000000..68b9e0db --- /dev/null +++ b/src/pipeline/wowee_chat_commands.cpp @@ -0,0 +1,293 @@ +#include "pipeline/wowee_chat_commands.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'C', 'M', 'D'}; +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) != ".wcmd") { + base += ".wcmd"; + } + return base; +} + +} // namespace + +const WoweeChatCommands::Entry* +WoweeChatCommands::findById(uint32_t cmdId) const { + for (const auto& e : entries) + if (e.cmdId == cmdId) return &e; + return nullptr; +} + +const WoweeChatCommands::Entry* +WoweeChatCommands::findByCommand(const std::string& cmd) const { + for (const auto& e : entries) { + if (e.command == cmd) return &e; + for (const auto& a : e.aliases) { + if (a == cmd) return &e; + } + } + return nullptr; +} + +std::vector +WoweeChatCommands::findByMinSecurity(uint8_t playerSec) const { + std::vector out; + for (const auto& e : entries) { + if (e.minSecurityLevel <= playerSec) + out.push_back(&e); + } + return out; +} + +bool WoweeChatCommandsLoader::save(const WoweeChatCommands& 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.cmdId); + writeStr(os, e.command); + writePOD(os, e.minSecurityLevel); + writePOD(os, e.category); + writePOD(os, e.isHidden); + writePOD(os, e.pad0); + writePOD(os, e.throttleMs); + writeStr(os, e.argSchema); + writeStr(os, e.helpText); + uint32_t aliasCount = + static_cast(e.aliases.size()); + writePOD(os, aliasCount); + for (const auto& a : e.aliases) { + writeStr(os, a); + } + } + return os.good(); +} + +WoweeChatCommands WoweeChatCommandsLoader::load( + const std::string& basePath) { + WoweeChatCommands 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.cmdId)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.command)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.minSecurityLevel) || + !readPOD(is, e.category) || + !readPOD(is, e.isHidden) || + !readPOD(is, e.pad0) || + !readPOD(is, e.throttleMs)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.argSchema) || + !readStr(is, e.helpText)) { + out.entries.clear(); return out; + } + uint32_t aliasCount = 0; + if (!readPOD(is, aliasCount)) { + out.entries.clear(); return out; + } + // Sanity cap — no command should have more + // than 32 aliases. + if (aliasCount > 32) { + out.entries.clear(); return out; + } + e.aliases.resize(aliasCount); + for (auto& a : e.aliases) { + if (!readStr(is, a)) { + out.entries.clear(); return out; + } + } + } + return out; +} + +bool WoweeChatCommandsLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +namespace { + +WoweeChatCommands::Entry makeCmd( + uint32_t cmdId, const char* command, + uint8_t minSec, uint8_t category, uint8_t isHidden, + uint32_t throttleMs, + const char* argSchema, const char* helpText, + std::vector aliases) { + WoweeChatCommands::Entry e; + e.cmdId = cmdId; e.command = command; + e.minSecurityLevel = minSec; + e.category = category; + e.isHidden = isHidden; + e.throttleMs = throttleMs; + e.argSchema = argSchema; + e.helpText = helpText; + e.aliases = std::move(aliases); + return e; +} + +} // namespace + +WoweeChatCommands WoweeChatCommandsLoader::makeBasicCommands( + const std::string& catalogName) { + using W = WoweeChatCommands; + WoweeChatCommands c; + c.name = catalogName; + // Standard player-facing info commands. All + // Player security level. No throttle (used + // freely). Aliases provided where vanilla + // historically supported them. + c.entries.push_back(makeCmd( + 1, "who", W::Player, W::Info, 0, 0, + "[name|class|race|zone]", + "Show players matching the filter " + "(or all if no filter). Capped at 49 " + "results in vanilla.", + {"w"})); + c.entries.push_back(makeCmd( + 2, "played", W::Player, W::Info, 0, 0, + "(no args)", + "Print total /played time for this " + "character + total time at current level.", + {})); + c.entries.push_back(makeCmd( + 3, "time", W::Player, W::Info, 0, 0, + "(no args)", + "Print current server time + local " + "time + uptime.", + {})); + c.entries.push_back(makeCmd( + 4, "ginfo", W::Player, W::Info, 0, 0, + "(no args)", + "Print guild info: name, MOTD, member " + "count online/total, current GM.", + {"guildinfo"})); + return c; +} + +WoweeChatCommands +WoweeChatCommandsLoader::makeMovementCommands( + const std::string& catalogName) { + using W = WoweeChatCommands; + WoweeChatCommands c; + c.name = catalogName; + // Emote-style movement commands. Player level, + // Movement category, no throttle. Each has at + // least one alias for typing speed. + c.entries.push_back(makeCmd( + 10, "sit", W::Player, W::Movement, 0, 0, + "(no args)", + "Sit down. Regenerates health/mana 33% " + "faster while sitting.", + {"sitdown"})); + c.entries.push_back(makeCmd( + 11, "stand", W::Player, W::Movement, 0, 0, + "(no args)", + "Stand up.", + {"standup", "su"})); + c.entries.push_back(makeCmd( + 12, "sleep", W::Player, W::Movement, 0, 0, + "(no args)", + "Lie down to sleep. Cosmetic only.", + {"laydown"})); + return c; +} + +WoweeChatCommands WoweeChatCommandsLoader::makeAdminCommands( + const std::string& catalogName) { + using W = WoweeChatCommands; + WoweeChatCommands c; + c.name = catalogName; + // GameMaster security with rate-limiting to + // prevent admin-spam abuse / accidental + // floods. + c.entries.push_back(makeCmd( + 20, "announce", W::GameMaster, W::AdminCmd, 0, + 5000 /* 5s throttle */, + "", + "Broadcast a server-wide announcement to " + "all online players. 5s per-GM throttle " + "to prevent spam.", + {"broadcast"})); + c.entries.push_back(makeCmd( + 21, "kick", W::GameMaster, W::AdminCmd, 0, + 2000 /* 2s throttle */, + " [reason]", + "Force a player offline. Reason is sent " + "as a system message before disconnect. " + "2s throttle.", + {})); + c.entries.push_back(makeCmd( + 22, "ban", W::GameMaster, W::AdminCmd, 0, + 10000 /* 10s throttle — bans are heavy */, + " ", + "Ban an account for the specified duration. " + "Use durationHours=0 for permanent ban. " + "Reason is logged. 10s throttle.", + {})); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 78dd6032..bbba237d 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -437,6 +437,8 @@ const char* const kArgRequired[] = { "--gen-tut-newbie", "--gen-tut-levelup", "--gen-tut-bg", "--info-wtur", "--validate-wtur", "--export-wtur-json", "--import-wtur-json", + "--gen-cmd-basic", "--gen-cmd-movement", "--gen-cmd-admin", + "--info-wcmd", "--validate-wcmd", "--gen-weather-temperate", "--gen-weather-arctic", "--gen-weather-desert", "--gen-weather-stormy", "--gen-zone-atmosphere", diff --git a/tools/editor/cli_chat_commands_catalog.cpp b/tools/editor/cli_chat_commands_catalog.cpp new file mode 100644 index 00000000..968c85e4 --- /dev/null +++ b/tools/editor/cli_chat_commands_catalog.cpp @@ -0,0 +1,337 @@ +#include "cli_chat_commands_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_chat_commands.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWcmdExt(std::string base) { + stripExt(base, ".wcmd"); + return base; +} + +const char* securityLevelName(uint8_t s) { + using W = wowee::pipeline::WoweeChatCommands; + switch (s) { + case W::Player: return "player"; + case W::Helper: return "helper"; + case W::Moderator: return "moderator"; + case W::GameMaster: return "gamemaster"; + case W::Admin: return "admin"; + default: return "?"; + } +} + +const char* categoryName(uint8_t c) { + using W = wowee::pipeline::WoweeChatCommands; + switch (c) { + case W::Info: return "info"; + case W::Movement: return "movement"; + case W::Communication: return "communication"; + case W::AdminCmd: return "admincmd"; + case W::Debug: return "debug"; + default: return "?"; + } +} + +bool saveOrError(const wowee::pipeline::WoweeChatCommands& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeChatCommandsLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wcmd\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeChatCommands& c, + const std::string& base) { + std::printf("Wrote %s.wcmd\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" commands: %zu\n", c.entries.size()); +} + +int handleGenBasic(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "BasicChatCommands"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWcmdExt(base); + auto c = wowee::pipeline::WoweeChatCommandsLoader:: + makeBasicCommands(name); + if (!saveOrError(c, base, "gen-cmd-basic")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenMovement(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "MovementChatCommands"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWcmdExt(base); + auto c = wowee::pipeline::WoweeChatCommandsLoader:: + makeMovementCommands(name); + if (!saveOrError(c, base, "gen-cmd-movement")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenAdmin(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "AdminChatCommands"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWcmdExt(base); + auto c = wowee::pipeline::WoweeChatCommandsLoader:: + makeAdminCommands(name); + if (!saveOrError(c, base, "gen-cmd-admin")) 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 = stripWcmdExt(base); + if (!wowee::pipeline::WoweeChatCommandsLoader::exists(base)) { + std::fprintf(stderr, "WCMD not found: %s.wcmd\n", + base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeChatCommandsLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wcmd"] = base + ".wcmd"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + arr.push_back({ + {"cmdId", e.cmdId}, + {"command", e.command}, + {"minSecurityLevel", e.minSecurityLevel}, + {"minSecurityLevelName", + securityLevelName(e.minSecurityLevel)}, + {"category", e.category}, + {"categoryName", + categoryName(e.category)}, + {"isHidden", e.isHidden != 0}, + {"throttleMs", e.throttleMs}, + {"argSchema", e.argSchema}, + {"helpText", e.helpText}, + {"aliases", e.aliases}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WCMD: %s.wcmd\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" commands: %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id /command security category hide throttleMs aliases argSchema\n"); + for (const auto& e : c.entries) { + std::printf(" %4u /%-15s %-10s %-13s %s %8u %5zu %s\n", + e.cmdId, e.command.c_str(), + securityLevelName(e.minSecurityLevel), + categoryName(e.category), + e.isHidden ? "Y" : "n", + e.throttleMs, + e.aliases.size(), + e.argSchema.c_str()); + } + return 0; +} + +int handleValidate(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWcmdExt(base); + if (!wowee::pipeline::WoweeChatCommandsLoader::exists(base)) { + std::fprintf(stderr, + "validate-wcmd: WCMD not found: %s.wcmd\n", + base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeChatCommandsLoader::load(base); + std::vector errors; + std::vector warnings; + if (c.entries.empty()) { + warnings.push_back("catalog has zero entries"); + } + std::set idsSeen; + // Per-name uniqueness: tracks ALL command names + // (canonical + aliases) since chat parser + // dispatches uniformly. + std::set allNamesSeen; + auto addName = [&](const std::string& nm, + const std::string& ctx, + const std::string& source) { + if (nm.empty()) return; + if (!allNamesSeen.insert(nm).second) { + errors.push_back(ctx + + ": " + source + " '" + nm + + "' collides with another command name " + "or alias — chat parser would dispatch " + "ambiguously"); + } + // Lowercase check: chat parser is case- + // insensitive but storing canonical lowercase + // is the convention. + for (char ch : nm) { + if (ch >= 'A' && ch <= 'Z') { + warnings.push_back(ctx + + ": " + source + " '" + nm + + "' contains uppercase — convention " + "is canonical lowercase (chat parser " + "is case-insensitive)"); + break; + } + } + }; + for (size_t k = 0; k < c.entries.size(); ++k) { + const auto& e = c.entries[k]; + std::string ctx = "entry " + std::to_string(k) + + " (cmdId=" + std::to_string(e.cmdId); + if (!e.command.empty()) + ctx += " /" + e.command; + ctx += ")"; + if (e.cmdId == 0) + errors.push_back(ctx + ": cmdId is 0"); + if (e.command.empty()) + errors.push_back(ctx + + ": command name is empty"); + if (e.minSecurityLevel > 4) { + errors.push_back(ctx + + ": minSecurityLevel " + + std::to_string(e.minSecurityLevel) + + " out of range (0..4)"); + } + if (e.category > 4) { + errors.push_back(ctx + ": category " + + std::to_string(e.category) + + " out of range (0..4)"); + } + if (e.helpText.empty()) { + warnings.push_back(ctx + + ": helpText is empty — /help would " + "show this command without " + "description"); + } + // Throttle > 60s is almost certainly a typo + // (units mismatch — milliseconds vs seconds). + if (e.throttleMs > 60000) { + warnings.push_back(ctx + + ": throttleMs=" + + std::to_string(e.throttleMs) + + " exceeds 60000ms (60s) — verify " + "intentional or check units (ms vs s " + "typo)"); + } + // Admin-category command at Player security + // level is a security hole — warn. + using W = wowee::pipeline::WoweeChatCommands; + if (e.category == W::AdminCmd && + e.minSecurityLevel <= W::Helper) { + warnings.push_back(ctx + + ": Admin category command at security " + "level " + + std::to_string(e.minSecurityLevel) + + " (Player/Helper) — likely security " + "misconfiguration; admin commands " + "usually require GameMaster+"); + } + // Per-name uniqueness check (canonical + + // aliases share same flat namespace). + addName(e.command, ctx, "command"); + for (const auto& a : e.aliases) { + addName(a, ctx, "alias"); + // Self-alias is meaningless (canonical + // already matches). + if (a == e.command) { + warnings.push_back(ctx + + ": alias '" + a + "' equals " + "canonical command name — " + "redundant entry"); + } + } + if (!idsSeen.insert(e.cmdId).second) { + errors.push_back(ctx + ": duplicate cmdId"); + } + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wcmd"] = base + ".wcmd"; + 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-wcmd: %s.wcmd\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu commands, all cmdIds + " + "names + aliases unique across flat " + "namespace, security 0..4, category " + "0..4, all command/alias names " + "lowercase\n", + c.entries.size()); + return 0; + } + if (!warnings.empty()) { + std::printf(" warnings (%zu):\n", warnings.size()); + for (const auto& w : warnings) + std::printf(" - %s\n", w.c_str()); + } + if (!errors.empty()) { + std::printf(" ERRORS (%zu):\n", errors.size()); + for (const auto& e : errors) + std::printf(" - %s\n", e.c_str()); + } + return ok ? 0 : 1; +} + +} // namespace + +bool handleChatCommandsCatalog(int& i, int argc, char** argv, + int& outRc) { + if (std::strcmp(argv[i], "--gen-cmd-basic") == 0 && + i + 1 < argc) { + outRc = handleGenBasic(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-cmd-movement") == 0 && + i + 1 < argc) { + outRc = handleGenMovement(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-cmd-admin") == 0 && + i + 1 < argc) { + outRc = handleGenAdmin(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wcmd") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wcmd") == 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_chat_commands_catalog.hpp b/tools/editor/cli_chat_commands_catalog.hpp new file mode 100644 index 00000000..f8b1506f --- /dev/null +++ b/tools/editor/cli_chat_commands_catalog.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleChatCommandsCatalog(int& i, int argc, char** argv, + int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_dispatch.cpp b/tools/editor/cli_dispatch.cpp index 6aa997c0..c24c4536 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -187,6 +187,7 @@ #include "cli_battleground_rewards_catalog.hpp" #include "cli_sound_swap_catalog.hpp" #include "cli_tutorial_steps_catalog.hpp" +#include "cli_chat_commands_catalog.hpp" #include "cli_catalog_pluck.hpp" #include "cli_catalog_find.hpp" #include "cli_catalog_by_name.hpp" @@ -419,6 +420,7 @@ constexpr DispatchFn kDispatchTable[] = { handleBattlegroundRewardsCatalog, handleSoundSwapCatalog, handleTutorialStepsCatalog, + handleChatCommandsCatalog, handleCatalogPluck, handleCatalogFind, handleCatalogByName, diff --git a/tools/editor/cli_format_table.cpp b/tools/editor/cli_format_table.cpp index 67fe91bb..08440199 100644 --- a/tools/editor/cli_format_table.cpp +++ b/tools/editor/cli_format_table.cpp @@ -145,6 +145,7 @@ constexpr FormatMagicEntry kFormats[] = { {{'W','B','R','D'}, ".wbrd", "pvp", "--info-wbrd", "Battleground reward stages catalog"}, {{'W','S','W','P'}, ".wswp", "audio", "--info-wswp", "Sound swap rules catalog"}, {{'W','T','U','R'}, ".wtur", "ui", "--info-wtur", "Tutorial steps catalog"}, + {{'W','C','M','D'}, ".wcmd", "chat", "--info-wcmd", "Chat slash command catalog"}, {{'W','F','A','C'}, ".wfac", "factions", nullptr, "Faction catalog"}, {{'W','L','C','K'}, ".wlck", "locks", nullptr, "Lock catalog"}, {{'W','S','K','L'}, ".wskl", "skills", nullptr, "Skill catalog"}, diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index 01157f11..f50e6b54 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -2755,6 +2755,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wtur to a human-editable JSON sidecar (defaults to .wtur.json; emits triggerEvent as int + name string; multibyte strings preserved)\n"); std::printf(" --import-wtur-json [out-base]\n"); std::printf(" Import a .wtur.json sidecar back into binary .wtur (triggerEvent int OR \"login\"/\"zoneenter\"/\"levelup\"/\"itempickup\"/\"skilltrain\" — round-trips title/body/target text byte-identical)\n"); + std::printf(" --gen-cmd-basic [name]\n"); + std::printf(" Emit .wcmd 4 standard player Info commands (/who /played /time /ginfo) all at Player security level, no throttle\n"); + std::printf(" --gen-cmd-movement [name]\n"); + std::printf(" Emit .wcmd 3 emote-style Movement commands (/sit /stand /sleep) with short aliases\n"); + std::printf(" --gen-cmd-admin [name]\n"); + std::printf(" Emit .wcmd 3 GameMaster-only Admin commands (/announce 5s throttle / /kick 2s throttle / /ban 10s throttle) demonstrating per-command rate-limiting\n"); + std::printf(" --info-wcmd [--json]\n"); + std::printf(" Print WCMD entries (id / /command / minSecurityLevel / category / hidden / throttleMs / aliases count / argSchema)\n"); + std::printf(" --validate-wcmd [--json]\n"); + std::printf(" Static checks: id+command required, minSecurityLevel 0..4, category 0..4, no duplicate cmdIds; CRITICAL: command names + aliases share one flat namespace (chat parser dispatches uniformly) — duplicate name across canonical+aliases errors. Warns on uppercase chars (parser is case-insensitive but convention is canonical lowercase), Admin-category command at Player/Helper security (likely security misconfiguration), throttleMs > 60000 (likely ms-vs-s units typo), self-alias (canonical already matches), and empty helpText (/help would show no description)\n"); std::printf(" --catalog-pluck [--json]\n"); std::printf(" Extract one entry by id from any registered catalog format. Auto-detects magic, dispatches to the per-format --info-* handler internally, then prints just the matching entry. Primary-key field is auto-detected (first *Id field, or first numeric)\n"); std::printf(" --catalog-find [--magic ] [--json]\n"); diff --git a/tools/editor/cli_list_formats.cpp b/tools/editor/cli_list_formats.cpp index 4f19a4a9..88b50039 100644 --- a/tools/editor/cli_list_formats.cpp +++ b/tools/editor/cli_list_formats.cpp @@ -167,6 +167,7 @@ constexpr FormatRow kFormats[] = { {"WBRD", ".wbrd", "pvp", "BattlemasterList.dbc + BattlegroundMgr","Battleground reward stages catalog (per-bracket honor + marks)"}, {"WSWP", ".wswp", "audio", "(absent in vanilla — patch-level edits)","Sound swap rules catalog (priority + condition-gated substitution)"}, {"WTUR", ".wtur", "ui", "TutorialFrame.xml + Tutorial.lua", "Tutorial steps catalog (event-triggered first-time-player popup sequences)"}, + {"WCMD", ".wcmd", "chat", "ChatFrame.lua + per-command CommandHandler","Chat slash command catalog (security-gated registry + aliases + throttle)"}, // Additional pipeline catalogs without the alternating // gen/info/validate CLI surface (loaded by the engine