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