From ddee7e14acda36ab787c6e18d15e45c062d7c94f Mon Sep 17 00:00:00 2001 From: sindoring Date: Sat, 13 Jun 2026 15:09:04 +0400 Subject: [PATCH] =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20instance=20id=20=D0=B4=D0=BB=D1=8F=20encounter?= =?UTF-8?q?=20journal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.json | 3 +- .../src/playerGuide/PlayerGuideMgr.cpp | 133 +++++++++++++----- .../src/playerGuide/PlayerGuideMgr.h | 51 +++++-- .../src/playerGuide/PlayerGuideScripts.cpp | 15 +- 4 files changed, 151 insertions(+), 51 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index b83e624..4e264b7 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -20,7 +20,8 @@ "Bash(python3 /home/sindo/moonwell-core/modules/mod-encounter-journal/tools/import_donor_journal.py -i /home/sindo/moonwell-core/Generated_EncounterJournal.lua -o /tmp/donor_import.sql)", "Bash(python3 /home/sindo/moonwell-core/modules/mod-encounter-journal/tools/import_donor_journal.py -i /home/sindo/moonwell-core/Generated_EncounterJournal.lua -o /tmp/donor_import_filtered.sql --known-maps /tmp/known_maps.txt --known-creatures /tmp/known_creatures.txt --known-items /tmp/known_items.txt)", "Bash(sudo grep -c \"Encounter Journal loaded\\\\|mod-encounter-journal\" /var/lib/docker/containers/8a3d7a23f170597b5a8eae754ef3259ac6f2af352d78958707ef5cf3010133e3/8a3d7a23f170597b5a8eae754ef3259ac6f2af352d78958707ef5cf3010133e3-json.log)", - "Bash(xargs -I {} stat -c '%y %n' {})" + "Bash(xargs -I {} stat -c '%y %n' {})", + "Bash(grep -n \"INSERT INTO \\\\`custom_ej_instance\\\\`\" /home/sindo/moonwell-core/modules/mod-encounter-journal/data/sql/db-world/base/02_encounter_journal_sample.sql)" ] } } diff --git a/modules/mod-individual-progression/src/playerGuide/PlayerGuideMgr.cpp b/modules/mod-individual-progression/src/playerGuide/PlayerGuideMgr.cpp index fe4de30..a61c7d1 100644 --- a/modules/mod-individual-progression/src/playerGuide/PlayerGuideMgr.cpp +++ b/modules/mod-individual-progression/src/playerGuide/PlayerGuideMgr.cpp @@ -9,8 +9,11 @@ #include "../IndividualProgression.h" #include "Config.h" +#include "DatabaseEnv.h" +#include "Field.h" #include "Log.h" #include "Player.h" +#include "QueryResult.h" #include "QuestDef.h" #include @@ -202,6 +205,45 @@ namespace MoonWell::PlayerGuide // characters. We leave room for the framing header. if (_chunkSize < 64) _chunkSize = 64; if (_chunkSize > 240) _chunkSize = 240; + + ReloadEncounterJournalCache(); + } + + void Mgr::ReloadEncounterJournalCache() + { + _ejByMapId.clear(); + + // mod-encounter-journal may be disabled or its table absent. + // The query then yields no result and we keep an empty cache; + // recommendations that would have used EJ ids are skipped. + QueryResult result = WorldDatabase.Query( + "SELECT `id`, `map_id`, `name` " + "FROM `custom_ej_instance`"); + if (!result) + { + LOG_INFO("server.loading", + ">> PlayerGuide: no custom_ej_instance rows found. " + "Recommendations will be empty."); + return; + } + + do + { + Field* f = result->Fetch(); + EjInstanceRef ref; + ref.ejId = f[0].Get(); + uint32 mapId = f[1].Get(); + ref.name = f[2].Get(); + + // First entry wins. If the EJ has duplicates for the same + // map (e.g. heroic variants) the lowest id is taken via the + // INSERT order; we don't reorder. + _ejByMapId.emplace(mapId, std::move(ref)); + } while (result->NextRow()); + + LOG_INFO("server.loading", + ">> PlayerGuide: cached {} EJ instance entries by mapId.", + _ejByMapId.size()); } std::string Mgr::StageName(uint32 stage) const @@ -258,14 +300,19 @@ namespace MoonWell::PlayerGuide std::string const& title, uint32 order) const { + auto it = _ejByMapId.find(mapId); + if (it == _ejByMapId.end()) + return; // no EJ entry -> skip rather than emit a stub. + Objective o; - o.id = order; - o.type = "dungeon"; - o.title = title; - o.required = 1; - o.targetId = mapId; - o.instanceId = mapId; - o.order = order; + o.id = order; + o.type = "dungeon"; + o.title = it->second.name.empty() ? title : it->second.name; + o.required = 1; + o.targetId = it->second.ejId; // primary id for click + o.ejInstanceId = it->second.ejId; // JOURNALINSTANCE key + o.mapId = mapId; // WoW map id (auxiliary) + o.order = order; out.objectives.push_back(std::move(o)); } @@ -273,14 +320,19 @@ namespace MoonWell::PlayerGuide std::string const& title, uint32 order) const { + auto it = _ejByMapId.find(mapId); + if (it == _ejByMapId.end()) + return; + Objective o; - o.id = order; - o.type = "raid"; - o.title = title; - o.required = 1; - o.targetId = mapId; - o.instanceId = mapId; - o.order = order; + o.id = order; + o.type = "raid"; + o.title = it->second.name.empty() ? title : it->second.name; + o.required = 1; + o.targetId = it->second.ejId; + o.ejInstanceId = it->second.ejId; + o.mapId = mapId; + o.order = order; out.objectives.push_back(std::move(o)); } @@ -298,18 +350,29 @@ namespace MoonWell::PlayerGuide continue; if (level + 2 < d.minLevel || level > d.maxLevel + 1) continue; - if (!seen.insert(d.mapId).second) + + // Resolve EJ id from custom_ej_instance. If the EJ does + // not know about this map, we cannot link to a card, so + // we skip the recommendation entirely. This keeps the + // contract that recommendations.instanceID is always a + // valid EJ id. + auto it = _ejByMapId.find(d.mapId); + if (it == _ejByMapId.end()) + continue; + if (!seen.insert(it->second.ejId).second) continue; Recommendation r; - r.id = recId++; - r.type = "dungeon"; - r.title = d.title; - r.instanceId = d.mapId; - r.priority = 100 - + r.id = recId++; + r.type = "dungeon"; + r.title = it->second.name.empty() ? d.title + : it->second.name; + r.ejInstanceId = it->second.ejId; // JOURNALINSTANCE key + r.mapId = d.mapId; // WoW map id (auxiliary) + r.priority = 100 - static_cast(std::abs(int32(level) - int32((d.minLevel + d.maxLevel) / 2))); - r.description = "Подходит для вашего текущего этапа."; + r.description = "Подходит для вашего текущего этапа."; out.recommendations.push_back(std::move(r)); if (out.recommendations.size() >= 6) @@ -496,15 +559,16 @@ namespace MoonWell::PlayerGuide WriteUInt(s, "targetID", o.targetId, first); WriteUInt(s, "order", o.order, first); - WriteOptUInt(s, "questID", o.questId, first); - WriteOptUInt(s, "questChainID", o.questChainId, first); - WriteOptUInt(s, "instanceID", o.instanceId, first); - WriteOptUInt(s, "encounterID", o.encounterId, first); - WriteOptUInt(s, "achievementID", o.achievementId, first); - WriteOptUInt(s, "factionID", o.factionId, first); - WriteOptUInt(s, "professionID", o.professionId, first); - WriteOptUInt(s, "itemID", o.itemId, first); - WriteOptUInt(s, "currencyID", o.currencyId, first); + WriteOptUInt(s, "questID", o.questId, first); + WriteOptUInt(s, "questChainID", o.questChainId, first); + WriteOptUInt(s, "ej_instanceID", o.ejInstanceId, first); + WriteOptUInt(s, "mapID", o.mapId, first); + WriteOptUInt(s, "encounterID", o.encounterId, first); + WriteOptUInt(s, "achievementID", o.achievementId, first); + WriteOptUInt(s, "factionID", o.factionId, first); + WriteOptUInt(s, "professionID", o.professionId, first); + WriteOptUInt(s, "itemID", o.itemId, first); + WriteOptUInt(s, "currencyID", o.currencyId, first); s << '}'; return s.str(); } @@ -551,10 +615,11 @@ namespace MoonWell::PlayerGuide WriteString(s, "title", r.title, f); if (!r.description.empty()) WriteString(s, "description", r.description, f); - WriteOptUInt(s, "instanceID", r.instanceId, f); - WriteOptUInt(s, "encounterID", r.encounterId, f); - WriteOptUInt(s, "questID", r.questId, f); - WriteOptUInt(s, "itemID", r.itemId, f); + WriteOptUInt(s, "ej_instanceID", r.ejInstanceId, f); + WriteOptUInt(s, "mapID", r.mapId, f); + WriteOptUInt(s, "encounterID", r.encounterId, f); + WriteOptUInt(s, "questID", r.questId, f); + WriteOptUInt(s, "itemID", r.itemId, f); WriteUInt(s, "priority", r.priority, f); s << '}'; firstRec = false; diff --git a/modules/mod-individual-progression/src/playerGuide/PlayerGuideMgr.h b/modules/mod-individual-progression/src/playerGuide/PlayerGuideMgr.h index 41a9ab1..0ce3732 100644 --- a/modules/mod-individual-progression/src/playerGuide/PlayerGuideMgr.h +++ b/modules/mod-individual-progression/src/playerGuide/PlayerGuideMgr.h @@ -11,6 +11,7 @@ #include #include +#include #include class Player; @@ -36,28 +37,34 @@ namespace MoonWell::PlayerGuide // Optional type-specific helpers. Zero means "not set" and // serialiser omits it. - uint32 questId = 0; - uint32 questChainId = 0; - uint32 instanceId = 0; - uint32 encounterId = 0; - uint32 achievementId = 0; - uint32 factionId = 0; - uint32 professionId = 0; - uint32 itemId = 0; - uint32 currencyId = 0; + uint32 questId = 0; + uint32 questChainId = 0; + // ej_instanceID: key into client's JOURNALINSTANCE table. + // Consumed by EncounterJournal_DisplayInstance / EJ_SelectInstance. + uint32 ejInstanceId = 0; + // WoW map id (for telemetry / teleport / fallback). NOT used by + // the client to open EJ. + uint32 mapId = 0; + uint32 encounterId = 0; + uint32 achievementId = 0; + uint32 factionId = 0; + uint32 professionId = 0; + uint32 itemId = 0; + uint32 currencyId = 0; }; struct Recommendation { - uint32 id = 0; + uint32 id = 0; std::string type; std::string title; std::string description; - uint32 instanceId = 0; - uint32 encounterId = 0; - uint32 questId = 0; - uint32 itemId = 0; - uint32 priority = 0; + uint32 ejInstanceId = 0; // JOURNALINSTANCE key + uint32 mapId = 0; // WoW map id (auxiliary) + uint32 encounterId = 0; + uint32 questId = 0; + uint32 itemId = 0; + uint32 priority = 0; }; struct Reward @@ -105,6 +112,11 @@ namespace MoonWell::PlayerGuide bool LogPayloads() const { return _logPayloads; } uint32 ChunkSize() const { return _chunkSize; } + // Reloads (map_id -> ej_instance.id/name) cache from + // custom_ej_instance. Called from LoadConfig() and from the + // OnLoadCustomDatabaseTable world hook. + void ReloadEncounterJournalCache(); + private: Mgr() = default; @@ -127,6 +139,15 @@ namespace MoonWell::PlayerGuide void AppendRecommendations(Payload& out, Player* player) const; + struct EjInstanceRef + { + uint32 ejId = 0; // custom_ej_instance.id + std::string name; // custom_ej_instance.name + }; + + // mapId -> EJ instance ref. Filled at config load. + std::unordered_map _ejByMapId; + bool _enabled = true; bool _logPayloads = false; uint32 _chunkSize = 230; // per-frame body capacity (addon channel) diff --git a/modules/mod-individual-progression/src/playerGuide/PlayerGuideScripts.cpp b/modules/mod-individual-progression/src/playerGuide/PlayerGuideScripts.cpp index b3ff98d..78e9ac8 100644 --- a/modules/mod-individual-progression/src/playerGuide/PlayerGuideScripts.cpp +++ b/modules/mod-individual-progression/src/playerGuide/PlayerGuideScripts.cpp @@ -164,7 +164,10 @@ namespace MoonWell::PlayerGuide public: PlayerGuide_WorldScript() : WorldScript("PlayerGuide_WorldScript", - { WORLDHOOK_ON_AFTER_CONFIG_LOAD }) { } + { + WORLDHOOK_ON_AFTER_CONFIG_LOAD, + WORLDHOOK_ON_LOAD_CUSTOM_DATABASE_TABLE + }) { } void OnAfterConfigLoad(bool /*reload*/) override { @@ -173,6 +176,16 @@ namespace MoonWell::PlayerGuide ">> PlayerGuide module: {}.", sPlayerGuideMgr->IsEnabled() ? "enabled" : "disabled"); } + + void OnLoadCustomDatabaseTable() override + { + // Re-read custom_ej_instance after the EJ loader has had + // a chance to populate it. Order between modules on this + // hook is undefined, but we are reading the table itself + // (not EJ's in-memory cache) so we are fine as long as + // the SQL has been applied. + sPlayerGuideMgr->ReloadEncounterJournalCache(); + } }; }