diff --git a/CMakeLists.txt b/CMakeLists.txt index 3397fe5b..e368823e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -617,6 +617,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_chars.cpp src/pipeline/wowee_tokens.cpp src/pipeline/wowee_triggers.cpp + src/pipeline/wowee_titles.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1380,6 +1381,7 @@ add_executable(wowee_editor tools/editor/cli_chars_catalog.cpp tools/editor/cli_tokens_catalog.cpp tools/editor/cli_triggers_catalog.cpp + tools/editor/cli_titles_catalog.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp @@ -1475,6 +1477,7 @@ add_executable(wowee_editor src/pipeline/wowee_chars.cpp src/pipeline/wowee_tokens.cpp src/pipeline/wowee_triggers.cpp + src/pipeline/wowee_titles.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_titles.hpp b/include/pipeline/wowee_titles.hpp new file mode 100644 index 00000000..70ebb7ec --- /dev/null +++ b/include/pipeline/wowee_titles.hpp @@ -0,0 +1,105 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Title catalog (.wtit) — novel replacement for +// Blizzard's CharTitles.dbc + the AzerothCore-style +// character_title SQL table. The 30th open format added to +// the editor. +// +// Defines the player-display titles awarded for completing +// achievements ("the Versatile"), reaching PvP ranks +// ("Sergeant Major"), participating in raids ("Champion of +// the Naaru"), levelling a class ("Master Locksmith"), or +// participating in seasonal events ("Brewmaster", "the +// Hallowed"). +// +// Cross-references with previously-added formats: +// WACH.entry.titleReward (string) ≈ WTIT.entry.name +// (string match — the +// runtime resolves +// achievement-granted +// titles by looking up +// the matching WTIT +// entry by name) +// +// Binary layout (little-endian): +// magic[4] = "WTIT" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// titleId (uint32) +// nameLen + name -- canonical / English form +// maleLen + nameMale -- empty = use canonical +// femaleLen + nameFemale -- empty = use canonical +// iconLen + iconPath +// prefix (uint8) -- 0=suffix, 1=prefix +// category (uint8) +// sortOrder (uint16) +struct WoweeTitle { + enum Category : uint8_t { + Achievement = 0, + Pvp = 1, + Raid = 2, + ClassTitle = 3, + Event = 4, + Profession = 5, + Lore = 6, + Custom = 7, + }; + + struct Entry { + uint32_t titleId = 0; + std::string name; // canonical (genderless) + std::string nameMale; // empty = use canonical + std::string nameFemale; + std::string iconPath; + uint8_t prefix = 0; // 0 = "Name the Versatile" + uint8_t category = Achievement; + uint16_t sortOrder = 0; // display order in UI + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t titleId) const; + // String match against the canonical name — used to + // resolve WACH.titleReward references. + const Entry* findByName(const std::string& name) const; + + static const char* categoryName(uint8_t c); +}; + +class WoweeTitleLoader { +public: + static bool save(const WoweeTitle& cat, + const std::string& basePath); + static WoweeTitle load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-titles* variants. + // + // makeStarter — 4 titles covering Achievement / Pvp / + // Raid / Event categories. + // makePvp — 14-rank Honor System ladder + // (Private/Corporal/...Knight-Lieutenant/ + // ...Field Marshal) with both Alliance + // and Horde titles where they differ. + // makeAchievement — titles granted by the WACH meta + // preset including "the Versatile" + // from achievement 250. + static WoweeTitle makeStarter(const std::string& catalogName); + static WoweeTitle makePvp(const std::string& catalogName); + static WoweeTitle makeAchievement(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_titles.cpp b/src/pipeline/wowee_titles.cpp new file mode 100644 index 00000000..d166f35c --- /dev/null +++ b/src/pipeline/wowee_titles.cpp @@ -0,0 +1,224 @@ +#include "pipeline/wowee_titles.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'T', 'I', 'T'}; +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) != ".wtit") { + base += ".wtit"; + } + return base; +} + +} // namespace + +const WoweeTitle::Entry* WoweeTitle::findById(uint32_t titleId) const { + for (const auto& e : entries) if (e.titleId == titleId) return &e; + return nullptr; +} + +const WoweeTitle::Entry* WoweeTitle::findByName(const std::string& n) const { + for (const auto& e : entries) if (e.name == n) return &e; + return nullptr; +} + +const char* WoweeTitle::categoryName(uint8_t c) { + switch (c) { + case Achievement: return "achievement"; + case Pvp: return "pvp"; + case Raid: return "raid"; + case ClassTitle: return "class"; + case Event: return "event"; + case Profession: return "profession"; + case Lore: return "lore"; + case Custom: return "custom"; + default: return "unknown"; + } +} + +bool WoweeTitleLoader::save(const WoweeTitle& 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.titleId); + writeStr(os, e.name); + writeStr(os, e.nameMale); + writeStr(os, e.nameFemale); + writeStr(os, e.iconPath); + writePOD(os, e.prefix); + writePOD(os, e.category); + writePOD(os, e.sortOrder); + } + return os.good(); +} + +WoweeTitle WoweeTitleLoader::load(const std::string& basePath) { + WoweeTitle 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.titleId)) { out.entries.clear(); return out; } + if (!readStr(is, e.name) || !readStr(is, e.nameMale) || + !readStr(is, e.nameFemale) || !readStr(is, e.iconPath)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.prefix) || + !readPOD(is, e.category) || + !readPOD(is, e.sortOrder)) { + out.entries.clear(); return out; + } + } + return out; +} + +bool WoweeTitleLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeTitle WoweeTitleLoader::makeStarter(const std::string& catalogName) { + WoweeTitle c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint8_t cat, + uint16_t sort, uint8_t prefix = 0) { + WoweeTitle::Entry e; + e.titleId = id; e.name = name; e.category = cat; + e.sortOrder = sort; e.prefix = prefix; + c.entries.push_back(e); + }; + add(1, "the Versatile", WoweeTitle::Achievement, 100); + add(2, "Sergeant", WoweeTitle::Pvp, 200, 1); + add(3, "Champion", WoweeTitle::Raid, 300, 1); + add(4, "the Hallowed", WoweeTitle::Event, 400); + return c; +} + +WoweeTitle WoweeTitleLoader::makePvp(const std::string& catalogName) { + WoweeTitle c; + c.name = catalogName; + // Classic Honor System rank ladder (14 tiers per faction). + // Most ranks share names across both factions; only the + // ranked-officer titles (Knight / Stone Guard etc.) split. + auto add = [&](uint32_t id, const char* name, uint16_t sort) { + WoweeTitle::Entry e; + e.titleId = id; e.name = name; + e.category = WoweeTitle::Pvp; + e.sortOrder = sort; e.prefix = 1; + c.entries.push_back(e); + }; + // Alliance ladder. + add(101, "Private", 10); + add(102, "Corporal", 20); + add(103, "Sergeant", 30); + add(104, "Master Sergeant", 40); + add(105, "Sergeant Major", 50); + add(106, "Knight", 60); + add(107, "Knight-Lieutenant", 70); + add(108, "Knight-Captain", 80); + add(109, "Knight-Champion", 90); + add(110, "Lieutenant Commander", 100); + add(111, "Commander", 110); + add(112, "Marshal", 120); + add(113, "Field Marshal", 130); + add(114, "Grand Marshal", 140); + // Horde ladder (parallel ranks 6-14). + add(201, "Scout", 15); + add(202, "Grunt", 25); + add(203, "Sergeant", 35); + add(204, "Senior Sergeant", 45); + add(205, "First Sergeant", 55); + add(206, "Stone Guard", 65); + add(207, "Blood Guard", 75); + add(208, "Legionnaire", 85); + add(209, "Centurion", 95); + add(210, "Champion", 105); + add(211, "Lieutenant General", 115); + add(212, "General", 125); + add(213, "Warlord", 135); + add(214, "High Warlord", 145); + return c; +} + +WoweeTitle WoweeTitleLoader::makeAchievement(const std::string& catalogName) { + WoweeTitle c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint16_t sort, + uint8_t prefix = 0, uint8_t cat = WoweeTitle::Achievement) { + WoweeTitle::Entry e; + e.titleId = id; e.name = name; e.category = cat; + e.sortOrder = sort; e.prefix = prefix; + c.entries.push_back(e); + }; + // titleId=1 + name="the Versatile" matches WACH.makeMeta + // achievement 250 (Jack of All Trades). + add(1, "the Versatile", 100); + add(2, "the Explorer", 200); + add(3, "Loremaster", 300, 1); + add(4, "the Insane", 400); + add(5, "the Patient", 500); + add(6, "Salty", 600); + // Profession capstone titles. + add(10, "Master Locksmith", 1000, 0, WoweeTitle::Profession); + add(11, "Master Chef", 1100, 0, WoweeTitle::Profession); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 9cad3017..cb5d9f22 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -86,6 +86,8 @@ const char* const kArgRequired[] = { "--gen-triggers", "--gen-triggers-dungeon", "--gen-triggers-flightpath", "--info-wtrg", "--validate-wtrg", "--export-wtrg-json", "--import-wtrg-json", + "--gen-titles", "--gen-titles-pvp", "--gen-titles-achievement", + "--info-wtit", "--validate-wtit", "--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 f683e7d2..412bc616 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -57,6 +57,7 @@ #include "cli_chars_catalog.hpp" #include "cli_tokens_catalog.hpp" #include "cli_triggers_catalog.hpp" +#include "cli_titles_catalog.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -155,6 +156,7 @@ constexpr DispatchFn kDispatchTable[] = { handleCharsCatalog, handleTokensCatalog, handleTriggersCatalog, + handleTitlesCatalog, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index c2b8e032..c5ea0238 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1121,6 +1121,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wtrg to a human-editable JSON sidecar (defaults to .wtrg.json)\n"); std::printf(" --import-wtrg-json [out-base]\n"); std::printf(" Import a .wtrg.json sidecar back into binary .wtrg (accepts shape/kind int OR name forms)\n"); + std::printf(" --gen-titles [name]\n"); + std::printf(" Emit .wtit starter: 4 titles covering Achievement / Pvp / Raid / Event categories\n"); + std::printf(" --gen-titles-pvp [name]\n"); + std::printf(" Emit .wtit Honor System ladder: 14 Alliance + 14 Horde rank titles (Private..Grand Marshal / Scout..High Warlord)\n"); + std::printf(" --gen-titles-achievement [name]\n"); + std::printf(" Emit .wtit achievement titles incl. 'the Versatile' (matches WACH meta-achievement 250 titleReward)\n"); + std::printf(" --info-wtit [--json]\n"); + std::printf(" Print WTIT entries (id / sort / prefix vs suffix / category / canonical name)\n"); + std::printf(" --validate-wtit [--json]\n"); + std::printf(" Static checks: titleId>0+unique, name not empty, category in 0..7, gender variants paired\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_titles_catalog.cpp b/tools/editor/cli_titles_catalog.cpp new file mode 100644 index 00000000..115ca949 --- /dev/null +++ b/tools/editor/cli_titles_catalog.cpp @@ -0,0 +1,223 @@ +#include "cli_titles_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_titles.hpp" +#include + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWtitExt(std::string base) { + stripExt(base, ".wtit"); + return base; +} + +bool saveOrError(const wowee::pipeline::WoweeTitle& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeTitleLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wtit\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeTitle& c, + const std::string& base) { + std::printf("Wrote %s.wtit\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" titles : %zu\n", c.entries.size()); +} + +int handleGenStarter(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "StarterTitles"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWtitExt(base); + auto c = wowee::pipeline::WoweeTitleLoader::makeStarter(name); + if (!saveOrError(c, base, "gen-titles")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenPvp(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "PvpTitles"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWtitExt(base); + auto c = wowee::pipeline::WoweeTitleLoader::makePvp(name); + if (!saveOrError(c, base, "gen-titles-pvp")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenAchievement(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "AchievementTitles"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWtitExt(base); + auto c = wowee::pipeline::WoweeTitleLoader::makeAchievement(name); + if (!saveOrError(c, base, "gen-titles-achievement")) 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 = stripWtitExt(base); + if (!wowee::pipeline::WoweeTitleLoader::exists(base)) { + std::fprintf(stderr, "WTIT not found: %s.wtit\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeTitleLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wtit"] = base + ".wtit"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + arr.push_back({ + {"titleId", e.titleId}, + {"name", e.name}, + {"nameMale", e.nameMale}, + {"nameFemale", e.nameFemale}, + {"iconPath", e.iconPath}, + {"prefix", e.prefix}, + {"prefixName", e.prefix ? "prefix" : "suffix"}, + {"category", e.category}, + {"categoryName", wowee::pipeline::WoweeTitle::categoryName(e.category)}, + {"sortOrder", e.sortOrder}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WTIT: %s.wtit\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" titles : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id sort pos category name\n"); + for (const auto& e : c.entries) { + std::printf(" %4u %4u %-6s %-12s %s\n", + e.titleId, e.sortOrder, + e.prefix ? "prefix" : "suffix", + wowee::pipeline::WoweeTitle::categoryName(e.category), + 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 = stripWtitExt(base); + if (!wowee::pipeline::WoweeTitleLoader::exists(base)) { + std::fprintf(stderr, + "validate-wtit: WTIT not found: %s.wtit\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeTitleLoader::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.titleId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.titleId == 0) errors.push_back(ctx + ": titleId is 0"); + if (e.name.empty()) errors.push_back(ctx + ": name is empty"); + if (e.category > wowee::pipeline::WoweeTitle::Custom) { + errors.push_back(ctx + ": category " + + std::to_string(e.category) + " not in 0..7"); + } + // If gender variants are set, both should be set (otherwise + // the runtime fall-back to canonical leaves one gender + // displaying the wrong form). + if (!e.nameMale.empty() && e.nameFemale.empty()) { + warnings.push_back(ctx + + ": nameMale set but nameFemale empty (mixed-gender display)"); + } + if (!e.nameFemale.empty() && e.nameMale.empty()) { + warnings.push_back(ctx + + ": nameFemale set but nameMale empty (mixed-gender display)"); + } + for (uint32_t prev : idsSeen) { + if (prev == e.titleId) { + errors.push_back(ctx + ": duplicate titleId"); + break; + } + } + idsSeen.push_back(e.titleId); + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wtit"] = base + ".wtit"; + 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-wtit: %s.wtit\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu titles, all titleIds unique\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 handleTitlesCatalog(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--gen-titles") == 0 && i + 1 < argc) { + outRc = handleGenStarter(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-titles-pvp") == 0 && i + 1 < argc) { + outRc = handleGenPvp(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-titles-achievement") == 0 && i + 1 < argc) { + outRc = handleGenAchievement(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wtit") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wtit") == 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_titles_catalog.hpp b/tools/editor/cli_titles_catalog.hpp new file mode 100644 index 00000000..03e068f9 --- /dev/null +++ b/tools/editor/cli_titles_catalog.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleTitlesCatalog(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee