From 383d8d730ab59dd409325b3f8dfaca6df81778bd Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 20:00:56 -0700 Subject: [PATCH] feat(pipeline): add WGTP (Wowee Game Tips) catalog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 52nd open format — replaces GameTips.dbc plus loading-screen tutorial hint tables. Defines the rotating tips shown during world loads, the contextual tutorial hints that fire on first gameplay events (first quest accept, first death, first dungeon entry), and the persistent tooltip-help strings that explain UI elements. 4 display kinds (LoadingScreen / Tutorial / TooltipHelp / Hint), 7 audience-filter bits (Alliance / Horde / NewPlayer / Hardcore / PvE / PvP / Roleplay) for pool selection, level range gating (minLevel + maxLevel), displayWeight for relative frequency within the pool, optional WPCN condition cross-ref for further gating, and class-mask restriction matching WCHC bit positions. Cross-references with prior formats — conditionId points at WPCN.conditionId for advanced gating; requiredClassMask uses the same WCHC.classId bit layout as WGLY/WSET. CLI: --gen-tips (3 generic loading-screen tips), --gen-tips- new-player (5 onboarding Tutorial-kind tips for level 1-15, weighted higher for new players), --gen-tips-advanced (4 endgame tips for level 70+ covering raid mechanics / arena / daily professions / dungeon finder), --info-wgtp, --validate-wgtp with --json variants. Validator catches id/name/text required, kind 0..3, audienceFilter=0 (tip never shown), invalid level range, displayWeight=0 (in pool but never picked) warning, and brevity check (>280 chars) on Tutorial / Hint kinds that need to fit on screen. Format graph: 51 → 52 binary formats. CLI flag count: 770 → 775. --- CMakeLists.txt | 3 + include/pipeline/wowee_game_tips.hpp | 114 +++++++++++ src/pipeline/wowee_game_tips.cpp | 253 +++++++++++++++++++++++++ tools/editor/cli_arg_required.cpp | 2 + tools/editor/cli_dispatch.cpp | 2 + tools/editor/cli_format_table.cpp | 1 + tools/editor/cli_game_tips_catalog.cpp | 245 ++++++++++++++++++++++++ tools/editor/cli_game_tips_catalog.hpp | 11 ++ tools/editor/cli_help.cpp | 10 + tools/editor/cli_list_formats.cpp | 1 + 10 files changed, 642 insertions(+) create mode 100644 include/pipeline/wowee_game_tips.hpp create mode 100644 src/pipeline/wowee_game_tips.cpp create mode 100644 tools/editor/cli_game_tips_catalog.cpp create mode 100644 tools/editor/cli_game_tips_catalog.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 76ba8b31..b5b2cd49 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -640,6 +640,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_trade_skills.cpp src/pipeline/wowee_creature_equipment.cpp src/pipeline/wowee_item_sets.cpp + src/pipeline/wowee_game_tips.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1432,6 +1433,7 @@ add_executable(wowee_editor tools/editor/cli_creature_equipment_catalog.cpp tools/editor/cli_item_sets_catalog.cpp tools/editor/cli_touch_tree.cpp + tools/editor/cli_game_tips_catalog.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp @@ -1550,6 +1552,7 @@ add_executable(wowee_editor src/pipeline/wowee_trade_skills.cpp src/pipeline/wowee_creature_equipment.cpp src/pipeline/wowee_item_sets.cpp + src/pipeline/wowee_game_tips.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_game_tips.hpp b/include/pipeline/wowee_game_tips.hpp new file mode 100644 index 00000000..d9524d25 --- /dev/null +++ b/include/pipeline/wowee_game_tips.hpp @@ -0,0 +1,114 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Game Tips catalog (.wgtp) — novel replacement +// for Blizzard's GameTips.dbc plus loading-screen tutorial +// hint tables. Defines the rotating tips shown during world +// loads, the contextual tutorial hints that fire on first +// gameplay events (first quest accept, first death, first +// dungeon entry), and the persistent tooltip-help strings +// that explain UI elements. +// +// Each tip has filter criteria — audience bitmask (faction / +// new-player / hardcore), level range, optional class mask, +// optional WPCN condition cross-ref — that the runtime uses +// to pick the right pool of tips for the current player. +// displayWeight controls relative frequency within the pool. +// +// Cross-references with previously-added formats: +// WGTP.entry.conditionId → WPCN.entry.conditionId +// (further gate beyond audience) +// WGTP.entry.requiredClassMask bit positions match +// WCHC.class.classId +// +// Binary layout (little-endian): +// magic[4] = "WGTP" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// tipId (uint32) +// nameLen + name +// textLen + text +// iconLen + iconPath +// displayKind (uint8) / pad[3] +// audienceFilter (uint32) +// minLevel (uint16) / maxLevel (uint16) +// displayWeight (uint16) / pad[2] +// conditionId (uint32) +// requiredClassMask (uint32) +struct WoweeGameTip { + enum DisplayKind : uint8_t { + LoadingScreen = 0, // long load-time scrolling tip + Tutorial = 1, // contextual modal on first event + TooltipHelp = 2, // persistent UI element help + Hint = 3, // brief on-screen flyout + }; + + // audienceFilter bits — combine with bitwise OR to broaden. + static constexpr uint32_t kAudienceAlliance = 1u << 0; + static constexpr uint32_t kAudienceHorde = 1u << 1; + static constexpr uint32_t kAudienceNewPlayer = 1u << 2; + static constexpr uint32_t kAudienceHardcore = 1u << 3; + static constexpr uint32_t kAudiencePvE = 1u << 4; + static constexpr uint32_t kAudiencePvP = 1u << 5; + static constexpr uint32_t kAudienceRoleplay = 1u << 6; + static constexpr uint32_t kAudienceAll = 0xFFFFFFFFu; + + struct Entry { + uint32_t tipId = 0; + std::string name; // internal stable id ("FirstQuest") + std::string text; // the displayed text + std::string iconPath; + uint8_t displayKind = LoadingScreen; + uint32_t audienceFilter = kAudienceAll; + uint16_t minLevel = 1; + uint16_t maxLevel = 80; + uint16_t displayWeight = 1; // relative frequency + uint32_t conditionId = 0; // WPCN cross-ref (0 = none) + uint32_t requiredClassMask = 0; // 0 = any class + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t tipId) const; + + static const char* displayKindName(uint8_t k); +}; + +class WoweeGameTipLoader { +public: + static bool save(const WoweeGameTip& cat, + const std::string& basePath); + static WoweeGameTip load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-tips* variants. + // + // makeStarter — 3 generic loading-screen tips + // (combat hint / movement hint / + // quest hint) — kAudienceAll, no + // condition gate. + // makeNewPlayer — 5 onboarding tips for level 1-15 + // players (kAudienceNewPlayer bit + // set), Tutorial display kind. + // makeAdvanced — 4 tips for max-level players + // (raid mechanics / PvP mechanics / + // profession dailies / dungeon-finder + // etiquette) gated by minLevel 70+. + static WoweeGameTip makeStarter(const std::string& catalogName); + static WoweeGameTip makeNewPlayer(const std::string& catalogName); + static WoweeGameTip makeAdvanced(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_game_tips.cpp b/src/pipeline/wowee_game_tips.cpp new file mode 100644 index 00000000..f420b4b2 --- /dev/null +++ b/src/pipeline/wowee_game_tips.cpp @@ -0,0 +1,253 @@ +#include "pipeline/wowee_game_tips.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'G', 'T', 'P'}; +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) != ".wgtp") { + base += ".wgtp"; + } + return base; +} + +} // namespace + +const WoweeGameTip::Entry* +WoweeGameTip::findById(uint32_t tipId) const { + for (const auto& e : entries) if (e.tipId == tipId) return &e; + return nullptr; +} + +const char* WoweeGameTip::displayKindName(uint8_t k) { + switch (k) { + case LoadingScreen: return "loading-screen"; + case Tutorial: return "tutorial"; + case TooltipHelp: return "tooltip-help"; + case Hint: return "hint"; + default: return "unknown"; + } +} + +bool WoweeGameTipLoader::save(const WoweeGameTip& 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.tipId); + writeStr(os, e.name); + writeStr(os, e.text); + writeStr(os, e.iconPath); + writePOD(os, e.displayKind); + uint8_t pad3[3] = {0, 0, 0}; + os.write(reinterpret_cast(pad3), 3); + writePOD(os, e.audienceFilter); + writePOD(os, e.minLevel); + writePOD(os, e.maxLevel); + writePOD(os, e.displayWeight); + uint8_t pad2[2] = {0, 0}; + os.write(reinterpret_cast(pad2), 2); + writePOD(os, e.conditionId); + writePOD(os, e.requiredClassMask); + } + return os.good(); +} + +WoweeGameTip WoweeGameTipLoader::load(const std::string& basePath) { + WoweeGameTip 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.tipId)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.name) || !readStr(is, e.text) || + !readStr(is, e.iconPath)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.displayKind)) { + out.entries.clear(); return out; + } + uint8_t pad3[3]; + is.read(reinterpret_cast(pad3), 3); + if (is.gcount() != 3) { out.entries.clear(); return out; } + if (!readPOD(is, e.audienceFilter) || + !readPOD(is, e.minLevel) || + !readPOD(is, e.maxLevel) || + !readPOD(is, e.displayWeight)) { + out.entries.clear(); return out; + } + uint8_t pad2[2]; + is.read(reinterpret_cast(pad2), 2); + if (is.gcount() != 2) { out.entries.clear(); return out; } + if (!readPOD(is, e.conditionId) || + !readPOD(is, e.requiredClassMask)) { + out.entries.clear(); return out; + } + } + return out; +} + +bool WoweeGameTipLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeGameTip WoweeGameTipLoader::makeStarter( + const std::string& catalogName) { + WoweeGameTip c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, const char* text, + uint16_t weight) { + WoweeGameTip::Entry e; + e.tipId = id; e.name = name; e.text = text; + e.iconPath = "Interface/TipOfTheDay/icon_generic.blp"; + e.displayKind = WoweeGameTip::LoadingScreen; + e.displayWeight = weight; + c.entries.push_back(e); + }; + add(1, "CombatHint", + "Press to cycle through nearby enemies. " + "Right-click to attack.", 1); + add(2, "MovementHint", + "Hold both mouse buttons to move forward without " + "pressing W. Hold right-click to steer with the mouse.", 1); + add(3, "QuestHint", + "Yellow exclamation marks (!) above NPCs mean a " + "quest is available. Yellow question marks (?) mean " + "a quest is ready to turn in.", 2); + return c; +} + +WoweeGameTip WoweeGameTipLoader::makeNewPlayer( + const std::string& catalogName) { + WoweeGameTip c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, const char* text, + uint16_t maxLevel) { + WoweeGameTip::Entry e; + e.tipId = id; e.name = name; e.text = text; + e.iconPath = "Interface/TipOfTheDay/icon_newplayer.blp"; + e.displayKind = WoweeGameTip::Tutorial; + e.audienceFilter = WoweeGameTip::kAudienceNewPlayer | + WoweeGameTip::kAudienceAlliance | + WoweeGameTip::kAudienceHorde; + e.minLevel = 1; + e.maxLevel = maxLevel; + e.displayWeight = 5; // weighted higher for new players + c.entries.push_back(e); + }; + add(100, "BindHearthstone", + "Visit an innkeeper to bind your Hearthstone — it's " + "the easiest way to return home.", 10); + add(101, "TalentSpec", + "At level 10 you can spend talent points. Visit your " + "class trainer to learn how.", 15); + add(102, "FirstMount", + "At level 20 you can ride a mount! Save 1 gold and " + "visit a mount vendor in your faction's capital.", 25); + add(103, "QuestLog", + "Press 'L' to open your quest log. You can track up " + "to 25 active quests at once.", 15); + add(104, "ProfessionPick", + "Visit a profession trainer to learn a primary trade. " + "You can have two primary professions.", 15); + return c; +} + +WoweeGameTip WoweeGameTipLoader::makeAdvanced( + const std::string& catalogName) { + WoweeGameTip c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, const char* text, + uint8_t kind, uint32_t audience, uint32_t cond, + uint16_t weight) { + WoweeGameTip::Entry e; + e.tipId = id; e.name = name; e.text = text; + e.iconPath = "Interface/TipOfTheDay/icon_advanced.blp"; + e.displayKind = kind; + e.audienceFilter = audience; + e.minLevel = 70; + e.maxLevel = 80; + e.conditionId = cond; + e.displayWeight = weight; + c.entries.push_back(e); + }; + add(200, "RaidMechanic", + "Raid bosses telegraph their abilities — watch for " + "ground markers and mechanic announcements.", + WoweeGameTip::Hint, WoweeGameTip::kAudiencePvE, 0, 3); + add(201, "PvPArena", + "Arena teams require a charter signed by 4 players. " + "Visit an Arena Battlemaster to start one.", + WoweeGameTip::TooltipHelp, WoweeGameTip::kAudiencePvP, 0, 2); + add(202, "DailyProfession", + "Some professions have daily quests at exalted with " + "your faction. Check Shattrath and Dalaran daily.", + WoweeGameTip::LoadingScreen, + WoweeGameTip::kAudienceAll, 0, 2); + add(203, "DungeonFinder", + "Press 'I' to open the Dungeon Finder. It will form a " + "group across servers and teleport you to the dungeon.", + WoweeGameTip::Tutorial, + WoweeGameTip::kAudienceAll, 0, 4); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 7f74269e..2a9e48ce 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -157,6 +157,8 @@ const char* const kArgRequired[] = { "--gen-itset", "--gen-itset-tier", "--gen-itset-pvp", "--info-wset", "--validate-wset", "--export-wset-json", "--import-wset-json", + "--gen-tips", "--gen-tips-new-player", "--gen-tips-advanced", + "--info-wgtp", "--validate-wgtp", "--gen-weather-temperate", "--gen-weather-arctic", "--gen-weather-desert", "--gen-weather-stormy", "--gen-zone-atmosphere", diff --git a/tools/editor/cli_dispatch.cpp b/tools/editor/cli_dispatch.cpp index a3bacdd8..ade533ac 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -85,6 +85,7 @@ #include "cli_creature_equipment_catalog.hpp" #include "cli_item_sets_catalog.hpp" #include "cli_touch_tree.hpp" +#include "cli_game_tips_catalog.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -211,6 +212,7 @@ constexpr DispatchFn kDispatchTable[] = { handleCreatureEquipmentCatalog, handleItemSetsCatalog, handleTouchTree, + handleGameTipsCatalog, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_format_table.cpp b/tools/editor/cli_format_table.cpp index d3d5eb53..e56e94a9 100644 --- a/tools/editor/cli_format_table.cpp +++ b/tools/editor/cli_format_table.cpp @@ -54,6 +54,7 @@ constexpr FormatMagicEntry kFormats[] = { {{'W','T','S','K'}, ".wtsk", "crafting", "--info-wtsk", "Trade skill recipe catalog"}, {{'W','C','E','Q'}, ".wceq", "creatures", "--info-wceq", "Creature equipment loadout catalog"}, {{'W','S','E','T'}, ".wset", "items", "--info-wset", "Item set / tier-bonus catalog"}, + {{'W','G','T','P'}, ".wgtp", "ui", "--info-wgtp", "Game tips / tutorial 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_game_tips_catalog.cpp b/tools/editor/cli_game_tips_catalog.cpp new file mode 100644 index 00000000..d473baac --- /dev/null +++ b/tools/editor/cli_game_tips_catalog.cpp @@ -0,0 +1,245 @@ +#include "cli_game_tips_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_game_tips.hpp" +#include + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWgtpExt(std::string base) { + stripExt(base, ".wgtp"); + return base; +} + +bool saveOrError(const wowee::pipeline::WoweeGameTip& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeGameTipLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wgtp\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeGameTip& c, + const std::string& base) { + std::printf("Wrote %s.wgtp\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" tips : %zu\n", c.entries.size()); +} + +int handleGenStarter(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "StarterTips"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWgtpExt(base); + auto c = wowee::pipeline::WoweeGameTipLoader::makeStarter(name); + if (!saveOrError(c, base, "gen-tips")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenNewPlayer(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "NewPlayerTips"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWgtpExt(base); + auto c = wowee::pipeline::WoweeGameTipLoader::makeNewPlayer(name); + if (!saveOrError(c, base, "gen-tips-new-player")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenAdvanced(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "AdvancedTips"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWgtpExt(base); + auto c = wowee::pipeline::WoweeGameTipLoader::makeAdvanced(name); + if (!saveOrError(c, base, "gen-tips-advanced")) 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 = stripWgtpExt(base); + if (!wowee::pipeline::WoweeGameTipLoader::exists(base)) { + std::fprintf(stderr, "WGTP not found: %s.wgtp\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeGameTipLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wgtp"] = base + ".wgtp"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + arr.push_back({ + {"tipId", e.tipId}, + {"name", e.name}, + {"text", e.text}, + {"iconPath", e.iconPath}, + {"displayKind", e.displayKind}, + {"displayKindName", wowee::pipeline::WoweeGameTip::displayKindName(e.displayKind)}, + {"audienceFilter", e.audienceFilter}, + {"minLevel", e.minLevel}, + {"maxLevel", e.maxLevel}, + {"displayWeight", e.displayWeight}, + {"conditionId", e.conditionId}, + {"requiredClassMask", e.requiredClassMask}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WGTP: %s.wgtp\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" tips : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id kind audience levels wt cond classMask name\n"); + for (const auto& e : c.entries) { + std::printf(" %4u %-13s 0x%08x %3u-%3u %3u %5u 0x%08x %s\n", + e.tipId, + wowee::pipeline::WoweeGameTip::displayKindName(e.displayKind), + e.audienceFilter, + e.minLevel, e.maxLevel, + e.displayWeight, e.conditionId, + e.requiredClassMask, + e.name.c_str()); + } + return 0; +} + +int handleValidate(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWgtpExt(base); + if (!wowee::pipeline::WoweeGameTipLoader::exists(base)) { + std::fprintf(stderr, + "validate-wgtp: WGTP not found: %s.wgtp\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeGameTipLoader::load(base); + std::vector errors; + std::vector warnings; + if (c.entries.empty()) { + warnings.push_back("catalog has zero entries"); + } + std::vector idsSeen; + for (size_t k = 0; k < c.entries.size(); ++k) { + const auto& e = c.entries[k]; + std::string ctx = "entry " + std::to_string(k) + + " (id=" + std::to_string(e.tipId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.tipId == 0) + errors.push_back(ctx + ": tipId is 0"); + if (e.name.empty()) + errors.push_back(ctx + ": name is empty"); + if (e.text.empty()) + errors.push_back(ctx + ": text is empty"); + if (e.displayKind > wowee::pipeline::WoweeGameTip::Hint) { + errors.push_back(ctx + ": displayKind " + + std::to_string(e.displayKind) + " not in 0..3"); + } + if (e.audienceFilter == 0) { + errors.push_back(ctx + + ": audienceFilter=0 (tip would never be shown)"); + } + if (e.minLevel > e.maxLevel) { + errors.push_back(ctx + ": minLevel " + + std::to_string(e.minLevel) + " > maxLevel " + + std::to_string(e.maxLevel)); + } + if (e.displayWeight == 0) { + warnings.push_back(ctx + + ": displayWeight=0 (tip is in pool but never picked)"); + } + // Tutorial / Hint kinds typically need to be brief — + // > 280 characters won't fit cleanly on screen. + bool brief = e.displayKind == wowee::pipeline::WoweeGameTip::Tutorial || + e.displayKind == wowee::pipeline::WoweeGameTip::Hint; + if (brief && e.text.size() > 280) { + warnings.push_back(ctx + + ": text length " + std::to_string(e.text.size()) + + " exceeds 280 chars (tutorial/hint should be brief)"); + } + for (uint32_t prev : idsSeen) { + if (prev == e.tipId) { + errors.push_back(ctx + ": duplicate tipId"); + break; + } + } + idsSeen.push_back(e.tipId); + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wgtp"] = base + ".wgtp"; + 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-wgtp: %s.wgtp\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu tips, all tipIds unique, all level ranges valid\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 handleGameTipsCatalog(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--gen-tips") == 0 && i + 1 < argc) { + outRc = handleGenStarter(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-tips-new-player") == 0 && + i + 1 < argc) { + outRc = handleGenNewPlayer(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-tips-advanced") == 0 && + i + 1 < argc) { + outRc = handleGenAdvanced(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wgtp") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wgtp") == 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_game_tips_catalog.hpp b/tools/editor/cli_game_tips_catalog.hpp new file mode 100644 index 00000000..52e3a65a --- /dev/null +++ b/tools/editor/cli_game_tips_catalog.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleGameTipsCatalog(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index f1e39d0d..a079cb59 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1457,6 +1457,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wset to a human-editable JSON sidecar with nested itemIds[] + bonuses[] arrays\n"); std::printf(" --import-wset-json [out-base]\n"); std::printf(" Import a .wset.json sidecar back into binary .wset (pieceCount/bonusCount derived from array sizes; missing slots cleared)\n"); + std::printf(" --gen-tips [name]\n"); + std::printf(" Emit .wgtp starter: 3 generic loading-screen tips (combat / movement / quest hints) for kAudienceAll\n"); + std::printf(" --gen-tips-new-player [name]\n"); + std::printf(" Emit .wgtp 5 onboarding tutorial tips (hearthstone / talents / mount / quest log / professions) for level 1-15\n"); + std::printf(" --gen-tips-advanced [name]\n"); + std::printf(" Emit .wgtp 4 endgame tips (raid mechanics / arena / daily professions / dungeon finder) for level 70+\n"); + std::printf(" --info-wgtp [--json]\n"); + std::printf(" Print WGTP entries (id / kind / audience mask / level range / weight / condition / classMask / name)\n"); + std::printf(" --validate-wgtp [--json]\n"); + std::printf(" Static checks: id+name+text required, kind 0..3, audienceFilter>0, valid level range, brevity check on tutorial/hint kinds\n"); std::printf(" --gen-weather-temperate [zoneName]\n"); std::printf(" Emit .wow weather schedule: clear-dominant + occasional rain + fog (forest / grassland)\n"); std::printf(" --gen-weather-arctic [zoneName]\n"); diff --git a/tools/editor/cli_list_formats.cpp b/tools/editor/cli_list_formats.cpp index f3f92b60..a928d508 100644 --- a/tools/editor/cli_list_formats.cpp +++ b/tools/editor/cli_list_formats.cpp @@ -76,6 +76,7 @@ constexpr FormatRow kFormats[] = { {"WTSK", ".wtsk", "crafting", "SkillLineAbility.dbc + recipes", "Trade skill recipes (per-profession crafts)"}, {"WCEQ", ".wceq", "creatures", "creature_equip_template", "Creature equipment loadout (visible weapons)"}, {"WSET", ".wset", "items", "ItemSet.dbc + ItemSetSpell.dbc", "Item set + tier-bonus catalog"}, + {"WGTP", ".wgtp", "ui", "GameTips.dbc + tutorial hints", "Game tips / tutorial / loading-screen catalog"}, // Additional pipeline catalogs without the alternating // gen/info/validate CLI surface (loaded by the engine