From cbdf03c07ed8e24f8f93f513d1b52c9fa55094b0 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 22 Mar 2026 16:04:33 -0700 Subject: [PATCH] feat: add quest objective leaderboard API for WatchFrame quest tracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement GetNumQuestLeaderBoards and GetQuestLogLeaderBoard — the core functions WatchFrame.lua and QuestLogFrame.lua use to display objective progress like "Kobold Vermin slain: 3/8" or "Linen Cloth: 2/6". GetNumQuestLeaderBoards counts kill + item objectives from the parsed SMSG_QUEST_QUERY_RESPONSE data. GetQuestLogLeaderBoard returns the formatted progress text, type ("monster"/"item"/"object"), and completion status for each objective. Also adds ExpandQuestHeader/CollapseQuestHeader (no-ops for flat quest list) and GetQuestLogSpecialItemInfo stub. --- src/addons/lua_engine.cpp | 90 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/src/addons/lua_engine.cpp b/src/addons/lua_engine.cpp index 847bdc0f..944be318 100644 --- a/src/addons/lua_engine.cpp +++ b/src/addons/lua_engine.cpp @@ -2371,6 +2371,91 @@ static int lua_GetQuestLink(lua_State* L) { return 1; } +// GetNumQuestLeaderBoards(questLogIndex) → count of objectives +static int lua_GetNumQuestLeaderBoards(lua_State* L) { + auto* gh = getGameHandler(L); + int index = static_cast(luaL_checknumber(L, 1)); + if (!gh || index < 1) { lua_pushnumber(L, 0); return 1; } + const auto& ql = gh->getQuestLog(); + if (index > static_cast(ql.size())) { lua_pushnumber(L, 0); return 1; } + const auto& q = ql[index - 1]; + int count = 0; + for (const auto& ko : q.killObjectives) { + if (ko.npcOrGoId != 0 || ko.required > 0) ++count; + } + for (const auto& io : q.itemObjectives) { + if (io.itemId != 0 || io.required > 0) ++count; + } + lua_pushnumber(L, count); + return 1; +} + +// GetQuestLogLeaderBoard(objIndex, questLogIndex) → text, type, finished +// objIndex is 1-based within the quest's objectives +static int lua_GetQuestLogLeaderBoard(lua_State* L) { + auto* gh = getGameHandler(L); + int objIdx = static_cast(luaL_checknumber(L, 1)); + int questIdx = static_cast(luaL_optnumber(L, 2, + gh ? gh->getSelectedQuestLogIndex() : 0)); + if (!gh || questIdx < 1 || objIdx < 1) { lua_pushnil(L); return 1; } + const auto& ql = gh->getQuestLog(); + if (questIdx > static_cast(ql.size())) { lua_pushnil(L); return 1; } + const auto& q = ql[questIdx - 1]; + + // Build ordered list: kill objectives first, then item objectives + int cur = 0; + for (int i = 0; i < 4; ++i) { + if (q.killObjectives[i].npcOrGoId == 0 && q.killObjectives[i].required == 0) continue; + ++cur; + if (cur == objIdx) { + // Get current count from killCounts map (keyed by abs(npcOrGoId)) + uint32_t key = static_cast(std::abs(q.killObjectives[i].npcOrGoId)); + uint32_t current = 0; + auto it = q.killCounts.find(key); + if (it != q.killCounts.end()) current = it->second.first; + uint32_t required = q.killObjectives[i].required; + bool finished = (current >= required); + // Build display text like "Kobold Vermin slain: 3/8" + std::string text = (q.killObjectives[i].npcOrGoId < 0 ? "Object" : "Creature") + + std::string(" slain: ") + std::to_string(current) + "/" + std::to_string(required); + lua_pushstring(L, text.c_str()); + lua_pushstring(L, q.killObjectives[i].npcOrGoId < 0 ? "object" : "monster"); + lua_pushboolean(L, finished ? 1 : 0); + return 3; + } + } + for (int i = 0; i < 6; ++i) { + if (q.itemObjectives[i].itemId == 0 && q.itemObjectives[i].required == 0) continue; + ++cur; + if (cur == objIdx) { + uint32_t current = 0; + auto it = q.itemCounts.find(q.itemObjectives[i].itemId); + if (it != q.itemCounts.end()) current = it->second; + uint32_t required = q.itemObjectives[i].required; + bool finished = (current >= required); + // Get item name if available + std::string itemName; + const auto* info = gh->getItemInfo(q.itemObjectives[i].itemId); + if (info && !info->name.empty()) itemName = info->name; + else itemName = "Item #" + std::to_string(q.itemObjectives[i].itemId); + std::string text = itemName + ": " + std::to_string(current) + "/" + std::to_string(required); + lua_pushstring(L, text.c_str()); + lua_pushstring(L, "item"); + lua_pushboolean(L, finished ? 1 : 0); + return 3; + } + } + lua_pushnil(L); + return 1; +} + +// ExpandQuestHeader / CollapseQuestHeader — no-ops (flat quest list, no headers) +static int lua_ExpandQuestHeader(lua_State* L) { (void)L; return 0; } +static int lua_CollapseQuestHeader(lua_State* L) { (void)L; return 0; } + +// GetQuestLogSpecialItemInfo(questLogIndex) — returns nil (no special items) +static int lua_GetQuestLogSpecialItemInfo(lua_State* L) { (void)L; lua_pushnil(L); return 1; } + // --- Skill Line API --- // GetNumSkillLines() → count @@ -4142,6 +4227,11 @@ void LuaEngine::registerCoreAPI() { {"RemoveQuestWatch", lua_RemoveQuestWatch}, {"IsQuestWatched", lua_IsQuestWatched}, {"GetQuestLink", lua_GetQuestLink}, + {"GetNumQuestLeaderBoards", lua_GetNumQuestLeaderBoards}, + {"GetQuestLogLeaderBoard", lua_GetQuestLogLeaderBoard}, + {"ExpandQuestHeader", lua_ExpandQuestHeader}, + {"CollapseQuestHeader", lua_CollapseQuestHeader}, + {"GetQuestLogSpecialItemInfo", lua_GetQuestLogSpecialItemInfo}, // Skill line API {"GetNumSkillLines", lua_GetNumSkillLines}, {"GetSkillLineInfo", lua_GetSkillLineInfo},