From 81b1897a2492499f6be951a0cacb09068848acb0 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 15:44:26 -0700 Subject: [PATCH] feat(pipeline): add WLCK (Wowee Lock Template) format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Novel open replacement for Blizzard's Lock.dbc. The 18th open format added to the editor. Closes the cross-reference gap from WGOT.entry.lockId — until now that field pointed to a format that didn't exist yet. A lock is a multi-channel security check. Each lock has up to 5 independent channels; a player can open the lock by satisfying ANY ONE channel: • Item — requires a specific key item (WIT cross-ref) • Lockpick — requires the lockpicking skill at minimum rank (rogue / engineering profession) • Spell — requires casting a specific spell • Damage — can be forced open with attack damage Cross-references with previously-added formats: WGOT.entry.lockId -> WLCK.entry.lockId WLCK.channel.targetId (Item) -> WIT.entry.itemId WLCK.channel.targetId (Lockpick) -> future WSKL skillId WLCK.channel.targetId (Spell) -> future WSPL spellId The starter and dungeon presets' lockIds (1 and 2) deliberately match WGOT.makeDungeon's iron-door lockId=1 and bandit-strongbox lockId=2, so the demo content stack already wires together: WSPN spawn -> WGOT object template -> WLCK lock template -> WIT key items. Format: • magic "WLCK", version 1, little-endian • per lock: lockId / name / flags / 5 fixed channel slots • per channel: kind / skillRequired / targetId • all 5 slots written even when unused (kind=None + zeroed fields), keeping the per-entry size constant for fast random access Enums: • ChannelKind: None / Item / Lockpick / Spell / Damage • Flags: DestructOnOpen / RespawnOnKey / TrapOnFail API: WoweeLockLoader::save / load / exists / findById; presets makeStarter (Iron Door + Wooden Chest), makeDungeon (matches WGOT cross-references; light/heavy lockpicks + boss-key-only seal), makeProfessions (4-tier rogue lockpick progression at ranks 1/100/175/250). CLI added (5 flags, 521 documented total now): --gen-locks / --gen-locks-dungeon / --gen-locks-professions --info-wlck / --validate-wlck Validator catches: lockId=0 + duplicates, all-None channels (lock can never open), Item/Spell/Lockpick channels with targetId=0 (no resource referenced), unknown channel kind, skillRequired set on non-Lockpick channel (silently ignored at runtime — flag as warning). --- CMakeLists.txt | 3 + include/pipeline/wowee_locks.hpp | 116 +++++++++++++ src/pipeline/wowee_locks.cpp | 211 +++++++++++++++++++++++ tools/editor/cli_arg_required.cpp | 2 + tools/editor/cli_dispatch.cpp | 2 + tools/editor/cli_help.cpp | 10 ++ tools/editor/cli_locks_catalog.cpp | 266 +++++++++++++++++++++++++++++ tools/editor/cli_locks_catalog.hpp | 11 ++ 8 files changed, 621 insertions(+) create mode 100644 include/pipeline/wowee_locks.hpp create mode 100644 src/pipeline/wowee_locks.cpp create mode 100644 tools/editor/cli_locks_catalog.cpp create mode 100644 tools/editor/cli_locks_catalog.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 0aa969d9..522261a6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -605,6 +605,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_quests.cpp src/pipeline/wowee_objects.cpp src/pipeline/wowee_factions.cpp + src/pipeline/wowee_locks.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1356,6 +1357,7 @@ add_executable(wowee_editor tools/editor/cli_quests_catalog.cpp tools/editor/cli_objects_catalog.cpp tools/editor/cli_factions_catalog.cpp + tools/editor/cli_locks_catalog.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp @@ -1439,6 +1441,7 @@ add_executable(wowee_editor src/pipeline/wowee_quests.cpp src/pipeline/wowee_objects.cpp src/pipeline/wowee_factions.cpp + src/pipeline/wowee_locks.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_locks.hpp b/include/pipeline/wowee_locks.hpp new file mode 100644 index 00000000..2f3c6d4f --- /dev/null +++ b/include/pipeline/wowee_locks.hpp @@ -0,0 +1,116 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Lock Template (.wlck) — novel replacement for +// Blizzard's Lock.dbc. The 18th open format added to the +// editor. Closes the cross-reference gap from WGOT.entry.lockId +// (and the future WIT lockbox subset) — until now those +// fields pointed to a format that didn't exist yet. +// +// A lock is a multi-channel security check. Each lock has up +// to 5 independent channels; a player can open the lock by +// satisfying ANY ONE channel. Channels can be: +// • Item — requires a specific key item (WIT cross-ref) +// • Lockpick — requires the lockpicking skill at a minimum +// rank (rogue / engineering profession) +// • Spell — requires casting a specific spell +// • Damage — can be forced open with attack damage +// +// Cross-references with previously-added formats: +// WGOT.entry.lockId → WLCK.entry.lockId +// WLCK.channel.targetId (kind=Item) → WIT.entry.itemId +// WLCK.channel.targetId (kind=Skill) → future WSKL skillId +// WLCK.channel.targetId (kind=Spell) → future WSPL spellId +// +// Binary layout (little-endian): +// magic[4] = "WLCK" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// lockId (uint32) +// nameLen + name +// flags (uint32) +// -- 5 channel slots, all written even when unused; an +// unused slot has kind=None and zeroed fields. +// channels[5] × { +// kind (uint8) + pad[1] +// skillRequired (uint16) +// targetId (uint32) +// } +struct WoweeLock { + static constexpr int kChannelSlots = 5; + + enum ChannelKind : uint8_t { + ChannelNone = 0, + ChannelItem = 1, // requires a key item (targetId = WIT itemId) + ChannelLockpick = 2, // requires lockpicking skill (targetId = skill ID) + ChannelSpell = 3, // requires casting a spell (targetId = spell ID) + ChannelDamage = 4, // can be forced open with damage (targetId unused) + }; + + enum Flags : uint32_t { + DestructOnOpen = 0x01, // lock destroyed after one successful open + RespawnOnKey = 0x02, // re-locks itself after key use (timed) + TrapOnFail = 0x04, // failure triggers a trap (script-handled) + }; + + struct Channel { + uint8_t kind = ChannelNone; + uint16_t skillRequired = 0; // only meaningful for ChannelLockpick + uint32_t targetId = 0; // item / skill / spell id + }; + + struct Entry { + uint32_t lockId = 0; + std::string name; + uint32_t flags = 0; + Channel channels[kChannelSlots] = {}; + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + // Lookup by lockId — nullptr if not present. + const Entry* findById(uint32_t lockId) const; + + static const char* channelKindName(uint8_t k); +}; + +class WoweeLockLoader { +public: + static bool save(const WoweeLock& cat, + const std::string& basePath); + static WoweeLock load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-locks* variants. + // + // makeStarter — 2 locks: a basic wooden chest lock + // (lockId=1, requires no skill, can be + // forced open) plus a small key-required + // lockbox (lockId=2). lockId=1 matches + // WGOT.makeDungeon's iron-door lockId. + // makeDungeon — 3 dungeon-tier locks: light lockpick + // (lockId=2 matching WGOT bandit chest), + // steel chest (heavy lockpick OR specific + // key), and a quest-key-only seal. + // makeProfessions — 4 profession-keyed locks: lockpick at + // ranks 1/100/175/250 covering the + // classic-tier rogue / engineering + // progression curve. + static WoweeLock makeStarter(const std::string& catalogName); + static WoweeLock makeDungeon(const std::string& catalogName); + static WoweeLock makeProfessions(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_locks.cpp b/src/pipeline/wowee_locks.cpp new file mode 100644 index 00000000..cb0ac98e --- /dev/null +++ b/src/pipeline/wowee_locks.cpp @@ -0,0 +1,211 @@ +#include "pipeline/wowee_locks.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'L', 'C', 'K'}; +constexpr uint32_t kVersion = 1; +// SkillLine canonical ID for lockpicking (matches AzerothCore). +constexpr uint32_t kLockpickingSkill = 633; + +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) != ".wlck") { + base += ".wlck"; + } + return base; +} + +} // namespace + +const WoweeLock::Entry* WoweeLock::findById(uint32_t lockId) const { + for (const auto& e : entries) { + if (e.lockId == lockId) return &e; + } + return nullptr; +} + +const char* WoweeLock::channelKindName(uint8_t k) { + switch (k) { + case ChannelNone: return "-"; + case ChannelItem: return "item"; + case ChannelLockpick: return "lockpick"; + case ChannelSpell: return "spell"; + case ChannelDamage: return "damage"; + default: return "?"; + } +} + +bool WoweeLockLoader::save(const WoweeLock& 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.lockId); + writeStr(os, e.name); + writePOD(os, e.flags); + for (int k = 0; k < WoweeLock::kChannelSlots; ++k) { + const auto& ch = e.channels[k]; + writePOD(os, ch.kind); + uint8_t pad = 0; + writePOD(os, pad); + writePOD(os, ch.skillRequired); + writePOD(os, ch.targetId); + } + } + return os.good(); +} + +WoweeLock WoweeLockLoader::load(const std::string& basePath) { + WoweeLock 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.lockId)) { out.entries.clear(); return out; } + if (!readStr(is, e.name)) { out.entries.clear(); return out; } + if (!readPOD(is, e.flags)) { out.entries.clear(); return out; } + for (int k = 0; k < WoweeLock::kChannelSlots; ++k) { + auto& ch = e.channels[k]; + if (!readPOD(is, ch.kind)) { + out.entries.clear(); return out; + } + uint8_t pad = 0; + if (!readPOD(is, pad)) { + out.entries.clear(); return out; + } + if (!readPOD(is, ch.skillRequired) || + !readPOD(is, ch.targetId)) { + out.entries.clear(); return out; + } + } + } + return out; +} + +bool WoweeLockLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeLock WoweeLockLoader::makeStarter(const std::string& catalogName) { + WoweeLock c; + c.name = catalogName; + { + // lockId 1 matches the WGOT.makeDungeon Iron Door lock. + WoweeLock::Entry e; + e.lockId = 1; e.name = "Iron Door Lock"; + e.channels[0] = {WoweeLock::ChannelItem, 0, 5001}; // requires key item 5001 + e.channels[1] = {WoweeLock::ChannelDamage, 0, 0}; // OR force open + c.entries.push_back(e); + } + { + WoweeLock::Entry e; + e.lockId = 100; e.name = "Wooden Chest Lock"; + e.channels[0] = {WoweeLock::ChannelDamage, 0, 0}; // forceable + c.entries.push_back(e); + } + return c; +} + +WoweeLock WoweeLockLoader::makeDungeon(const std::string& catalogName) { + WoweeLock c; + c.name = catalogName; + { + // lockId 2 matches WGOT.makeDungeon's bandit strongbox. + WoweeLock::Entry e; + e.lockId = 2; e.name = "Light Bandit Strongbox"; + e.channels[0] = {WoweeLock::ChannelLockpick, + 1, kLockpickingSkill}; // any skill rank + c.entries.push_back(e); + } + { + WoweeLock::Entry e; + e.lockId = 200; e.name = "Steel Chest Lock"; + // Either heavy lockpick OR a specific key. + e.channels[0] = {WoweeLock::ChannelLockpick, + 175, kLockpickingSkill}; + e.channels[1] = {WoweeLock::ChannelItem, 0, 5101}; + c.entries.push_back(e); + } + { + WoweeLock::Entry e; + e.lockId = 300; e.name = "Boss Vault Seal"; + e.flags = WoweeLock::DestructOnOpen; + // Quest key only — no lockpick option (story-gated). + e.channels[0] = {WoweeLock::ChannelItem, 0, 5200}; + c.entries.push_back(e); + } + return c; +} + +WoweeLock WoweeLockLoader::makeProfessions(const std::string& catalogName) { + WoweeLock c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint16_t skillReq) { + WoweeLock::Entry e; + e.lockId = id; e.name = name; + e.channels[0] = {WoweeLock::ChannelLockpick, + skillReq, kLockpickingSkill}; + c.entries.push_back(e); + }; + add(401, "Battered Junkbox", 1); + add(402, "Worn Junkbox", 100); + add(403, "Sturdy Junkbox", 175); + add(404, "Heavy Junkbox", 250); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index d5e166e3..ec416f67 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -49,6 +49,8 @@ const char* const kArgRequired[] = { "--export-wgot-json", "--import-wgot-json", "--gen-factions", "--gen-factions-alliance", "--gen-factions-wildlife", "--info-wfac", "--validate-wfac", + "--gen-locks", "--gen-locks-dungeon", "--gen-locks-professions", + "--info-wlck", "--validate-wlck", "--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 23399526..7b1072d1 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -45,6 +45,7 @@ #include "cli_quests_catalog.hpp" #include "cli_objects_catalog.hpp" #include "cli_factions_catalog.hpp" +#include "cli_locks_catalog.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -131,6 +132,7 @@ constexpr DispatchFn kDispatchTable[] = { handleQuestsCatalog, handleObjectsCatalog, handleFactionsCatalog, + handleLocksCatalog, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index 9b9a54b9..041e8dbb 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -947,6 +947,16 @@ void printUsage(const char* argv0) { std::printf(" Print WFAC entries (id / parent / flags / enemy + friend counts / name)\n"); std::printf(" --validate-wfac [--json]\n"); std::printf(" Static checks: factionId>0+unique, name not empty, threshold ordering, no self-enemy, no enemy/friend overlap\n"); + std::printf(" --gen-locks [name]\n"); + std::printf(" Emit .wlck starter: 2 locks (Iron Door key+force, Wooden Chest force-only) — lockId=1 matches WGOT\n"); + std::printf(" --gen-locks-dungeon [name]\n"); + std::printf(" Emit .wlck dungeon set: light/heavy lockpicks (lockId=2 matches WGOT bandit chest), boss-key seal\n"); + std::printf(" --gen-locks-professions [name]\n"); + std::printf(" Emit .wlck profession-keyed locks at lockpick rank 1/100/175/250 (junkbox tier progression)\n"); + std::printf(" --info-wlck [--json]\n"); + std::printf(" Print WLCK lock entries with per-channel detail (kind / target / required skill rank)\n"); + std::printf(" --validate-wlck [--json]\n"); + std::printf(" Static checks: lockId>0+unique, at least 1 active channel, item/spell/lockpick need targetId\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_locks_catalog.cpp b/tools/editor/cli_locks_catalog.cpp new file mode 100644 index 00000000..fde87292 --- /dev/null +++ b/tools/editor/cli_locks_catalog.cpp @@ -0,0 +1,266 @@ +#include "cli_locks_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_locks.hpp" +#include + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWlckExt(std::string base) { + stripExt(base, ".wlck"); + return base; +} + +void appendLockFlagsStr(std::string& s, uint32_t flags) { + if (flags & wowee::pipeline::WoweeLock::DestructOnOpen) s += "destruct "; + if (flags & wowee::pipeline::WoweeLock::RespawnOnKey) s += "respawn "; + if (flags & wowee::pipeline::WoweeLock::TrapOnFail) s += "trap "; + if (s.empty()) s = "-"; + else if (s.back() == ' ') s.pop_back(); +} + +bool saveOrError(const wowee::pipeline::WoweeLock& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeLockLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wlck\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeLock& c, + const std::string& base) { + std::printf("Wrote %s.wlck\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" locks : %zu\n", c.entries.size()); +} + +int handleGenStarter(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "StarterLocks"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWlckExt(base); + auto c = wowee::pipeline::WoweeLockLoader::makeStarter(name); + if (!saveOrError(c, base, "gen-locks")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenDungeon(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "DungeonLocks"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWlckExt(base); + auto c = wowee::pipeline::WoweeLockLoader::makeDungeon(name); + if (!saveOrError(c, base, "gen-locks-dungeon")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenProfessions(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "ProfessionLocks"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWlckExt(base); + auto c = wowee::pipeline::WoweeLockLoader::makeProfessions(name); + if (!saveOrError(c, base, "gen-locks-professions")) 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 = stripWlckExt(base); + if (!wowee::pipeline::WoweeLockLoader::exists(base)) { + std::fprintf(stderr, "WLCK not found: %s.wlck\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeLockLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wlck"] = base + ".wlck"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + std::string fs; + appendLockFlagsStr(fs, e.flags); + nlohmann::json je; + je["lockId"] = e.lockId; + je["name"] = e.name; + je["flags"] = e.flags; + je["flagsStr"] = fs; + nlohmann::json chans = nlohmann::json::array(); + for (int k = 0; k < wowee::pipeline::WoweeLock::kChannelSlots; ++k) { + const auto& ch = e.channels[k]; + chans.push_back({ + {"slot", k}, + {"kind", ch.kind}, + {"kindName", wowee::pipeline::WoweeLock::channelKindName(ch.kind)}, + {"skillRequired", ch.skillRequired}, + {"targetId", ch.targetId}, + }); + } + je["channels"] = chans; + arr.push_back(je); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WLCK: %s.wlck\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" locks : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + for (const auto& e : c.entries) { + std::string fs; + appendLockFlagsStr(fs, e.flags); + std::printf("\n lockId=%u flags=%s %s\n", + e.lockId, fs.c_str(), e.name.c_str()); + for (int k = 0; k < wowee::pipeline::WoweeLock::kChannelSlots; ++k) { + const auto& ch = e.channels[k]; + if (ch.kind == wowee::pipeline::WoweeLock::ChannelNone) continue; + std::printf(" slot %d : %-9s target=%u skillReq=%u\n", + k, + wowee::pipeline::WoweeLock::channelKindName(ch.kind), + ch.targetId, ch.skillRequired); + } + } + return 0; +} + +int handleValidate(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWlckExt(base); + if (!wowee::pipeline::WoweeLockLoader::exists(base)) { + std::fprintf(stderr, + "validate-wlck: WLCK not found: %s.wlck\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeLockLoader::load(base); + std::vector errors; + std::vector warnings; + if (c.entries.empty()) { + warnings.push_back("catalog has zero entries"); + } + std::vector idsSeen; + idsSeen.reserve(c.entries.size()); + 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.lockId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.lockId == 0) { + errors.push_back(ctx + ": lockId is 0"); + } + // At least one channel must be active or the lock can + // never be opened. + bool anyActive = false; + for (int ci = 0; ci < wowee::pipeline::WoweeLock::kChannelSlots; ++ci) { + const auto& ch = e.channels[ci]; + if (ch.kind != wowee::pipeline::WoweeLock::ChannelNone) { + anyActive = true; + } + if (ch.kind > wowee::pipeline::WoweeLock::ChannelDamage) { + errors.push_back(ctx + " slot " + std::to_string(ci) + + ": kind " + std::to_string(ch.kind) + + " not in known range 0..4"); + } + // Item / Spell / Lockpick channels need a non-zero + // targetId; Damage channels don't. + if ((ch.kind == wowee::pipeline::WoweeLock::ChannelItem || + ch.kind == wowee::pipeline::WoweeLock::ChannelSpell || + ch.kind == wowee::pipeline::WoweeLock::ChannelLockpick) && + ch.targetId == 0) { + errors.push_back(ctx + " slot " + std::to_string(ci) + + ": kind requires non-zero targetId"); + } + // skillRequired only meaningful for Lockpick — flag + // odd usage on other kinds. + if (ch.kind != wowee::pipeline::WoweeLock::ChannelLockpick && + ch.kind != wowee::pipeline::WoweeLock::ChannelNone && + ch.skillRequired != 0) { + warnings.push_back(ctx + " slot " + std::to_string(ci) + + ": skillRequired set on non-Lockpick channel (ignored at runtime)"); + } + } + if (!anyActive) { + errors.push_back(ctx + ": all 5 channels are None (lock can never open)"); + } + for (uint32_t prev : idsSeen) { + if (prev == e.lockId) { + errors.push_back(ctx + ": duplicate lockId"); + break; + } + } + idsSeen.push_back(e.lockId); + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wlck"] = base + ".wlck"; + 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-wlck: %s.wlck\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu locks, all lockIds 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 handleLocksCatalog(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--gen-locks") == 0 && i + 1 < argc) { + outRc = handleGenStarter(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-locks-dungeon") == 0 && i + 1 < argc) { + outRc = handleGenDungeon(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-locks-professions") == 0 && i + 1 < argc) { + outRc = handleGenProfessions(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wlck") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wlck") == 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_locks_catalog.hpp b/tools/editor/cli_locks_catalog.hpp new file mode 100644 index 00000000..9d3bb137 --- /dev/null +++ b/tools/editor/cli_locks_catalog.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleLocksCatalog(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee