feat: standardize v1.5.0 and consolidate stability fixes

This commit is contained in:
Xurkon
2026-03-28 11:08:32 -05:00
parent 4c7a318de6
commit 59c33e9f9c
26 changed files with 942 additions and 771 deletions
+20 -174
View File
@@ -1,5 +1,24 @@
# Changelog
## v1.5.0 (2026-03-28)
- **[Fix — Database Robustness]** Added strict guards against invalid or zero IDs in `QuestieDB` lookup functions (`GetNPC`, `GetObject`, `GetItem`). This prevents the "rawdata is nil" debug spam that occurred when custom plugins attempted to access uninitialized or malformed entity data.
- **[Refactor — Lookup Logic]** Refactored `QuestieDB` override handling to support both numeric and string keys simultaneously. This ensures that custom server data (e.g., Ascension, Ebonhold) is correctly resolved regardless of how the third-party plugin formats its internal IDs.
- **[Refactor — Data Injection]** Updated `QuestieLearner:InjectLearnedData` to enforce numeric key normalization. All dynamically learned spawn data is now strictly indexed by numeric IDs, preventing type-mismatch collisions during database merging.
- **[Feature — Enhanced Logging]** Improved Stage 3 initialization logging to provide detailed reporting on custom data injection. Developers can now verify the exact number of NPCs, Objects, and Items injected by plugins directly from the `DEVELOP` log.
- **[Fix — Ascension Zone Mapping]** Fixed a regression in `QuestieCompat` where `uiMapData` for Ascension-specific zones was not correctly propagating to the global mapping table, restoring map pin functionality for seasonal and custom zones.
- **[Fix — QuestieLearner]** Centralized zone/area ID lookup in `l10n` module to prevent `GetAreaIdByLocalName` nil errors (Fixes Project Ebonhold runtime crash).
- **[Feature — Session Export]** Added `session_export` skill for standardized documentation and session artifact exports.
- **[Fix — Custom Server Compilation]** Fixed database compilation not running on custom servers (Ascension, Ebonhold, Turtle WoW, etc.) where plugins inject data after initial load.
- Modified `Modules/QuestieInit.lua` Stage1 to defer compilation to Stage3 for custom servers, ensuring plugins finish injecting data before compilation runs.
- Added `l10n:Initialize()` and `QuestieCorrections:MinimalInit()` calls when deferring to Stage3, as Stage2 (`QuestieJourney:Initialize()`) requires `hiddenQuests` to be populated.
- Added "Bronzebeard" and "Warcraft Reborn" to Ascension realm detection patterns in `Modules/QuestieServer.lua`.
- **[Fix — Zone Mapping Bug]** Fixed incorrect key assignment in `QuestiePluginAPI:InjectZoneTables()`. Changed `areaIdToUiMapId[uiMapId] = uiMapId` to `areaIdToUiMapId[areaId] = uiMapId` at line 138. This caused zone lookups to fail, resulting in "No UiMapID or fitting parentAreaId" errors for custom zone IDs.
### Known Issues (v1.5.0)
- **[Ebonhold Detection]** The `Questie.IsEbonhold` flag detection remains brittle. It currently relies on a substring match against `GetRealmName()`. Variations like "Test Ebonhold" or localized names may cause the flag to fail, resulting in missing Questie-X-Ebonhold features. A more robust detection pattern using `GetCVar("realmName")` or server-pushed flags is planned for a future update.
## v1.4.9 (2026-03-26)
- **[Fix — Quest Cache]** Resolved the "GetQuest: The quest doesn't exist in QuestLogCache" fatal error occurring during initialization on the Ascension WoW client.
@@ -14,179 +33,6 @@
- **[Fix — Zone Mapping]** Added UiMapId overrides for 1415 (Eastern Kingdoms) and 947 (Azeroth) in `zoneDB.lua` to resolve "No AreaId found" warnings on Ascension servers.
- **[Fix — Quest Validation]** Fixed `QuestieValidateGameCache` to silently skip "ghost quests" (removed from database but still in quest log) instead of failing validation, resolving infinite retry loops on servers with custom quest content.
- **[Fix — AscensionDB]** Fixed syntax error in `AscensionNpcDB_2.lua` (missing closing `}` at end of NPC entry for ID 3287).
## v1.4.8 (2026-03-25)
- **[Plugin Synchronization]** Overhauled the Questie-X plugin loading architecture. Introduced `QuestiePluginAPI:FinishLoading()` and a registration handshake to resolve race conditions during addon initialization. By ensuring that database plugins report their data-injection status before `QuestieInit` Stage 3 completes, we eliminated "ghost maps" where pins and statistics would fail to render until a manual `/reload`.
- **[Universal Lua 5.0 Refactor]** Executed a codebase-wide transition from `pairs()` and `ipairs()` to `next()` and numeric loops. This refactor targets the Lua 5.0 engine used by legacy clients (e.g., Turtle WoW), which can exhibit inconsistent behavior or performance degradation when using standard iterators in high-frequency database sweeps. This change guarantees stable, universal performance across all WoW versions from 1.12 to 3.3.5+.
- **[Fix — MapIconTooltip]** Fixed a critical syntax error in `MapIconTooltip.lua` at line 239. A malformed `if` statement was trapping execution, preventing tooltips from updating when hovering over Quest objectives on the World Map.
- **[Fix — AscensionDB]** Enhanced realm-specific logic in `AscensionLoader.lua`. The loader now utilizes an pattern-matching check against `GetRealmName()` to correctly identify and apply custom database overrides for all Project Ascension realms, including seasonal and specialized rule-set servers.
- **[Performance]** Refined the background loading throttler in `QuestieInit`. The initialization sequence now yields more efficiently to the main UI thread during massive database injections, reducing "frame-stutter" during the initial login sequence while strictly maintaining loading priority for essential UI modules.
## v1.4.7 (2026-03-22)
- **[Quest Cache]** Resolved the "Quest cache validation timed out!" error during initialization. Increased the validation timeout from 3 to 10 seconds and relaxed the internal validation criteria to prevent false-positives on slow servers or with custom quest data.
- **[Database Plugin Architecture]** Refactored the WotLK database plugin to avoid monolithic global arrays. Database tables are now populated safely within a localized `addonTable` rather than injecting payloads directly into `_G.QuestieDB`.
- **[Taint Resolution]** By avoiding the creation of large `_G` variables during database chunk loading, the `Questie-X-WotLKDB` module is now clean of taint vectors. This definitively resolves the `ADDON_ACTION_BLOCKED` errors that occurred when utilizing secure actions, such as `UseAction()` or `CastSpellByName()`, with `Questie-X-WotLKDB` enabled.
- **[Database Initialization]** Fixed a capitalization issue in the WotLK database plugin export globals that prevented `QuestieInit` from correctly finding and absorbing the loaded database statistics and payloads.
## v1.4.6 (2026-03-22)
- **[Fix]** Resolved issue where Questie would not save options or show the Welcome screen repeatedly. This was caused by version mismatches in `.toc` files and an initialization race condition.
- **[Fix]** Synchronized AceAddon registration name to `"Questie-X"` to match the folder name, ensuring proper `ADDON_LOADED` event handling and database initialization.
- **[Fix]** Patched `AceGUI-3.0` widgets (`Heading`, `Frame`, `Window`, `Icon`, `DropDown-Items`, `ColorPicker`) to use string texture paths instead of numeric `FileDataIDs`, resolving "red texture" issues on WotLK 3.3.5a clients.
- **[Cleanup]** Systematically removed all `QX:` debug print statements across the entire codebase for a cleaner production experience.
- **[Version]** Bumped version to 1.4.6 and updated `Interface` version to 30300 across all `.toc` files.
## v1.4.5 — Network & Taint Stability Update
- **[Network Fix]** Resolved a critical crash ("`Usage: AceSerializer:Deserialize(str): str must be a string, got table`") occurring in QuestieLearnerComms and Export functions. This was caused by a lightweight, customized `AceSerializer-3.0.lua` implementation in Questie-X that lacked proper `self` parameter handling for standard colon-syntax method calls (`:`). As a result, method calls were serializing/deserializing the library table itself instead of the intended payload string. We patched `AceSerializer` natively to dynamically support both dot (`.`) and colon (`:`) syntax seamlessly without dropping arguments, while ensuring `Deserialize` correctly yields `(success, result)` tuples expected by the calling functions.
- **[Network Fix]** Expanded the previous LibDeflate network crash hotfix: completely purged all 71 instances of unsafe `#` -> `table.getn` replacements injected by earlier vanilla compatibility automation throughout `LibDeflate.lua`. These have been wrapped with a custom dual-typed safe access function that flawlessly determines whether to compute properties natively via `string.len(x)` for strings or the standard `table.getn(x)` for structured tables—guaranteeing 100% stable networking cross-client from 1.12 to 3.3.5 and upwards.
- **[Network Fix]** Fixed a critical Lua error (`bad argument #1 to 'getn' (table expected, got string)`) occurring during data-sharing via the hidden `questiecomm` addon channel. A legacy string length method (`table.getn`) was mistakenly used in `LibDeflate` string decoding; this has been restored to the universally compatible `string.len`.
- **[Init Fix]** Fixed an issue where the database loader would silently abort on custom 3.3.5 / legacy client setups due to false-positive modern client detection. The `isModernClient` safeguard now uses a bulletproof `tocversion` range check to flawlessly differentiate between original legacy engines and modern Classic counterparts.
- **[QuestieLearner Fix]** Resolved an `attempt to index global 'l10n' (a nil value)` error that triggered when accepting a new quest, caused by a missing module import at the top of `QuestieLearner.lua`.
- **[Taint Fix]** Resolved lingering `ADDON_ACTION_BLOCKED` taint on `ActionButton` and `StaticPopup` that occurred when Questie loaded data from its cache rather than manually recompiling. The `_G` namespace cleanup for `Questie-X-WotLKDB` globals now runs accurately during cached loads (via `UpdateWotLKDBStats`), ensuring the taint vector is closed and additionally freeing ~20MB of redundant cached memory.
- **[Taint Fix]** Resolved `ADDON_ACTION_BLOCKED` errors caused by `Questie-X-WotLKDB` leaving tainted global variables in `_G` after initialization. `QuestieInit._pullGlobal` now sets `_G[globalName] = nil` immediately after copying each WotLKDB table reference into `QuestieDB`, removing the taint vector while keeping all data fully accessible through `QuestieDB`.
## v1.4.4 — AceGUI Pool & Event Handling Fixes
- **[AceGUI Fix]** Fixed `Compat/embeds.xml` to load Wrath-compatible Ace library versions from `Libs/` (AceGUI-3.0 v34, AceConfigDialog-3.0 v66) instead of newer versions from `..\Libs/` that caused widget pool corruption.
- **[AceGUI Fix]** Added nil checks throughout AceGUI-3.0 (`Create`, `Release`, `WidgetBase.Fire`, `WidgetContainerBase` methods) to prevent crashes when pooled widgets have corrupted/nil properties.
- **[AceGUI Fix]** Added content nil checks to layout functions (List, Flow, Fill, Table) to prevent crashes when `content` is nil during layout.
- **[AceGUI Fix]** Applied same nil check fixes to `Compat/Libs/AceGUI-3.0/AceGUI-3.0.lua` for consistency.
- **[Event Fix]** Added nil check for `message` parameter in `QuestieEventHandler:ChatMsgSystem` to prevent "bad argument #1 to 'find'" errors.
- **[Event Fix]** Added nil check for `level` parameter in `QuestiePlayer:SetPlayerLevel` to prevent "number expected, got nil" errors.
- **[QuestieLearner Fix]** Added `SanitizeData` function with depth limiting and proper key/value filtering to remove functions, userdata, and thread values from learned data before network serialization.
- **[QuestieLearner Fix]** Added pcall wrapper around AceSerializer:Serialize to catch and log any remaining serialization errors instead of crashing.
- **[QuestieLearner Fix]** Added early return checks in `BroadcastLearnedData` when data is nil or sanitization produces empty results.
- **[Journey Fix]** Added nil check for `container` in `HandleTabChange` to prevent "attempt to index local 'container'" errors.
- **[l10n Fix]** Added type check for `translationValue` in l10n:translate to prevent "bad argument #2 to 'format'" errors when translation is not a string or when format arguments are missing.
- **[Tracker Fix]** Removed redundant shift-click tracking toggle logic in `Hooks.lua` that was instantly reverting tracker states when shift-clicking a quest in the Quest Log.
## v1.4.3 — Taint & API Compatibility Fixes
- **[Taint Fix]** Deferred SetItemRef hook execution using C_Timer.After to avoid tainting protected execution contexts.
- **[Taint Fix]** Removed redundant `_G = _G or {}` from WotLKDB data file that could contribute to namespace pollution.
- **[Taint Fix]** Moved xpcall polyfill from bare `_G.xpcall` to `QuestieCompat.xpcall` namespace to prevent polluting the global table.
- **[API Fix]** Added polyfills for `GetCurrentRegion` and `GetCurrentRegionName` for AceDB-3.0 compatibility on WotLK/Classic.
- **[API Fix]** Added polyfills for `Ambiguate` and `RegisterAddonMessagePrefix` for AceComm-3.0 compatibility on WotLK/Classic.
- **[API Fix]** Added conditional check for `DialogBorderOpaqueTemplate` and `SetFixedFrameStrata` in AceConfigDialog for WotLK/Classic.
- **[AceGUI Fix]** Added WotLK-compatible fallback for `SetColorTexture` using `SetTexture` + `SetVertexColor`.
## v1.4.2 — Quest Route Optimization
- **[Feature]** Added Quest Route Optimization with three modes: Single Quest, All Tracked Quests, and TSP Approximation.
- **[Feature]** Route mode can be selected in Tracker options under "Route Mode".
- **[Feature]** Nearest-neighbor TSP algorithm for calculating optimal quest routes.
- **[Feature]** Visual route lines drawn on map connecting objectives in optimized order.
## v1.4.1 — Cleanup & Repository Maintenance
- **[Cleanup]** Removed `.history/` and `Research/` folders from repository tracking (were already gitignored but previously committed).
- **[Cleanup]** Removed `Tests/` folder from repository tracking.
- **[Badge]** Updated downloads badge format in README.
- **[Gitignore]** Added `tests/` to `.gitignore`.
## v1.4.0 — Taint Resolution & Compatibility Fixes
- **[C_Timer Fix]** Fixed `C_Timer` OnUpdate to use precise `(self, elapsed)` parameter instead of `1/GetFramerate()`.
- **[Achievement Fix]** Fixed `IsAchievementCompleted` to properly check completion boolean via `select(4, GetAchievementInfo(...))`.
- **[Map Fix]** Fixed `C_Map.GetPlayerMapPosition` to use correct legacy API (`GetPlayerMapPosition("player")`).
- **[QuestieLearner Fix]** Fixed `GetNpcIdFromGUID` and `GetObjectIdFromGUID` being called before definition.
- **[Taint Fix]** Added `InCombatLockdown()` guards and `pcall` wrappers to secure hooks to prevent protected function access errors.
## v1.3.9 — BackdropTemplate & Tracking Reliability
- **[Quest Tracking]** Significantly improved the reliability of shift-clicking to track or untrack quests.
- **[Quest Re-tracking]** Resolved an issue where hidden quests would refuse to re-track after being manually untracked.
- **[AceGUI Fix]** Fixed critical crash: `Couldn't find inherited node "BackdropTemplate"`.
- **[Verification]** Performed a comprehensive syntax audit using `luaparse`.
## v1.3.8 — UseAction Taint Fixes
- **[Taint Resolution]** Successfully resolved the `ADDON_ACTION_BLOCKED: UseAction()` error by updating internal libraries and eliminating global namespace pollution.
- **[Library Update]** Updated `Compat/embeds.xml` to use modern, taint-free versions of core libraries.
- **[Addon Stability]** Audited `QuestieInit.lua` and `QuestieLoader.lua` for safe global population.
## v1.3.7 — Quest Tracking & Robustness
- **[Quest Tracking]** Resolved inconsistent quest tracking/untracking by making the tracking state idempotent. This eliminates "doing nothing" results while toggling quests in the Quest Log and prevents tracking loops caused by Blizzard's auto-track feature.
- **[Robustness]** Improved `QuestieTracker` to safely handle `nil` returns from the WoW API (`GetQuestLogTitle`), preventing potential "attempt to compare number with nil" errors during rapid quest log updates.
- **[Internal Logic]** Refined the detection of internal Blizzard objective updates to ensure they don't accidentally untrack quests that the user intended to watch.
- **[Linting]** Suppressed diagnostic warnings related to global namespace shimming in `QuestieCompat.lua`.
## v1.3.6 — Tooltip Fixes & Enhanced Taint Workaround
- **[Quest Progress]** Resolved an issue where quest tooltips would not reliably update their progress counts when dynamically learning AI spawns or during rapid kill credit updates.
- **[Taint Workaround]** Reinforced the `WorldMapTaintWorkaround` for older WoW clients (3.3.5) by correcting the `IsAddOnLoaded` detection timing and adding explicit empty-function stubs to blocked dropdowns.
- **[Diagnostics]** Improved error reporting for global variable collisions during module initialization.
## v1.3.5 — Comprehensive Taint & Architecture Fixes
*A comprehensive update resolving 13 distinct issues related to global environment taint, unsafe polyfills, and module stability across all supported Lua environments (5.0, 5.1, 5.2).*
### Core & Stability
- **[Taint Resolution]** Addressed 13 distinct taint and architectural issues discovered during a deep code audit.
- **[Taint Fix]** Removed unsafe `function Questie:Warning(...)` global monkey-patching in `QuestieTracker.lua` (`_InstallMissingQuestLogWarningFilter` deleted). Ghost quest iterations now use a safe, two-phase collection and deletion pattern to prevent warning generation upfront.
- **[Global Safety]** `hooksecurefunc` polyfill in `QuestieCompat.lua` no longer pollutes the global `_G` namespace. It runs strictly as a local fallback when the native function is absent.
- **[Global Safety]** Removed bare `_G` writes for `C_Seasons` and `C_Timer`.
- **[Global Safety]** Prevented `_G.QuestieX_WotLKDB_Counts` from writing to the global namespace in `QuestieInit.lua`. Counts are now cleanly stored directly on the plugin object (`plugin.stats`).
- **[Module Safety]** The `CreateModule` function in `QuestieLoader.lua` now guards against double-registration of modules, preventing accidental aliasing and overwriting.
- **[Code Cleanup]** Eliminated duplicate shims (`string.match`, `select`) from `QuestieCompat.lua` that were already provided canonically by `QuestieLoader.lua`.
- **[Code Cleanup]** Replaced misleading, non-looping `while n > 0` structures in `select` polyfills with standard `do...end` blocks.
- **[Diagnostics]** Gated `loadstring` execution in `QuestieInit:LoadDatabase`. On modern clients, it now safely blocks and logs an error instead of executing potentially tainted legacy string DBs at runtime.
## v1.3.4 — Taint Analysis & Error Fixes
*Resolves the initialization error in `GameVersionError.lua` and implements a version guard to support Classic-era private servers. Also confirms the integrity of the WotLKDB module after a deep taint analysis.*
### Core & Stability
- **[Fix]** Resolved `attempt to call local 'l10n' (a table value)` in `GameVersionError.lua` and implemented a `tocVersion` guard to prevent the "unsupported client" error on Classic-era private servers (e.g., Turtle WoW).
- **[Fix]** Updated `GameVersionError.lua` strings to correctly identify Questie-X as supporting Classic and private servers.
- **[Taint Analysis]** Completed a full audit of `Questie-X-WotLKDB`. No direct taint vectors or secure function overrides were found.
- **[Version Sync]** Synchronized versions to v1.3.4 across all components.
## v1.3.3 — Critical Taint Fix & Module Loading
*Addresses the missing `WorldMapTaintWorkaround` module that was excluded from previous releases, finally enabling the intended seat-of-pants taint mitigation strategy.*
### Core & Stability
- **[Taint Fix]** Officially included `Modules\WorldMapTaintWorkaround.lua` in all TOC files to ensure it's loaded and executed.
- **[Error Handling]** Included `Modules\GameVersionError.lua` in all TOC files for better unsupported client detection.
- **[Version Sync]** Synchronized version numbers across all 4 TOC flavors.
---
## v1.3.2 — Taint Resolution & Stability
*Finalizes the Taint Resolution project, eliminating `ADDON_ACTION_BLOCKED: UseAction()` errors by refactoring internal hooks to use secure alternatives and hardening the global namespace against collisions.*
### Core & Stability
- **[Taint Resolution]** Refactored `Hooks.lua` to use `hooksecurefunc` instead of raw hooks for all secure functions.
- **[Global Safety]** Enhanced `QuestieLoader.lua` with a new collision-aware `PopulateGlobals` engine that prevents overwriting existing global variables and provides diagnostic warnings.
- **[Security]** Eliminated global namespace modifications in `QuestieInit.lua`.
- **[Workaround Hardening]** Refactored `WorldMapTaintWorkaround.lua` to remove legacy global function reassignments that were causing secondary taint.
---
## v1.3.8
- **[QuestieTracker]** Resolved persistent "stuck" quest tracking/untracking by ensuring `AQW_Insert` and `RemoveQuestWatch` hooks respect manual untracking state.
- **[QuestieTracker]** Refined watch hooks to distinguish between manual user actions (e.g. shift-clicking in Quest Log) and internal Blizzard/server objective updates.
- **[QuestieTracker]** Added robustness for Quest IDs passed directly to Blizzard watch APIs (common on custom WoW private servers).
- **[Global Safety]** Added lint suppressions for external frames (`VoiceOverFrame`, etc.) in `QuestieTracker.lua`.
## v1.3.1
- Refined data-sharing mechanism to use exclusively hidden global channels, removing guild-channel broadcasts to minimize chat traffic.
## v1.3.0 — QuestieLearner Confidence, Global Sharing & Stale Data Cleanup
### QuestieLearner.lua — Precision & Confidence
- **[Coordinate Scaling Fix]** Fixed player coordinates being recorded on a 0-1 scale; now correctly scales to 0-100 for compatibility with Questie map pins.
- **[Confidence Rating System]** Introduced a confidence system based on "Match Count" (`mc`). Data is now categorized as "Unconfirmed" (low confidence) or "Verified" (high confidence).
- **[Map Pin Gating]** Learned map pins (sword icons) now only appear after reaching a configurable confidence threshold (default: 2).
- **[Confidence in Tooltips]** NPC and Object tooltips now display their confidence level (e.g., `(Learned - Confidence: 2)`).
- **[Timestamp Tracking]** Added `lastSeen` (`ls`) timestamps to all learned entries to track data freshness.
@@ -469,7 +315,7 @@
- **[Options]** New **Credits Tab**! A dedicated tab in the options menu to acknowledge contributors and community partners.
- **[Tutorial]** Improved tutorial flows for objective type selection.
### Core & Compatibility
### Core & Stability (v1.3.5)
- **[Lua 5.0]** Globally polyfilled `string.match` and `string.gmatch` using `string.find` and `string.gfind` to ensure universal compatibility with legacy WoW clients (e.g., Turtle WoW).
- **[AceTimer]** Patched embedded `AceTimer-3.0` instances in ElvUI and OG-RaidHelper to resolve `math.mod` errors on Lua 5.0 clients.
+4
View File
@@ -115,6 +115,10 @@ function QuestieEvent:Load()
end
end
if not QuestieEvent.eventQuests then
return
end
for _, questData in pairs(QuestieEvent.eventQuests) do
local eventName = questData[1]
local questId = questData[2]
+130 -2
View File
@@ -33,6 +33,31 @@ local l10n = QuestieLoader:ImportModule("l10n")
---@type QuestLogCache
local QuestLogCache = QuestieLoader:ImportModule("QuestLogCache")
-- Dummy handles to prevent Stage 2 crashes during deferred initialization
local _dummyHandle = {
QuerySingle = function() return nil end,
Query = function() return nil end,
pointers = {}
}
QuestieDB.QueryNPC = _dummyHandle.Query
QuestieDB.QueryQuest = _dummyHandle.Query
QuestieDB.QueryObject = _dummyHandle.Query
QuestieDB.QueryItem = _dummyHandle.Query
QuestieDB.QueryQuestSingle = _dummyHandle.QuerySingle
QuestieDB.QueryNPCSingle = _dummyHandle.QuerySingle
QuestieDB.QueryObjectSingle = _dummyHandle.QuerySingle
QuestieDB.QueryItemSingle = _dummyHandle.QuerySingle
QuestieDB._QueryQuestSingle = _dummyHandle.QuerySingle
QuestieDB._QueryNPCSingle = _dummyHandle.QuerySingle
QuestieDB._QueryObjectSingle = _dummyHandle.QuerySingle
QuestieDB._QueryItemSingle = _dummyHandle.QuerySingle
-- Initialize pointers to empty tables to prevent Stage 2 crashes during deferred initialization
QuestieDB.QuestPointers = _dummyHandle.pointers
QuestieDB.NPCPointers = _dummyHandle.pointers
QuestieDB.ObjectPointers = _dummyHandle.pointers
QuestieDB.ItemPointers = _dummyHandle.pointers
---@type QuestieQuest
local QuestieQuest = QuestieLoader:ImportModule("QuestieQuest")
---@type QuestieQuestPrivate
@@ -338,6 +363,10 @@ function QuestieDB:Initialize()
end
function QuestieDB:GetObject(objectId)
if self ~= QuestieDB then
objectId = self
end
objectId = tonumber(objectId)
if not objectId then
return nil
end
@@ -348,6 +377,10 @@ function QuestieDB:GetObject(objectId)
--local rawdata = QuestieDB.objectData[objectId];
local rawdata = QuestieDB.QueryObject(objectId, QuestieDB._objectAdapterQueryOrder)
if not rawdata and QuestieDB.objectDataOverrides then
rawdata = QuestieDB.objectDataOverrides[objectId] or QuestieDB.objectDataOverrides[tostring(objectId)]
end
if not rawdata then
Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestieDB:GetObject] rawdata is nil for objectID:", objectId)
return nil
@@ -368,6 +401,10 @@ function QuestieDB:GetObject(objectId)
end
function QuestieDB:GetItem(itemId)
if self ~= QuestieDB then
itemId = self
end
itemId = tonumber(itemId)
if (not itemId) or (itemId == 0) then
return nil
end
@@ -377,6 +414,10 @@ function QuestieDB:GetItem(itemId)
local rawdata = QuestieDB.QueryItem(itemId, QuestieDB._itemAdapterQueryOrder)
if not rawdata and QuestieDB.itemDataOverrides then
rawdata = QuestieDB.itemDataOverrides[itemId] or QuestieDB.itemDataOverrides[tostring(itemId)]
end
if not rawdata then
Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestieDB:GetItem] rawdata is nil for itemID:", itemId)
return nil
@@ -1210,7 +1251,16 @@ end
---@param questId QuestId
---@return Quest|nil @The quest object or nil if the quest is missing
function QuestieDB.GetQuest(questId) -- /dump QuestieDB.GetQuest(867)
function QuestieDB.GetQuest(questId, ...) -- /dump QuestieDB.GetQuest(867)
-- Handle colon calling QuestieDB:GetQuest(questId)
if type(questId) == "table" and questId == QuestieDB then
questId = select(1, ...)
end
-- Handle string input from console or other sources
if type(questId) == "string" then
questId = tonumber(questId)
end
if not questId then
Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestieDB.GetQuest] No questId.")
return nil
@@ -1220,11 +1270,15 @@ function QuestieDB.GetQuest(questId) -- /dump QuestieDB.GetQuest(867)
end
local rawdata = QuestieDB.QueryQuest(questId, QuestieDB._questAdapterQueryOrder)
local overrideData = QuestieDB.questDataOverrides and (QuestieDB.questDataOverrides[questId] or QuestieDB.questDataOverrides[tostring(questId)])
if (not rawdata) then
rawdata = overrideData
if (not rawdata) then
Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestieDB.GetQuest] rawdata is nil for questID:", questId)
if questId == 0 then
Questie:Error("[QuestieDB.GetQuest] rawdata is nil for questID:", questId)
end
print(debugstack())
return nil
end
@@ -1294,6 +1348,39 @@ function QuestieDB.GetQuest(questId) -- /dump QuestieDB.GetQuest(867)
stringKey, intKey = next(questKeys, stringKey)
end
-- Support for index-aware objective mapping (QuestieLearner & custom overrides)
if rawdata.objIndex then QO.objIndex = rawdata.objIndex end
-- Apply overrides to QO directly (merging objectives if needed)
if overrideData and rawdata ~= overrideData then
local _sKey, _iKey = next(questKeys)
while _sKey do
if overrideData[_iKey] then
if _sKey == "objectives" and QO.objectives then
-- Merge objectives (index 10)
local _objIdx, _objList = next(overrideData[_iKey])
while _objIdx do
if not QO.objectives[_objIdx] then
QO.objectives[_objIdx] = _objList
else
local _id, _data = next(_objList)
while _id do
QO.objectives[_objIdx][_id] = _data
_id, _data = next(_objList, _id)
end
end
_objIdx, _objList = next(overrideData[_iKey], _objIdx)
end
else
QO[_sKey] = overrideData[_iKey]
end
end
_sKey, _iKey = next(questKeys, _sKey)
end
if overrideData.objIndex then QO.objIndex = overrideData.objIndex end
end
local questLevel, requiredLevel = QuestieLib.GetTbcLevel(questId)
QO.level = questLevel
QO.requiredLevel = requiredLevel
@@ -1357,6 +1444,25 @@ function QuestieDB.GetQuest(questId) -- /dump QuestieDB.GetQuest(867)
---@type Objective[]
QO.ObjectiveData = {}
-- Support for index-aware objective mapping (QuestieLearner & custom overrides)
-- This handles "kill-credit" scenarios and prevents "UI swapping" by pinning
-- entities to the specific quest log objective index 'j'.
if QO.objIndex then
local _index, _data = next(QO.objIndex)
while _index do
if _data and _data.id then
QO.ObjectiveData[_index] = {
Type = _data.type or "monster",
Id = _data.id,
Text = _data.text or "",
-- Store original mapping for reference
_isIndexMapped = true,
}
end
_index, _data = next(QO.objIndex, _index)
end
end
---@type RawObjectives
local objectives = QO.objectives
if objectives then
@@ -1364,6 +1470,18 @@ function QuestieDB.GetQuest(questId) -- /dump QuestieDB.GetQuest(867)
local _creatureObjective, creatureObjective = next(objectives[1])
while _creatureObjective do
if creatureObjective then
-- Only insert if not already mapped via objIndex
local alreadyMapped = false
if QO.objIndex then
for _, existing in pairs(QO.ObjectiveData) do
if existing.Id == creatureObjective[1] then
alreadyMapped = true
break
end
end
end
if not alreadyMapped then
---@type NpcObjective
table.insert(QO.ObjectiveData, {
Type = "monster",
@@ -1372,6 +1490,7 @@ function QuestieDB.GetQuest(questId) -- /dump QuestieDB.GetQuest(867)
HideCondition = creatureObjective[3],
})
end
end
_creatureObjective, creatureObjective = next(objectives[1], _creatureObjective)
end
end
@@ -1739,7 +1858,11 @@ local a = {
---@param npcId number
---@return table
function QuestieDB:GetNPC(npcId)
if not npcId then
if self ~= QuestieDB then
npcId = self
end
npcId = tonumber(npcId)
if not npcId or npcId == 0 then
return nil
end
if _QuestieDB.npcCache[npcId] then
@@ -1747,6 +1870,11 @@ function QuestieDB:GetNPC(npcId)
end
local rawdata = QuestieDB.QueryNPC(npcId, QuestieDB._npcAdapterQueryOrder)
if not rawdata and QuestieDB.npcDataOverrides then
rawdata = QuestieDB.npcDataOverrides[npcId] or QuestieDB.npcDataOverrides[tostring(npcId)]
end
if (not rawdata) then
Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestieDB:GetNPC] rawdata is nil for npcID:", npcId)
return nil
+28 -40
View File
@@ -61,7 +61,7 @@ function ZoneDB:Initialize()
end
function _ZoneDB:GenerateParentZoneToStartingZoneTable()
for startingZone, parentZone in pairs(subZoneToParentZone) do
for startingZone, parentZone in next, subZoneToParentZone do
parentZoneToSubZone[parentZone] = startingZone
end
end
@@ -87,49 +87,32 @@ function ZoneDB:GetAreaIdByUiMapId(uiMapId)
local foundId
-- First we look for a direct match
for AreaUiMapId, lAreaId in pairs(uiMapIdToAreaId) do
for AreaUiMapId, lAreaId in next, uiMapIdToAreaId do
local areaId = lAreaId
if (AreaUiMapId == uiMapId and not foundId) then
--Questie:Debug(Questie.DEBUG_DEVELOP, "[ZoneDB:GetAreaIdByUiMapId] : ", " AreaUiMapId: ", AreaUiMapId, " == uiMapId: ", uiMapId, " and areaId = ", areaId, " foundID is nil")
foundId = areaId
elseif AreaUiMapId == uiMapId and foundId ~= AreaUiMapId then
-- If we find a second match that does not match the first
-- Print an error, but we still return the first one we found.
-- Only print if debug is enabled.
if Questie.db.profile.debugEnabled then
Questie:Error("[ZoneDB:GetAreaIdByUiMapId] : ", "UiMapId", uiMapId, "has multiple AreaIds:", foundId,
areaId)
Questie:Error("[ZoneDB:GetAreaIdByUiMapId] : ", "UiMapId", uiMapId, "has multiple AreaIds:", foundId, areaId)
end
end
end
if foundId then -- debug --TechnoHunter adding debug print to report found AreaId
--if Questie.db.profile.debugEnabled then
--local uiMapInfo = C_Map.GetMapInfo(uiMapId)
--local foundName = C_Map.GetAreaInfo(foundId)
--Questie:Debug(Questie.DEBUG_DEVELOP, "[ZoneDB:GetAreaIdByUiMapId] : ", "Found AreaId", foundName, ":", foundId, " for UiMapId", uiMapInfo.name, ":", uiMapId, "direct match")
--end
if foundId then
return foundId
else
-- As a last resort we try to match AreaId and UiMapId by name
-- uses the original table in zoneTables as the area id's are
-- all in that and we dont care if the uiMapId is there or not
for areaId in pairs(areaIdToUiMapId) do
for areaId in next, areaIdToUiMapId do
local mapInfo = C_Map.GetMapInfo(uiMapId)
local areaName = C_Map.GetAreaInfo(areaId)
if mapInfo and mapInfo.name == areaName then
Questie:Debug(Questie.DEBUG_DEVELOP, "[ZoneDB:GetAreaIdByUiMapId] : ", "Found AreaId", areaName, ":",
areaId, "for UiMapId", mapInfo.name, ":", uiMapId, "by name")
Questie:Debug(Questie.DEBUG_DEVELOP, "[ZoneDB:GetAreaIdByUiMapId] : ", "Found AreaId", areaName, ":", areaId, "for UiMapId", mapInfo.name, ":", uiMapId, "by name")
return areaId
end
end
if Questie.db.profile.debugEnabled then
-- NOTE: On Ascension, hub cities (Stormwind, Shattrath, etc.) may return a
-- continent-level UiMapId from GetBestMapForUnit. This is harmless — the nil
-- return is handled gracefully by GetCurrentZoneId's callers.
Questie:Debug(Questie.DEBUG_DEVELOP,
"No AreaId found for UiMapId: " ..
uiMapId .. ":" .. (C_Map.GetMapInfo(uiMapId) and C_Map.GetMapInfo(uiMapId).name or "nil"))
Questie:Debug(Questie.DEBUG_DEVELOP, "No AreaId found for UiMapId: " .. uiMapId .. ":" .. (C_Map.GetMapInfo(uiMapId) and C_Map.GetMapInfo(uiMapId).name or "nil"))
end
return nil
end
@@ -233,15 +216,17 @@ do
end
-- 1) Base Questie quests (QuestPointers values must be numbers; ignore anything weird)
for questId, ptr in pairs(QuestieDB.QuestPointers) do
if type(QuestieDB.QuestPointers) == "table" then
for questId, ptr in next, QuestieDB.QuestPointers do
if type(ptr) == "number" then
ProcessQuestId(questId)
end
end
end
-- 2) Ascension custom quests (from overrides list)
if type(QuestieDB.ascensionQuestIds) == "table" then
for questId in pairs(QuestieDB.ascensionQuestIds) do
for questId in next, QuestieDB.ascensionQuestIds do
ProcessQuestId(questId)
end
end
@@ -255,7 +240,7 @@ end
---@param zoneOrSort ZoneOrSort
function _ZoneDB:IsSpecialQuest(zoneOrSort)
for _, v in pairs(QuestieDB.sortKeys) do
for _, v in next, QuestieDB.sortKeys do
if zoneOrSort == v then
return true
end
@@ -272,11 +257,11 @@ function _ZoneDB:GetZonesWithQuestsFromNPCs(zones, npcIds, questId)
return zones
end
for npcId in pairs(npcIds) do
for npcId in next, npcIds do
local spawns = QuestieDB.QueryNPCSingle(npcId, "spawns")
if spawns then
for zone in pairs(spawns) do
if not zones[zone] then zones[zone] = {} end
for zone in next, spawns do
if (not zones[zone]) then zones[zone] = {} end
zones[zone][questId] = true
end
end
@@ -294,11 +279,11 @@ function _ZoneDB:GetZonesWithQuestsFromObjects(zones, objectIds, questId)
return zones
end
for objectId in pairs(objectIds) do
for objectId in next, objectIds do
local spawns = QuestieDB.QueryObjectSingle(objectId, "spawns")
if spawns then
for zone in pairs(spawns) do
if not zones[zone] then zones[zone] = {} end
for zone in next, spawns do
if (not zones[zone]) then zones[zone] = {} end
zones[zone][questId] = true
end
end
@@ -314,7 +299,7 @@ function _ZoneDB:SplitSeasonalQuests()
end
local questsToSplit = zoneMap[QuestieDB.sortKeys.SEASONAL]
-- Merging SEASONAL and SPECIAL quests to be split into real groups
for k, v in pairs(zoneMap[QuestieDB.sortKeys.SPECIAL]) do questsToSplit[k] = v end
for k, v in next, zoneMap[QuestieDB.sortKeys.SPECIAL] do questsToSplit[k] = v end
local updatedZoneMap = zoneMap
updatedZoneMap[-400] = {}
@@ -323,7 +308,7 @@ function _ZoneDB:SplitSeasonalQuests()
updatedZoneMap[-403] = {}
updatedZoneMap[-404] = {}
for questId, _ in pairs(questsToSplit) do
for questId, _ in next, questsToSplit do
local eventName = QuestieEvent:GetEventNameFor(questId)
if eventName == "Love is in the Air" then
updatedZoneMap[-400][questId] = true
@@ -345,9 +330,10 @@ end
function ZoneDB:GetRelevantZones()
local zones = {}
for category, data in pairs(l10n.zoneCategoryLookup) do
if type(l10n.zoneCategoryLookup) == "table" then
for category, data in next, l10n.zoneCategoryLookup do
zones[category] = {}
for id, zoneName in pairs(data) do
for id, zoneName in next, data do
local zoneQuests = zoneMap[id]
if (not zoneQuests) then
zones[category][id] = nil
@@ -356,6 +342,7 @@ function ZoneDB:GetRelevantZones()
end
end
end
end
return zones
end
@@ -372,13 +359,14 @@ function _ZoneDB:RunTests()
[308] = true, -- ScholomanceOLD
[309] = true, -- ScholomanceOLD
}
for _, map in pairs(maps) do
if type(maps) == "table" then
for _, map in next, maps do
--- We don't care about World, Continent or Cosmic
if map.mapType ~= Enum.UIMapType.World and map.mapType ~= Enum.UIMapType.Continent and map.mapType ~= Enum.UIMapType.Cosmic then
local success, result = pcall(ZoneDB.GetAreaIdByUiMapId, ZoneDB, map.mapID)
if not success and not buggedMaps[map.mapID] then
Questie:Error("[ZoneDBTests] ZoneDB.GetAreaIdByUiMapId fails for " ..
map.name .. " (" .. map.mapID .. "). Result: " .. result)
Questie:Error("[ZoneDBTests] ZoneDB.GetAreaIdByUiMapId fails for " .. map.name .. " (" .. map.mapID .. "). Result: " .. result)
end
end
end
end
+1 -1
View File
@@ -1075,7 +1075,7 @@ function QuestieDBCompiler:Compile()
or type(QuestieDB.questData) == "table"
or (type(QuestieDB.questDataOverrides) == "table" and next(QuestieDB.questDataOverrides) ~= nil)
if not hasData then
Questie:Print("|cFFFF4444[Questie-X]|r No database plugin loaded. Install a DB plugin for your server — see Options \226\134\146 Database.")
Questie:Print("|cFFFF4444[Questie-X]|r No database plugin loaded. Install a DB plugin for your server — see Options Database.")
return
end
+48 -4
View File
@@ -47,17 +47,56 @@ function l10n:InitializeLocaleOverride()
end
end
---@param zoneName string
---@return number
function l10n:GetAreaIdByLocalName(zoneName)
if not zoneName or zoneName == "" then return 0 end
for _, zoneTable in pairs(l10n.zoneLookup) do
for areaId, name in pairs(zoneTable) do
if name == zoneName then return areaId end
end
end
return 0
end
---@param areaId number
---@return string
function l10n:GetLocalNameByAreaId(areaId)
if not areaId or areaId <= 0 then return l10n("Unknown Zone") end
for _, zoneTable in pairs(l10n.zoneLookup) do
if zoneTable[areaId] then
return zoneTable[areaId]
end
end
return l10n("Unknown Zone")
end
---@param areaId number
---@return number
function l10n:GetContinentIdByAreaId(areaId)
if not areaId or areaId <= 0 then return 0 end
for continentId, zoneTable in pairs(l10n.zoneLookup) do
if zoneTable[areaId] then
return continentId
end
end
return 0
end
function l10n:Initialize()
-- Load item locales
for id, name in pairs(l10n.itemLookup[locale] or {}) do
if l10n.itemLookup and l10n.itemLookup[locale] then
for id, name in pairs(l10n.itemLookup[locale]) do
if QuestieDB.itemData[id] and name then
QuestieDB.itemData[id][QuestieDB.itemKeys.name] = name
end
end
end
-- data is {<questName>, {<questDescription>,...}, {<questObjective>,...}}
-- Load quest locales
for id, data in pairs(l10n.questLookup[locale] or {}) do
if l10n.questLookup and l10n.questLookup[locale] then
for id, data in pairs(l10n.questLookup[locale]) do
if QuestieDB.questData[id] then
if data[1] then
QuestieDB.questData[id][QuestieDB.questKeys.name] = data[1]
@@ -73,9 +112,11 @@ function l10n:Initialize()
end
end
end
end
-- Load NPC locales
for id, data in pairs(l10n.npcNameLookup[locale] or {}) do
if l10n.npcNameLookup and l10n.npcNameLookup[locale] then
for id, data in pairs(l10n.npcNameLookup[locale]) do
if QuestieDB.npcData[id] and data then
if type(data) == "string" then
QuestieDB.npcData[id][QuestieDB.npcKeys.name] = data
@@ -85,14 +126,17 @@ function l10n:Initialize()
end
end
end
end
-- Load object locales
for id, name in pairs(l10n.objectLookup[locale] or {}) do
if l10n.objectLookup and l10n.objectLookup[locale] then
for id, name in pairs(l10n.objectLookup[locale]) do
if QuestieDB.objectData[id] and name then
QuestieDB.objectData[id][QuestieDB.objectKeys.name] = name
end
end
end
end
--Must be run in a coroutine as it yields
function l10n:PostBoot()
+1 -7
View File
@@ -41,13 +41,7 @@ function QuestieJourneyUtils:AddLine(frame, text)
end
function QuestieJourneyUtils:GetZoneName(id)
local name = l10n("Unknown Zone")
for category, data in pairs(l10n.zoneLookup) do
if data[id] then
name = l10n.zoneLookup[category][id]
break
end
end
local name = l10n:GetLocalNameByAreaId(id)
-- Ascension can use custom UiMapIds for zones/sub-zones (e.g. 1238 Northshire Valley).
-- Those won't exist in l10n.zoneLookup (which is AreaId-based), so fallback to UiMapData / mapInfo.
-145
View File
@@ -164,148 +164,3 @@ function MessageHandlerFactory.New()
return handler
end
----- Tests -----
do
--? This is the tests for MessageHandlerFactory
local function RunMessageHandlerTests()
Questie:Debug(Questie.DEBUG_CRITICAL, " -- Running " .. Questie:Colorize("MessageHandlerFactory", "yellow") .. " tests --")
local testEvent = "EVENT_TEST"
--- Test simple usage
do
local MessageHandler = MessageHandlerFactory:New()
local returnedCount = 0
local incrementFunction = function()
returnedCount = returnedCount + 1
end
-- Add and fire
MessageHandler:RegisterRepeating(testEvent, incrementFunction)
MessageHandler:Fire(testEvent)
assert(returnedCount == 1, Questie:Colorize(" -- FAILED: Event was not fired", "red"))
-- Unregister and fire
MessageHandler:UnregisterRepeating(testEvent, incrementFunction)
MessageHandler:Fire(testEvent)
assert(returnedCount == 1, Questie:Colorize(" -- FAILED: Event was fired after unregistering", "red"))
-- Register two events and fire
MessageHandler:RegisterRepeating(testEvent, incrementFunction)
MessageHandler:RegisterRepeating(testEvent, incrementFunction)
MessageHandler:Fire(testEvent)
assert(returnedCount == 3, Questie:Colorize(" -- FAILED: Event was not fired twice", "red"))
-- Unregister all events and fire
MessageHandler:UnregisterAll(testEvent)
MessageHandler:Fire(testEvent)
assert(returnedCount == 3, Questie:Colorize(" -- FAILED: Event was fired after unregistering all", "red"))
-- Register once and fire
MessageHandler:RegisterOnce(testEvent, incrementFunction)
MessageHandler:Fire(testEvent)
MessageHandler:Fire(testEvent)
assert(returnedCount == 4, Questie:Colorize(" -- FAILED: Event was not fired once", "red"))
end
--- Test multiple registered events
do
local MessageHandler = MessageHandlerFactory:New()
local returnedCount = 0
local incrementFunction = function()
returnedCount = returnedCount + 1
end
local incrementFunction2 = function()
returnedCount = returnedCount + 1
end
local testEvent2 = "EVENT_TEST2"
MessageHandler:RegisterRepeating(testEvent, incrementFunction)
MessageHandler:RegisterRepeating(testEvent2, incrementFunction2)
MessageHandler:Fire(testEvent)
assert(returnedCount == 1, Questie:Colorize(" -- FAILED: Event 1 was not fired", "red"))
MessageHandler:Fire(testEvent2)
assert(returnedCount == 2, Questie:Colorize(" -- FAILED: Event 2 was not fired", "red"))
-- Unregister and fire
MessageHandler:UnregisterRepeating(testEvent, incrementFunction)
MessageHandler:Fire(testEvent)
assert(returnedCount == 2, Questie:Colorize(" -- FAILED: Event 1 was fired after unregistering", "red"))
MessageHandler:Fire(testEvent2)
assert(returnedCount == 3, Questie:Colorize(" -- FAILED: Event 2 was not fired", "red"))
MessageHandler:UnregisterRepeating(testEvent2, incrementFunction2)
MessageHandler:Fire(testEvent2)
assert(returnedCount == 3, Questie:Colorize(" -- FAILED: Event 2 was fired after unregistering", "red"))
end
--- Test return
do
local MessageHandler = MessageHandlerFactory:New()
local returnedCount = 0
local incrementReturnFunction = function()
returnedCount = returnedCount + 1
return returnedCount
end
-- Register mutliple events and fire
for _ = 1, 5 do
MessageHandler:RegisterRepeating(testEvent, incrementReturnFunction)
end
local retVal = MessageHandler:Fire(testEvent)
assert(retVal, Questie:Colorize(" -- FAILED: Return value was nil", "red"))
assert(retVal[1] == 1, Questie:Colorize(" -- FAILED: 1 Function value was not returned", "red"))
assert(retVal[2] == 2, Questie:Colorize(" -- FAILED: 2 Function value was not returned", "red"))
assert(retVal[3] == 3, Questie:Colorize(" -- FAILED: 3 Function value was not returned", "red"))
assert(retVal[4] == 4, Questie:Colorize(" -- FAILED: 4 Function value was not returned", "red"))
assert(retVal[5] == 5, Questie:Colorize(" -- FAILED: 5 Function value was not returned", "red"))
end
--- Test async and async return
do
local MessageHandler = MessageHandlerFactory:New()
local returnedCount = 0
local incrementReturnFunction = function()
returnedCount = returnedCount + 1
return returnedCount
end
-- Register mutliple events and fire
for _ = 1, 5 do
MessageHandler:RegisterRepeating(testEvent, incrementReturnFunction)
end
local routine = coroutine.create(
function()
MessageHandler:FireAsync(testEvent, 2)
assert(returnedCount == 5, Questie:Colorize(" -- FAILED: Event was not fired the correct amount of times", "red"))
end
)
local timer
timer = C_Timer.NewTicker(0, function()
local success, retVal = coroutine.resume(routine)
if retVal then
assert(retVal[1] == 1, Questie:Colorize(" -- FAILED: 1 Function value was not returned", "red"))
assert(retVal[2] == 2, Questie:Colorize(" -- FAILED: 2 Function value was not returned", "red"))
assert(retVal[3] == 3, Questie:Colorize(" -- FAILED: 3 Function value was not returned", "red"))
assert(retVal[4] == 4, Questie:Colorize(" -- FAILED: 4 Function value was not returned", "red"))
assert(retVal[5] == 5, Questie:Colorize(" -- FAILED: 5 Function value was not returned", "red"))
end
assert(success, Questie:Colorize(" -- FAILED: Coroutine failed", "red"), retVal)
-- Kill the timer when the coroutine is dead.
if (coroutine.status(routine) == "dead") then
Questie:Debug(Questie.DEBUG_CRITICAL, "- MessageHandlerFactory - |cFF00FF00SUCCESS!|r")
timer:Cancel()
end
end)
end
end
-- Run it after all files has been loaded
C_Timer.After(2, RunMessageHandlerTests)
end
-----------------
+1 -1
View File
@@ -135,7 +135,7 @@ function QuestiePlugin:InjectZoneTables(customZoneTables)
while uiMapId do
if uiMapId and areaId then
ZoneDB.private.uiMapIdToAreaId[uiMapId] = areaId
ZoneDB.private.areaIdToUiMapId[uiMapId] = uiMapId
ZoneDB.private.areaIdToUiMapId[areaId] = uiMapId
if type(ZoneDB.private.dungeons) == "table" and ZoneDB.private.dungeons[areaId] then
ZoneDB.private.areaIdToUiMapId[areaId] = uiMapId
+96 -201
View File
@@ -30,52 +30,41 @@ local C_Map = QuestieCompat.C_Map
QuestieMap.ICON_MAP_TYPE = "MAP";
QuestieMap.ICON_MINIMAP_TYPE = "MINIMAP";
--Useful links.
-- https://github.com/tomrus88/BlizzardInterfaceCode/blob/master/Interface/SharedXML/Pools.lua
-- https://www.townlong-yak.com/framexml/27101/Blizzard_MapCanvas/Blizzard_MapCanvas.lua
-- https://www.townlong-yak.com/framexml/27101/Blizzard_MapCanvas/MapCanvas_DataProviderBase.lua
-- https://www.townlong-yak.com/framexml/27101/Blizzard_MapCanvas/MapCanvas_PinFrameLevelsManager.lua
-- List of frames sorted by quest ID (automatic notes)
-- E.g. {[questId] = {[frameName] = frame, ...}, ...}
-- For details about frame.data see calls to QuestieMap.DrawWorldIcon
QuestieMap.questIdFrames = {}
-- List of frames sorted by NPC/object ID (manual notes)
-- id > 0: NPC
-- id < 0: object
-- E.g. {[-objectId] = {[frameName] = frame, ...}, ...}
-- For details about frame.data see QuestieMap.ShowNPC and QuestieMap.ShowObject
QuestieMap.manualFrames = {}
--Used in my fadelogic.
local fadeOverDistance = 10;
local normalizedValue = 1 / fadeOverDistance; --Opacity / Distance to fade over
local normalizedValue = 1 / fadeOverDistance;
local HBD = QuestieCompat.HBD or LibStub("HereBeDragonsQuestie-2.0")
local HBDPins = QuestieCompat.HBDPins or LibStub("HereBeDragonsQuestie-Pins-2.0")
--We should really try and squeeze out all the performance we can, especially in this.
local tostring = tostring;
local tinsert = table.insert;
local pairs = pairs;
local ipairs = ipairs;
local tremove = table.remove;
local tunpack = unpack;
local drawTimer
local fadeLogicTimerShown
local fadeLogicCoroutine
local isDrawQueueDisabled = false
--* TODO: How the frames are handled needs to be reworked, why are we getting them from _G
--Get the frames for a quest, this returns all of the frames
function QuestieMap:GetFramesForQuest(questId)
local frames = {}
--If no frames exists or if the quest does not exist we just return an empty list
if QuestieMap.questIdFrames[questId] then
for _, name in next, QuestieMap.questIdFrames[questId] do
for _, name in pairs(QuestieMap.questIdFrames[questId]) do
if _G[name] then
frames[name] = _G[name]
end
@@ -87,14 +76,12 @@ end
function QuestieMap:UnloadQuestFrames(questId, iconType)
if QuestieMap.questIdFrames[questId] then
if not iconType then
local frameList = QuestieMap:GetFramesForQuest(questId)
for _, frame in next, frameList do
for _, frame in pairs(QuestieMap:GetFramesForQuest(questId)) do
frame:Unload();
end
QuestieMap.questIdFrames[questId] = nil;
else
local frameList = QuestieMap:GetFramesForQuest(questId)
for name, frame in next, frameList do
for name, frame in pairs(QuestieMap:GetFramesForQuest(questId)) do
if frame and frame.data and frame.data.Icon == iconType then
frame:Unload();
QuestieMap.questIdFrames[questId][name] = nil
@@ -111,9 +98,8 @@ end
function QuestieMap:GetManualFrames(id, typ)
typ = typ or "any"
local frames = {}
--If no frames exists or if the quest does not exist we just return an empty list
if QuestieMap.manualFrames[typ] and (QuestieMap.manualFrames[typ][id]) then
for _, name in next, QuestieMap.manualFrames[typ][id] do
for _, name in pairs(QuestieMap.manualFrames[typ][id]) do
tinsert(frames, _G[name])
end
end
@@ -124,8 +110,7 @@ end
function QuestieMap:UnloadManualFrames(id, typ)
typ = typ or "any"
if QuestieMap.manualFrames[typ] and (QuestieMap.manualFrames[typ][id]) then
local frameList = QuestieMap:GetManualFrames(id, typ)
for _, frame in next, frameList do
for _, frame in ipairs(QuestieMap:GetManualFrames(id, typ)) do
frame:Unload();
end
QuestieMap.manualFrames[typ][id] = nil;
@@ -134,22 +119,24 @@ end
function QuestieMap:ResetManualFrames(typ)
typ = typ or "any"
for id in next, QuestieMap.manualFrames[typ] do
if QuestieMap.manualFrames[typ] then
for id in pairs(QuestieMap.manualFrames[typ]) do
QuestieMap:UnloadManualFrames(id, typ)
end
end
end
-- Rescale all the icons
function QuestieMap:RescaleIcons()
local mapScale = QuestieMap.GetScaleValue()
for _, framelist in next, QuestieMap.questIdFrames do
for _, frameName in next, framelist do
for _, framelist in pairs(QuestieMap.questIdFrames) do
for _, frameName in pairs(framelist) do
QuestieMap.utils:RescaleIcon(frameName, mapScale)
end
end
for _, frameTypeList in next, QuestieMap.manualFrames do
for _, framelist in next, frameTypeList do
for _, frameName in next, framelist do
for _, frameTypeList in pairs(QuestieMap.manualFrames) do
for _, framelist in pairs(frameTypeList) do
for _, frameName in ipairs(framelist) do
QuestieMap.utils:RescaleIcon(frameName, mapScale)
end
end
@@ -162,15 +149,15 @@ local minimapDrawQueue = {};
QuestieMap._mapDrawQueue = mapDrawQueue
QuestieMap._minimapDrawQueue = minimapDrawQueue
function QuestieMap:InitializeQueue() -- now called on every loading screen
--- Called at startup (Stage 3) and on PLAYER_ENTERING_WORLD to reset the draw queue.
function QuestieMap:InitializeQueue()
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieMap] Starting draw queue timer!")
local isInInstance, instanceType = IsInInstance()
if (not isInInstance) or instanceType ~= "raid" then -- only run map updates when not in a raid
if (not isInInstance) or instanceType ~= "raid" then
isDrawQueueDisabled = false
if not drawTimer then
drawTimer = C_Timer.NewTicker(0.2, QuestieMap.ProcessQueue)
-- ! Remember to update the distance variable in ProcessShownMinimapIcons if you change the timer
fadeLogicTimerShown = C_Timer.NewTicker(0.1, function()
if fadeLogicCoroutine and coroutine.status(fadeLogicCoroutine) == "suspended" then
local success, errorMsg = coroutine.resume(fadeLogicCoroutine)
@@ -185,7 +172,7 @@ function QuestieMap:InitializeQueue() -- now called on every loading screen
fadeLogicCoroutine = coroutine.create(QuestieMap.ProcessShownMinimapIcons)
end
else
if drawTimer then -- cancel existing timer while in dungeon/raid
if drawTimer then
drawTimer:Cancel()
drawTimer = nil
fadeLogicTimerShown:Cancel()
@@ -197,69 +184,52 @@ end
---@return number @A scale value that is based of the map currently open, smaller icons for World and Continent
function QuestieMap.GetScaleValue()
if not HBDPins or not HBDPins.worldmapProvider then return 1 end
local mapId = HBDPins.worldmapProvider:GetMap():GetMapID();
local scaling = 1;
if C_Map and C_Map.GetAreaInfo then
local mapInfo = C_Map.GetMapInfo(mapId)
if (mapInfo.mapType == 0) then --? Cosmic, This is probably not needed but for the sake of completion...
if mapInfo then
if (mapInfo.mapType == 0) then
scaling = 0.85
elseif (mapInfo.mapType == 1) then -- World
elseif (mapInfo.mapType == 1) then
scaling = 0.85
elseif (mapInfo.mapType == 2) then -- Continent
elseif (mapInfo.mapType == 2) then
scaling = 0.9
end
end
end
return scaling
end
function QuestieMap:ProcessShownMinimapIcons()
--Upvalue the most used functions in here
local getTime, cYield, getWorldPos = GetTime, coroutine.yield, HBD.GetPlayerWorldPosition
--Max icons per tick
local maxCount = 50
--Local variables defined here instead of in loop
--saves time because it doesn't need to remake the variables
local doEdgeUpdate = true
local playerX, playerY
local count
local lastUpdate = getTime()
local xd, yd
local totalDistance = 0
--This coroutine never dies, we want it to keep looping forever
--yield stops it from being "infinite" and crashing the game
while true do
count = 0
playerX, playerY = getWorldPos()
--Calculate squared distance
-- No need for absolute values as these are used only as squared
xd = (playerX or 0) - (QuestieMap.playerX or 0)
yd = (playerY or 0) - (QuestieMap.playerY or 0)
--Instead of math.sqrt we just used the square distance for speed
totalDistance = totalDistance + (xd * xd + yd * yd)
--These variables are used inside the fadelogic
QuestieMap.playerX = playerX
QuestieMap.playerY = playerY
-- Only update icons on the edge every 1 seconds
-- totalDistance is used because sometimes we move so fast that we need to update it more often.
-- ! Remember to update the distance variable if you change the timer
if totalDistance > 3 or getTime() - lastUpdate >= 1 then
doEdgeUpdate = true
lastUpdate = getTime()
--print("Dist:", totalDistance)
totalDistance = 0
end
---@param minimapFrame IconFrame
for minimapFrame, data in next, HBDPins.activeMinimapPins do
if HBDPins and HBDPins.activeMinimapPins then
for minimapFrame, data in pairs(HBDPins.activeMinimapPins) do
if minimapFrame.miniMapIcon and ((data.distanceFromMinimapCenter < 1.1) or doEdgeUpdate) then
if minimapFrame.FadeLogic then
minimapFrame:FadeLogic()
@@ -269,13 +239,9 @@ function QuestieMap:ProcessShownMinimapIcons()
end
end
--Never run more than maxCount in a single run
if count > maxCount then
cYield()
if (not HBDPins.activeMinimapPins[minimapFrame]) then
-- table has been edited during traversal at critical key. we can't continue iterating over it. stop iteration and start again.
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieMap:ProcessShownMinimapIcons] FadeLogic loop coroutine: HBDPins.activeMinimapPins doesn't have the key anymore.")
-- force reupdate imeadiately
if not HBDPins.activeMinimapPins[minimapFrame] then
totalDistance = 9000
break
end
@@ -284,13 +250,14 @@ function QuestieMap:ProcessShownMinimapIcons()
count = count + 1
end
end
end
cYield()
doEdgeUpdate = false
end
end
function QuestieMap:QueueDraw(drawType, ...)
if (not isDrawQueueDisabled) then -- dont queue when in raid
if (not isDrawQueueDisabled) then
if (drawType == QuestieMap.ICON_MAP_TYPE) then
tinsert(mapDrawQueue, { ... });
elseif (drawType == QuestieMap.ICON_MINIMAP_TYPE) then
@@ -301,21 +268,18 @@ end
function QuestieMap.ProcessQueue()
if (not next(mapDrawQueue) and (not next(minimapDrawQueue))) then
-- Nothing to process
return
end
local scaleValue = QuestieMap.GetScaleValue()
for _ = 1, math.min(24, math.max(table.getn(mapDrawQueue), table.getn(minimapDrawQueue))) do
for _ = 1, math.min(24, math.max(#mapDrawQueue, #minimapDrawQueue)) do
local mapDrawCall = tremove(mapDrawQueue, 1);
if mapDrawCall then
local frame = mapDrawCall[2];
HBDPins:AddWorldMapIconMap(tunpack(mapDrawCall));
--? If you ever chanage this logic, make sure you change the logic in QuestieMap.utils:RescaleIcon function too!
local size = (16 * (frame.data.IconScale or 1) * (Questie.db.profile.globalScale or 0.7)) * scaleValue;
frame:SetSize(size, size)
QuestieMap.utils:SetDrawOrder(frame);
end
@@ -323,40 +287,38 @@ function QuestieMap.ProcessQueue()
if minimapDrawCall then
local frame = minimapDrawCall[2];
HBDPins:AddMinimapIconMap(tunpack(minimapDrawCall));
QuestieMap.utils:SetDrawOrder(frame);
end
if mapDrawCall then
mapDrawCall[2]._loaded = true
if mapDrawCall[2]._needsUnload then
mapDrawCall[2]:Unload()
end
end
if minimapDrawCall then
minimapDrawCall[2]._loaded = true
if minimapDrawCall[2]._needsUnload then
minimapDrawCall[2]:Unload()
end
end
end
end
-- Show NPC on map
-- This function does the same for manualFrames as similar functions in
-- QuestieQuest do for questIdFrames
---@param npcID number @The ID of the NPC
function QuestieMap:ShowNPC(npcID, icon, scale, title, body, disableShiftToRemove, typ, excludeDungeon)
if type(npcID) ~= "number" then
Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestieMap:ShowNPC] Got <" .. type(npcID) .. "> instead of <number>")
return
end
-- get the NPC data
local npc = QuestieDB:GetNPC(npcID)
if (not npc) or (not npc.spawns) then return end
-- create the icon data
local data = {}
data.id = npc.id
data.Icon = icon or "Interface\\WorldMap\\WorldMapPartyIcon"
data.GetIconScale = function() return scale or Questie.db.profile.manualScale or 0.7 end
data.IconScale = data:GetIconScale()
data.Type = "manual"
@@ -366,7 +328,7 @@ function QuestieMap:ShowNPC(npcID, icon, scale, title, body, disableShiftToRemov
data.IsObjectiveNote = false
data.ManualTooltipData = {}
local baseTitle = title or (npc.name .. " (" .. l10n("NPC") .. ")")
data.ManualTooltipData.Title = WeaponMasterSkills.AppendSkillsToTitle(baseTitle, data.id)
data.ManualTooltipData.Title = WeaponMasterSkills and WeaponMasterSkills.AppendSkillsToTitle(baseTitle, data.id) or baseTitle
local level = tostring(npc.minLevel)
local health = tostring(npc.minLevelHealth)
if npc.minLevel ~= npc.maxLevel then
@@ -381,32 +343,22 @@ function QuestieMap:ShowNPC(npcID, icon, scale, title, body, disableShiftToRemov
data.ManualTooltipData.disableShiftToRemove = disableShiftToRemove
local manualIcons = {}
-- draw the notes
for zone, spawns in next, npc.spawns do
for zone, spawns in pairs(npc.spawns) do
if (zone ~= nil and spawns ~= nil) and ((not excludeDungeon) or (not ZoneDB.IsDungeonZone(zone))) then
local spIndex = 1
while spawns[spIndex] do
local coords = spawns[spIndex]
-- instance spawn, draw entrance on map
for _, coords in ipairs(spawns) do
local dungeonLocation = ZoneDB:GetDungeonLocation(zone)
if dungeonLocation ~= nil then
local dlIndex = 1
while dungeonLocation[dlIndex] do
local value = dungeonLocation[dlIndex]
for _, value in ipairs(dungeonLocation) do
QuestieMap:DrawManualIcon(data, value[1], value[2], value[3], typ)
dlIndex = dlIndex + 1
end
-- world spawn
else
manualIcons[zone] = QuestieMap:DrawManualIcon(data, zone, coords[1], coords[2], typ)
end
spIndex = spIndex + 1
end
end
end
-- draw waypoints
if npc.waypoints then
for zone, waypoints in next, npc.waypoints do
for zone, waypoints in pairs(npc.waypoints) do
if not ZoneDB:GetDungeonLocation(zone) and waypoints[1] and waypoints[1][1] and waypoints[1][1][1] then
if not manualIcons[zone] then
manualIcons[zone] = QuestieMap:DrawManualIcon(data, zone, waypoints[1][1][1], waypoints[1][1][2])
@@ -418,18 +370,13 @@ function QuestieMap:ShowNPC(npcID, icon, scale, title, body, disableShiftToRemov
end
-- Show object on map
-- This function does the same for manualFrames as similar functions in
-- QuestieQuest do for questIdFrames
---@param objectID number
function QuestieMap:ShowObject(objectID, icon, scale, title, body, disableShiftToRemove, typ)
if type(objectID) ~= "number" then return end
-- get the gameobject data
local object = QuestieDB:GetObject(objectID)
if not object or not object.spawns then return end
-- create the icon data
local data = {}
-- hack: clean up this code, we shouldnt be using negative indexes
if typ then
data.id = object.id
else
@@ -450,26 +397,17 @@ function QuestieMap:ShowObject(objectID, icon, scale, title, body, disableShiftT
}
data.ManualTooltipData.disableShiftToRemove = disableShiftToRemove
-- draw the notes
for zone, spawns in next, object.spawns do
for zone, spawns in pairs(object.spawns) do
if (zone ~= nil and spawns ~= nil) then
local spIndex = 1
while spawns[spIndex] do
local coords = spawns[spIndex]
-- instance spawn, draw entrance on map
for _, coords in ipairs(spawns) do
local dungeonLocation = ZoneDB:GetDungeonLocation(zone)
if dungeonLocation ~= nil then
local dlIndex = 1
while dungeonLocation[dlIndex] do
local value = dungeonLocation[dlIndex]
for _, value in ipairs(dungeonLocation) do
QuestieMap:DrawManualIcon(data, value[1], value[2], value[3], typ)
dlIndex = dlIndex + 1
end
-- world spawn
else
QuestieMap:DrawManualIcon(data, zone, coords[1], coords[2], typ)
end
spIndex = spIndex + 1
end
end
end
@@ -477,32 +415,34 @@ end
function QuestieMap:DrawLineIcon(lineFrame, areaID, x, y)
if type(areaID) ~= "number" or type(x) ~= "number" or type(y) ~= "number" then
error("Questie" .. ": AddWorldMapIconMap: 'AreaID', 'x' and 'y' must be numbers " .. areaID .. " " .. x .. " " .. y)
Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestieMap:DrawLineIcon] 'AreaID', 'x' and 'y' must be numbers:", areaID, x, y)
return nil, nil
end
local uiMapId = ZoneDB:GetUiMapIdByAreaId(areaID)
if not uiMapId then
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieMap:DrawLineIcon] No UiMapID for areaId:", areaID)
return nil, nil
end
HBDPins:AddWorldMapIconMap(Questie, lineFrame, uiMapId, x, y, HBD_PINS_WORLDMAP_SHOW_CURRENT)
end
-- Draw manually added NPC/object notes
-- TODO: item and custom notes
--@param data table<...> @A table created by the calling function, must contain `id`, `Name`, `GetIconScale()`, and `Type`
--@param AreaID number @The zone ID from the raw data
--@param x float @The X coordinate in 0-100 format
--@param y float @The Y coordinate in 0-100 format
function QuestieMap:DrawManualIcon(data, areaID, x, y, typ)
if type(data) ~= "table" then
error("Questie" .. ": AddWorldMapIconMap: must have some data")
Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestieMap:DrawManualIcon] must have some data")
return nil, nil
end
if type(areaID) ~= "number" or type(x) ~= "number" or type(y) ~= "number" then
error("Questie" .. ": AddWorldMapIconMap: 'AreaID', 'x' and 'y' must be numbers " .. areaID .. " " .. x .. " " .. y)
Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestieMap:DrawManualIcon] 'AreaID', 'x' and 'y' must be numbers:", areaID, x, y)
return nil, nil
end
if type(data.id) ~= "number" or type(data.id) ~= "number" then
error("Questie" .. "Data.id must be set to the NPC or object ID!")
if type(data.id) ~= "number" then
Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestieMap:DrawManualIcon] Data.id must be set to the NPC or object ID!")
return nil, nil
end
-- this needs to be refactored. Fix the capitalization. Who made this id instead of Id?
data.Id = data.id
local uiMapId = ZoneDB:GetUiMapIdByAreaId(areaID)
@@ -510,10 +450,8 @@ function QuestieMap:DrawManualIcon(data, areaID, x, y, typ)
Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestieMap:DrawManualIcon] No UiMapID for areaId:", areaID, tostring(data.Name))
return nil, nil
end
-- set the icon
local texture = data.Icon or "Interface\\WorldMap\\WorldMapPartyIcon"
-- Save new zone ID format, used in QuestieFramePool
-- create a list for all frames belonging to a NPC (id > 0) or an object (id < 0)
typ = typ or "any"
if not QuestieMap.manualFrames[typ] then
QuestieMap.manualFrames[typ] = {}
@@ -522,23 +460,20 @@ function QuestieMap:DrawManualIcon(data, areaID, x, y, typ)
QuestieMap.manualFrames[typ][data.id] = {}
end
-- create the map icon
local icon = QuestieFramePool:GetFrame()
icon.data = data
icon.x = x
icon.y = y
icon.AreaID = areaID -- used by QuestieFramePool
icon.AreaID = areaID
icon.UiMapID = uiMapId
icon.miniMapIcon = false;
icon.texture:SetTexture(texture)
icon:SetWidth(16 * (data:GetIconScale() or 0.7))
icon:SetHeight(16 * (data:GetIconScale() or 0.7))
-- add the map icon
QuestieMap:QueueDraw(QuestieMap.ICON_MAP_TYPE, Questie, icon, icon.UiMapID, x / 100, y / 100, 3) -- showFlag)
QuestieMap:QueueDraw(QuestieMap.ICON_MAP_TYPE, Questie, icon, icon.UiMapID, x / 100, y / 100, 3)
tinsert(QuestieMap.manualFrames[typ][data.id], icon:GetName())
-- create the minimap icon
local iconMinimap = QuestieFramePool:GetFrame()
local colorsMinimap = { 1, 1, 1 }
if data.IconColor ~= nil and Questie.db.profile.questMinimapObjectiveColors then
@@ -549,18 +484,16 @@ function QuestieMap:DrawManualIcon(data, areaID, x, y, typ)
iconMinimap.data = data
iconMinimap.x = x
iconMinimap.y = y
iconMinimap.AreaID = areaID -- used by QuestieFramePool
iconMinimap.AreaID = areaID
iconMinimap.UiMapID = uiMapId
iconMinimap.texture:SetTexture(texture)
iconMinimap.texture:SetVertexColor(colorsMinimap[1], colorsMinimap[2], colorsMinimap[3], 1);
iconMinimap.miniMapIcon = true;
-- add the minimap icon
QuestieMap:QueueDraw(QuestieMap.ICON_MINIMAP_TYPE, Questie, iconMinimap, iconMinimap.UiMapID, x / 100, y / 100, true, true);
tinsert(QuestieMap.manualFrames[typ][data.id], iconMinimap:GetName())
-- make sure notes are only shown when they are supposed to
if (not Questie.db.profile.enabled) then -- TODO: or (not Questie.db.profile.manualNotes)
if (not Questie.db.profile.enabled) then
icon:FakeHide()
iconMinimap:FakeHide()
else
@@ -572,31 +505,26 @@ function QuestieMap:DrawManualIcon(data, areaID, x, y, typ)
end
end
if QuestieMap.utils and QuestieMap.utils.RescaleIcon then
QuestieMap.utils:RescaleIcon(icon)
end
-- return the frames in case they need to be stored seperately from QuestieMap.manualFrames
return icon, iconMinimap;
end
--A layer to keep the area convertion away from the other parts of the code
--A layer to keep the area conversion away from the other parts of the code
--coordinates need to be 0-1 instead of 0-100
--showFlag isn't required but may want to be Modified
---@return IconFrame, IconFrame
function QuestieMap:DrawWorldIcon(data, areaID, x, y, showFlag)
if type(data) ~= "table" then
error("Questie" .. ": AddWorldMapIconMap: must have some data")
Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestieMap:DrawWorldIcon] must have some data")
return nil, nil
end
--if type(areaID) ~= "number" or type(x) ~= "number" or type(y) ~= "number" then
-- error("Questie"..": AddWorldMapIconMap: 'AreaID', 'x' and 'y' must be numbers "..areaID.." "..x.." "..y.." "..tostring(showFlag))
--end
--if type(data.Id) ~= "number" or type(data.Id) ~= "number"then
-- error("Questie".."Data.Id must be set to the quests ID!")
--end
local uiMapId = ZoneDB:GetUiMapIdByAreaId(areaID)
if (not uiMapId) then
local parentMapId
local mapInfo = C_Map.GetMapInfo(areaID)
local mapInfo = C_Map and C_Map.GetMapInfo and C_Map.GetMapInfo(areaID)
if mapInfo then
parentMapId = mapInfo.parentMapID
else
@@ -604,14 +532,11 @@ function QuestieMap:DrawWorldIcon(data, areaID, x, y, showFlag)
end
if (not parentMapId) then
error("No UiMapID or fitting parentAreaId for areaId : " .. areaID .. " - " .. tostring(data.Name))
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieMap:DrawWorldIcon] No UiMapID or fitting parentAreaId for areaId:", areaID, tostring(data.Name))
return nil, nil
else
areaID = parentMapId
uiMapId = ZoneDB:GetUiMapIdByAreaId(areaID)
if not uiMapId then
return nil, nil
end
end
end
@@ -619,10 +544,8 @@ function QuestieMap:DrawWorldIcon(data, areaID, x, y, showFlag)
showFlag = HBD_PINS_WORLDMAP_SHOW_WORLD
end
--print("UIMAPID: " .. tostring(uiMapId))
if not uiMapId then
--ZoneDB:GetUiMapIdByAreaId
error("No UiMapID or fitting uiMapId for areaId : " .. areaID .. " - " .. tostring(data.Name))
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieMap:DrawWorldIcon] No UiMapID or fitting uiMapId for areaId:", areaID, tostring(data.Name))
return nil, nil
end
@@ -645,8 +568,6 @@ function QuestieMap:DrawWorldIcon(data, areaID, x, y, showFlag)
iconMinimap.y = y
iconMinimap.AreaID = areaID
iconMinimap.UiMapID = uiMapId
--data.refMiniMap = iconMinimap -- used for removing
--Are we a minimap note?
iconMinimap.miniMapIcon = true;
iconMinimap:UpdateTexture(Questie.usedIcons[data.Icon]);
@@ -676,7 +597,6 @@ function QuestieMap:DrawWorldIcon(data, areaID, x, y, showFlag)
y = self.worldY
end
if (x and y) then
--Very small value before, hard to work with.
local distance = QuestieLib:Euclid(QuestieMap.playerX, QuestieMap.playerY, x, y) / 10;
if (distance > profile.fadeLevel) then
@@ -684,7 +604,6 @@ function QuestieMap:DrawWorldIcon(data, areaID, x, y, showFlag)
self:SetFade(fade)
elseif (distance < profile.fadeOverPlayerDistance) and profile.fadeOverPlayer then
local fadeAmount = profile.fadeOverPlayerLevel + distance * (1 - profile.fadeOverPlayerLevel) / profile.fadeOverPlayerDistance
-- local fadeAmount = math.max(fadeAmount, 0.5);
if self.faded and fadeAmount > profile.iconFadeLevel then
fadeAmount = profile.iconFadeLevel
end
@@ -722,8 +641,6 @@ function QuestieMap:DrawWorldIcon(data, areaID, x, y, showFlag)
QuestieMap.questIdFrames[data.Id] = {}
end
-- tinsert(QuestieMap.questIdFrames[data.Id], iconMap:GetName())
-- tinsert(QuestieMap.questIdFrames[data.Id], iconMinimap:GetName())
QuestieMap.questIdFrames[data.Id][iconMap:GetName()] = iconMap:GetName()
QuestieMap.questIdFrames[data.Id][iconMinimap:GetName()] = iconMinimap:GetName()
@@ -744,7 +661,7 @@ local closestStarter = {}
function QuestieMap:FindClosestStarter()
local playerX, playerY, _ = HBD:GetPlayerWorldPosition();
local playerZone = HBD:GetPlayerWorldPosition();
for questId in next, QuestiePlayer.currentQuestlog do
for questId in pairs(QuestiePlayer.currentQuestlog) do
if (not closestStarter[questId]) then
local quest = QuestieDB.GetQuest(questId);
if quest then
@@ -755,24 +672,18 @@ function QuestieMap:FindClosestStarter()
zone = -1,
type = "",
}
for starterType, starters in next, quest.Starts do
for starterType, starters in pairs(quest.Starts) do
if (starterType == "GameObject") then
local stIndex = 1
while starters and starters[stIndex] do
local ObjectID = starters[stIndex]
for _, ObjectID in ipairs(starters or {}) do
local obj = QuestieDB:GetObject(ObjectID)
if (obj ~= nil and obj.spawns ~= nil) then
for Zone, Spawns in next, obj.spawns do
for Zone, Spawns in pairs(obj.spawns) do
if (Zone ~= nil and Spawns ~= nil) then
local spIndex = 1
while Spawns[spIndex] do
local coords = Spawns[spIndex]
if (coords[1] == -1 or coords[2] == -1) then -- instace locations
for _, coords in ipairs(Spawns) do
if (coords[1] == -1 or coords[2] == -1) then
local dungeonLocation = ZoneDB:GetDungeonLocation(Zone)
if dungeonLocation ~= nil then
local dlIndex = 1
while dungeonLocation[dlIndex] do
local value = dungeonLocation[dlIndex]
for _, value in ipairs(dungeonLocation) do
if (value[1] and value[2]) then
local x, y, _ = HBD:GetWorldCoordinatesFromZone(value[1] / 100, value[2] / 100, ZoneDB:GetUiMapIdByAreaId(value[3]))
if (x and y) then
@@ -786,7 +697,6 @@ function QuestieMap:FindClosestStarter()
end
end
end
dlIndex = dlIndex + 1
end
end
else
@@ -803,30 +713,22 @@ function QuestieMap:FindClosestStarter()
end
end
end
spIndex = spIndex + 1
end
end
end
end
stIndex = stIndex + 1
end
elseif (starterType == "NPC") then
local stIndex = 1
while starters and starters[stIndex] do
local NPCID = starters[stIndex]
for _, NPCID in ipairs(starters or {}) do
local NPC = QuestieDB:GetNPC(NPCID)
if (NPC ~= nil and NPC.spawns ~= nil and NPC.friendly) then
for Zone, Spawns in next, NPC.spawns do
for Zone, Spawns in pairs(NPC.spawns) do
if (Zone ~= nil and Spawns ~= nil) then
local spIndex = 1
while Spawns[spIndex] do
local coords = Spawns[spIndex]
for _, coords in ipairs(Spawns) do
if (coords[1] == -1 or coords[2] == -1) then
local dungeonLocation = ZoneDB:GetDungeonLocation(Zone)
if dungeonLocation ~= nil then
local dlIndex = 1
while dungeonLocation[dlIndex] do
local value = dungeonLocation[dlIndex]
for _, value in ipairs(dungeonLocation) do
if (value[1] and value[2]) then
local uiMapId = ZoneDB:GetUiMapIdByAreaId(value[3])
local x, y, _ = HBD:GetWorldCoordinatesFromZone(value[1] / 100, value[2] / 100, uiMapId)
@@ -841,7 +743,6 @@ function QuestieMap:FindClosestStarter()
end
end
end
dlIndex = dlIndex + 1
end
end
elseif (coords[1] and coords[2]) then
@@ -858,12 +759,10 @@ function QuestieMap:FindClosestStarter()
end
end
end
spIndex = spIndex + 1
end
end
end
end
stIndex = stIndex + 1
end
end
end
@@ -887,17 +786,16 @@ function QuestieMap:GetNearestSpawn(objective)
local playerX, playerY, playerI = HBD:GetPlayerWorldPosition()
local bestDistance = 999999999
local bestSpawn, bestSpawnZone, bestSpawnId, bestSpawnType, bestSpawnName
-- TODO: This is just a temporary workaround - We have to find out why "objective.spawnList" can be nil
if objective and objective.spawnList and next(objective.spawnList) then
for id, spawnData in next, objective.spawnList do
for zone, spawns in next, spawnData.Spawns do
for _, spawn in next, spawns do
for id, spawnData in pairs(objective.spawnList) do
for zone, spawns in pairs(spawnData.Spawns) do
for _, spawn in pairs(spawns) do
local uiMapId = ZoneDB:GetUiMapIdByAreaId(zone)
local dX, dY, dInstance = HBD:GetWorldCoordinatesFromZone(spawn[1] / 100.0, spawn[2] / 100.0, uiMapId)
local dist = HBD:GetWorldDistance(dInstance, playerX, playerY, dX, dY)
if dist then
if dInstance ~= playerI then
dist = 500000 + dist * 100 -- hack
dist = 500000 + dist * 100
end
if dist < bestDistance then
bestDistance = dist
@@ -925,25 +823,23 @@ function QuestieMap:GetNearestQuestSpawn(quest)
local finisherName
if quest.Finisher ~= nil then
if quest.Finisher.Type == "monster" then
--finisher = QuestieDB:GetNPC(quest.Finisher.Id)
finisherSpawns, finisherName = QuestieDB.QueryNPCSingle(quest.Finisher.Id, "spawns"), QuestieDB.QueryNPCSingle(quest.Finisher.Id, "name")
elseif quest.Finisher.Type == "object" then
--finisher = QuestieDB:GetObject(quest.Finisher.Id)
finisherSpawns, finisherName = QuestieDB.QueryObjectSingle(quest.Finisher.Id, "spawns"), QuestieDB.QueryObjectSingle(quest.Finisher.Id, "name")
end
end
if finisherSpawns then -- redundant code
if finisherSpawns then
local bestDistance = 999999999
local playerX, playerY, playerI = HBD:GetPlayerWorldPosition()
local bestSpawn, bestSpawnZone, bestSpawnType, bestSpawnName
for zone, spawns in next, finisherSpawns do
for _, spawn in next, spawns do
for zone, spawns in pairs(finisherSpawns) do
for _, spawn in pairs(spawns) do
local uiMapId = ZoneDB:GetUiMapIdByAreaId(zone)
local dX, dY, dInstance = HBD:GetWorldCoordinatesFromZone(spawn[1] / 100.0, spawn[2] / 100.0, uiMapId)
local dist = HBD:GetWorldDistance(dInstance, playerX, playerY, dX, dY)
if dist then
if dInstance ~= playerI then
dist = 500000 + dist * 100 -- hack
dist = 500000 + dist * 100
end
if dist < bestDistance then
bestDistance = dist
@@ -963,7 +859,7 @@ function QuestieMap:GetNearestQuestSpawn(quest)
local bestDistance = 999999999
local bestSpawn, bestSpawnZone, bestSpawnId, bestSpawnType, bestSpawnName
for _, objective in next, quest.Objectives do
for _, objective in pairs(quest.Objectives) do
local spawn, zone, Name, id, Type, dist = QuestieMap:GetNearestSpawn(objective)
if spawn and dist < bestDistance and ((not objective.Needed) or objective.Needed ~= objective.Collected) then
bestDistance = dist
@@ -975,7 +871,7 @@ function QuestieMap:GetNearestQuestSpawn(quest)
end
end
for _, objective in next, quest.SpecialObjectives do
for _, objective in pairs(quest.SpecialObjectives) do
local spawn, zone, Name, id, Type, dist = QuestieMap:GetNearestSpawn(objective)
if spawn and dist < bestDistance and ((not objective.Needed) or objective.Needed ~= objective.Collected) then
bestDistance = dist
@@ -989,7 +885,7 @@ function QuestieMap:GetNearestQuestSpawn(quest)
return bestSpawn, bestSpawnZone, bestSpawnName, bestSpawnId, bestSpawnType, bestDistance
end
QuestieMap.zoneWaypointColorOverrides = { -- this is used when the default orange color doesn't work well in specific zones. Not needed after 769ea832ff772b10c57351eb199348393625a99b
QuestieMap.zoneWaypointColorOverrides = {
-- [14] = {0,0.1,0.9,0.7}, -- durotar
-- [38] = {0,0.1,0.9,0.7} -- loch modan
}
@@ -1000,13 +896,12 @@ QuestieMap.zoneWaypointHoverColorOverrides = {
}
function QuestieMap:DrawWaypoints(icon, waypoints, zone, color)
if waypoints and waypoints[1] and waypoints[1][1] and waypoints[1][1][1] then -- check that waypoint data actually exists
if waypoints and waypoints[1] and waypoints[1][1] and waypoints[1][1][1] then
local lineFrames = QuestieFramePool:CreateWaypoints(icon, waypoints, nil, color or QuestieMap.zoneWaypointColorOverrides[zone], zone)
local lIndex = 1
while lineFrames[lIndex] do
local lineFrame = lineFrames[lIndex]
for _, lineFrame in ipairs(lineFrames) do
QuestieMap:DrawLineIcon(lineFrame, zone, waypoints[1][1][1], waypoints[1][1][2])
lIndex = lIndex + 1
end
end
end
return QuestieMap
+48 -32
View File
@@ -2,6 +2,8 @@
local QuestieInit = QuestieLoader:CreateModule("QuestieInit")
local _QuestieInit = QuestieInit.private
local compilationDeferred = false
local needsCompilation = false
---@type ThreadLib
local ThreadLib = QuestieLoader:ImportModule("ThreadLib")
@@ -58,6 +60,9 @@ local QuestieValidateGameCache = QuestieLoader:ImportModule("QuestieValidateGame
local MinimapIcon = QuestieLoader:ImportModule("MinimapIcon")
---@type QuestieComms
local QuestieComms = QuestieLoader:ImportModule("QuestieComms");
local WOW_PROJECT_ID = QuestieCompat.WOW_PROJECT_ID
local WOW_PROJECT_CLASSIC = QuestieCompat.WOW_PROJECT_CLASSIC
---@type QuestieCompat
local QuestieCompat = QuestieLoader:ImportModule("QuestieCompat")
---@type QuestieOptions
@@ -196,7 +201,6 @@ end
-- ********************************************************************************
-- Start of QuestieInit.Stages ******************************************************
-- stage worker functions. Most are coroutines.
QuestieInit.Stages = {}
@@ -268,12 +272,19 @@ QuestieInit.Stages[1] = function() -- run as a coroutine
end
-- Check if the DB needs to be recompiled
do
local addonV = QuestieLib:GetAddonVersionString()
local uiLoc = l10n:GetUILocale()
local storedExp = Questie.db.global.dbCompiledExpansion
end
if (not dbIsCompiled) or (QuestieLib:GetAddonVersionString() ~= dbCompiledOnVersion) or (l10n:GetUILocale() ~= dbCompiledLang) or (Questie.db.global.dbCompiledExpansion ~= WOW_PROJECT_ID) then
local dbCompiledOnVersion = Questie.db.global.dbCompiledOnVersion
local dbCompiledLang = Questie.db.global.dbCompiledLang
local dbIsCompiled = Questie.db.global.dbIsCompiled
needsCompilation = (not dbIsCompiled) or (QuestieLib:GetAddonVersionString() ~= dbCompiledOnVersion) or (l10n:GetUILocale() ~= dbCompiledLang) or (Questie.db.global.dbCompiledExpansion ~= WOW_PROJECT_ID)
-- Custom servers or presence of DB plugins: always defer to Stage3 to wait for plugin data injection
if Questie.IsAscension or Questie.IsEbonhold or Questie.IsValanior or Questie.IsTurtle or QuestieServer:IsAnyDBPluginEnabled() then
compilationDeferred = true
l10n:Initialize()
coYield()
QuestieCorrections:MinimalInit() -- Needed for Stage2 which runs before Stage3
elseif needsCompilation then
print("|cFFAAEEFF" ..
l10n("Questie DB has updated!") ..
"|r|cFFFF6F22 " .. l10n("Data is being processed, this may take a few moments and cause some lag..."))
@@ -287,7 +298,6 @@ QuestieInit.Stages[1] = function() -- run as a coroutine
l10n:Initialize()
coYield()
QuestieCorrections:MinimalInit()
-- DB is cached — LoadBaseDB never runs, so the plugin loader will handle stats ingestion.
end
local dbCompiledCount = Questie.IsSoD and Questie.db.global.sod.dbCompiledCount or Questie.db.global.dbCompiledCount
@@ -361,9 +371,39 @@ end
QuestieInit.Stages[3] = function() -- run as a coroutine
Questie:Debug(Questie.DEBUG_INFO, "[QuestieInit:Stage3] Stage 3 start.")
-- Wait for registered plugins to finish loading (ensure data injection is complete)
local QuestiePluginAPI = QuestieLoader:ImportModule("QuestiePluginAPI")
local waitStart = GetTime()
Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestieInit:Stage3] Waiting for plugins to register/finish. Initial pending: " .. QuestiePluginAPI.pendingPluginsCount)
-- Give other addons/scripts a moment to fire and register if they were waiting for PLAYER_LOGIN
while (GetTime() - waitStart < 1.0) do
coYield()
end
local timeout = 10
local elapsed = GetTime() - waitStart
while QuestiePluginAPI:HasPendingPlugins() and (elapsed < timeout) do
coYield()
elapsed = GetTime() - waitStart
end
-- Always re-compile on custom servers to pick up QuestieLearner changes from SavedVariables,
-- or if compilation was explicitly deferred/needed.
local isCustomServer = Questie.IsAscension or Questie.IsEbonhold or Questie.IsValanior or Questie.IsTurtle or QuestieServer:IsAnyDBPluginEnabled()
if isCustomServer or needsCompilation or (not Questie.db.global.dbIsCompiled) then
Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestieInit:Stage3] Starting compilation (Server=" .. tostring(isCustomServer) .. ", Needed=" .. tostring(needsCompilation) .. ")")
if not QuestieDB.questData then
loadFullDatabase()
end
QuestieDBCompiler:Compile()
QuestieDB:Initialize()
end
-- register events that rely on questie being initialized
QuestieEventHandler:RegisterLateEvents()
-- ** OLD ** Questie:ContinueInit() ** START **
QuestieTooltips:Initialize()
QuestieCoords:Initialize()
@@ -447,31 +487,7 @@ QuestieInit.Stages[3] = function() -- run as a coroutine
end
end
-- Wait for registered plugins to finish loading (ensure data injection is complete)
local QuestiePluginAPI = QuestieLoader:ImportModule("QuestiePluginAPI")
local waitStart = GetTime()
-- Give other addons/scripts a moment to fire and register if they were waiting for PLAYER_LOGIN
Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestieInit:Stage3] Waiting for plugins to register/finish. Initial pending: " .. QuestiePluginAPI.pendingPluginsCount)
while (GetTime() - waitStart < 2.0) do
coYield()
end
local timeout = 10
local elapsed = GetTime() - waitStart
while QuestiePluginAPI:HasPendingPlugins() and (elapsed < timeout) do
if ((math.floor(elapsed * 10) % 10) == 0) then -- log every second
Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestieInit:Stage3] Still waiting for plugins... Pending: " .. QuestiePluginAPI.pendingPluginsCount .. " Elapsed: " .. string.format("%.1f", elapsed))
end
coYield()
elapsed = GetTime() - waitStart
end
if QuestiePluginAPI:HasPendingPlugins() then
Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestieInit:Stage3] TIMEOUT waiting for plugins! Proceeding anyway. Pending: " .. QuestiePluginAPI.pendingPluginsCount)
else
Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestieInit:Stage3] All registered plugins finished loading.")
end
-- We do this last because it will run for a while and we don't want to block the rest of the init
coYield()
+146 -22
View File
@@ -649,6 +649,8 @@ function QuestieLearner:LearnQuest(questId, data)
Questie:Debug(Questie.DEBUG_LEARNER, "[QuestieLearner] LearnQuest blocked: learner not enabled")
return
end
questId = tonumber(questId)
if not questId or questId <= 0 then return end
if not Questie.dbLearner.global.settings.learnQuests then
Questie:Debug(Questie.DEBUG_LEARNER, "[QuestieLearner] LearnQuest blocked: learnQuests=", tostring(Questie.dbLearner.global.settings.learnQuests))
return
@@ -695,6 +697,7 @@ end
function QuestieLearner:LearnQuestGiver(questId, entityId, entityType, isStart)
if not self:IsEnabled() then return end
if not Questie.dbLearner.global.settings.learnQuests then return end
questId, entityId = tonumber(questId), tonumber(entityId)
if not questId or questId <= 0 or not entityId or entityId <= 0 then return end
local existing = Questie.dbLearner.global.quests[questId]
@@ -740,9 +743,11 @@ end
-- Adds npcId as a creatureObjective for questId ([10][1] in questKeys schema).
-- If the NPC already exists in the base DB the spawn data is already there;
-- we only need the quest to reference it so tooltips/map-pins get registered.
function QuestieLearner:LearnQuestObjectiveNPC(questId, npcId, objText)
function QuestieLearner:LearnQuestObjectiveNPC(questId, npcId, objText, objectiveIndex)
if not self:IsEnabled() then return end
if not Questie.dbLearner.global.settings.learnQuests then return end
questId, npcId = tonumber(questId), tonumber(npcId)
objectiveIndex = tonumber(objectiveIndex)
if not questId or questId <= 0 or not npcId or npcId <= 0 then return end
-- 1. Persist to SavedVariables
@@ -758,6 +763,27 @@ function QuestieLearner:LearnQuestObjectiveNPC(questId, npcId, objText)
table.insert(existing[10][1], { npcId, objText or "" })
end
-- 1.1 Persist exact objective index mapping
if objectiveIndex then
existing.objIndex = existing.objIndex or {}
local entry = existing.objIndex[objectiveIndex]
if not entry then
existing.objIndex[objectiveIndex] = { type = "monster", id = npcId, text = objText or "" }
elseif entry.id ~= npcId then
-- Handle Kill Credit (multiple NPCs for one objective)
if type(entry.id) == "number" then
entry.id = { entry.id, npcId }
entry.type = "killcredit"
else
local found = false
for _, id in ipairs(entry.id) do
if id == npcId then found = true; break end
end
if not found then table.insert(entry.id, npcId) end
end
end
end
-- 2. Apply to live questDataOverrides immediately (no reload needed)
if QuestieDB and QuestieDB.questDataOverrides then
local ovr = QuestieDB.questDataOverrides[questId] or {}
@@ -771,6 +797,12 @@ function QuestieLearner:LearnQuestObjectiveNPC(questId, npcId, objText)
if not alreadyPresent then
table.insert(ovr[10][1], { npcId, objText or "" })
end
-- Update live objIndex override
if objectiveIndex then
ovr.objIndex = ovr.objIndex or {}
ovr.objIndex[objectiveIndex] = existing.objIndex[objectiveIndex]
end
end
-- 3. Re-process the quest so PopulateObjective registers tooltips & map pins
@@ -797,6 +829,7 @@ end
function QuestieLearner:LearnItem(itemId, name, itemLevel, requiredLevel, itemClass, itemSubClass)
if not self:IsEnabled() then return end
if not Questie.dbLearner.global.settings.learnItems then return end
itemId = tonumber(itemId)
if not itemId or itemId <= 0 then return end
local existing = Questie.dbLearner.global.items[itemId]
@@ -836,6 +869,7 @@ end
function QuestieLearner:LearnItemDrop(itemId, npcId)
if not self:IsEnabled() then return end
if not Questie.dbLearner.global.settings.learnItems then return end
itemId, npcId = tonumber(itemId), tonumber(npcId)
if not itemId or itemId <= 0 or not npcId or npcId <= 0 then return end
local existing = Questie.dbLearner.global.items[itemId]
@@ -875,6 +909,7 @@ end
function QuestieLearner:LearnObject(objectId, name)
if not self:IsEnabled() then return end
if not Questie.dbLearner.global.settings.learnObjects then return end
objectId = tonumber(objectId)
if not objectId or objectId <= 0 then return end
local zoneId = GetZoneId()
@@ -970,13 +1005,18 @@ function QuestieLearner:InjectLearnedData()
local npcCount, questCount, itemCount, objectCount = 0, 0, 0, 0
-- 1. NPCs
local npcIdsToFix = {}
for npcId, data in pairs(learned.npcs) do
local nid = tonumber(npcId)
if type(npcId) == "string" and nid then
npcIdsToFix[npcId] = nid
end
self:Sanitize(data)
if not QuestieDB.npcDataOverrides[npcId] then
QuestieDB.npcDataOverrides[npcId] = data
if not QuestieDB.npcDataOverrides[nid or npcId] then
QuestieDB.npcDataOverrides[nid or npcId] = data
npcCount = npcCount + 1
else
local existing = QuestieDB.npcDataOverrides[npcId]
local existing = QuestieDB.npcDataOverrides[nid or npcId]
if data[7] then
existing[7] = existing[7] or {}
for zoneId, coords in pairs(data[7]) do
@@ -994,9 +1034,18 @@ function QuestieLearner:InjectLearnedData()
end
end
end
for old, new in pairs(npcIdsToFix) do
learned.npcs[new] = learned.npcs[old]
learned.npcs[old] = nil
end
-- 2. Quests
local questIdsToFix = {}
for questId, data in pairs(learned.quests) do
local qid = tonumber(questId)
if type(questId) == "string" and qid then
questIdsToFix[questId] = qid
end
self:Sanitize(data)
-- Legacy cleanup for malformed objective data
if data[10] ~= nil then
@@ -1012,11 +1061,11 @@ function QuestieLearner:InjectLearnedData()
data[8] = nil
end
if not QuestieDB.questDataOverrides[questId] then
QuestieDB.questDataOverrides[questId] = data
if not QuestieDB.questDataOverrides[qid or questId] then
QuestieDB.questDataOverrides[qid or questId] = data
questCount = questCount + 1
else
local existing = QuestieDB.questDataOverrides[questId]
local existing = QuestieDB.questDataOverrides[qid or questId]
for k, v in pairs(data) do
if k ~= "mc" then
if k == 10 then
@@ -1041,23 +1090,41 @@ function QuestieLearner:InjectLearnedData()
end
end
end
for old, new in pairs(questIdsToFix) do
learned.quests[new] = learned.quests[old]
learned.quests[old] = nil
end
-- 3. Items
local itemIdsToFix = {}
for itemId, data in pairs(learned.items) do
if not QuestieDB.itemDataOverrides[itemId] then
QuestieDB.itemDataOverrides[itemId] = data
local iid = tonumber(itemId)
if type(itemId) == "string" and iid then
itemIdsToFix[itemId] = iid
end
if not QuestieDB.itemDataOverrides[iid or itemId] then
QuestieDB.itemDataOverrides[iid or itemId] = data
itemCount = itemCount + 1
end
end
for old, new in pairs(itemIdsToFix) do
learned.items[new] = learned.items[old]
learned.items[old] = nil
end
-- 4. Objects
local objectIdsToFix = {}
for objectId, data in pairs(learned.objects) do
local oid = tonumber(objectId)
if type(objectId) == "string" and oid then
objectIdsToFix[objectId] = oid
end
self:Sanitize(data)
if not QuestieDB.objectDataOverrides[objectId] then
QuestieDB.objectDataOverrides[objectId] = data
if not QuestieDB.objectDataOverrides[oid or objectId] then
QuestieDB.objectDataOverrides[oid or objectId] = data
objectCount = objectCount + 1
else
local existing = QuestieDB.objectDataOverrides[objectId]
local existing = QuestieDB.objectDataOverrides[oid or objectId]
if data[4] then
existing[4] = existing[4] or {}
for zoneId, coords in pairs(data[4]) do
@@ -1075,6 +1142,10 @@ function QuestieLearner:InjectLearnedData()
end
end
end
for old, new in pairs(objectIdsToFix) do
learned.objects[new] = learned.objects[old]
learned.objects[old] = nil
end
if npcCount > 0 or questCount > 0 or itemCount > 0 or objectCount > 0 then
Questie:Debug(Questie.DEBUG_INFO, "[QuestieLearner] Injected learned data:",
@@ -1377,9 +1448,33 @@ function QuestieLearner:OnQuestComplete()
end
end
-- Helper to find an NPC ID by name (case-insensitive substring match)
-- Used for proactive objective mapping when a quest is first accepted.
function QuestieLearner:GetNPCIdByName(npcName)
if not npcName or npcName == "" then return nil end
local lowerName = string.lower(npcName)
-- Check overrides first (most likely for custom servers)
if QuestieDB.npcDataOverrides then
for id, data in pairs(QuestieDB.npcDataOverrides) do
if data and data[1] and string.lower(data[1]) == lowerName then
return id
end
end
end
-- Check base NPC database
-- Note: QueryNPCSingle is a dummy until initialization, but we can fallback to raw access
local npcData = QuestieDB.npcData or {}
for id, data in pairs(npcData) do
if data and data[1] and string.lower(data[1]) == lowerName then
return id
end
end
return nil
end
-- Fires when a quest is accepted.
-- Ascension 3.3.5 passes the quest log index as the first arg; some builds pass questID directly.
-- We detect which by checking if the value could be a log index and resolving via GetQuestLogTitle.
function QuestieLearner:OnQuestAccepted(firstArg, secondArg)
Questie:Debug(Questie.DEBUG_LEARNER, "[QuestieLearner] OnQuestAccepted raw args: first=" .. tostring(firstArg) .. " second=" .. tostring(secondArg))
local questId
@@ -1429,19 +1524,47 @@ function QuestieLearner:OnQuestAccepted(firstArg, secondArg)
-- Zone: reverse-lookup from GetRealZoneText() which is always accurate on 3.3.5.
local zoneText = GetRealZoneText()
if zoneText and zoneText ~= "" then
for _, zoneTable in pairs(l10n.zoneLookup) do
for areaId, name in pairs(zoneTable) do
if name == zoneText then
local areaId = l10n:GetAreaIdByLocalName(zoneText)
if areaId and areaId > 0 then
data[17] = areaId
break
end
end
if data[17] then break end
end
end
self:LearnQuest(questId, data)
-- Proactively map objectives based on quest log text
local logIdx = 0
for i = 1, GetNumQuestLogEntries() do
local _, _, _, isHeader, _, _, _, id = QuestieCompat.GetQuestLogTitle(i)
if not isHeader and id == questId then
logIdx = i
break
end
end
if logIdx > 0 then
local numObj = GetNumQuestLeaderBoards and GetNumQuestLeaderBoards(logIdx) or 0
for j = 1, numObj do
local objText, objType, finished = GetQuestLogLeaderBoard(j, logIdx)
if objText and not finished and objType == "monster" then
-- Try to extract "Boar" from "0/10 Boar Slain"
local targetName = objText:match("^%d+/%d+%s+(.+)%s*") or objText:match("^(.+):%s*%d+/%d+")
if not targetName then
-- Fallback: strip everything that looks like a count or punctuation
targetName = objText:gsub("%d+/%d+", ""):gsub("%d+", ""):gsub("[:!?,.%(%)]", ""):gsub("^%s+", ""):gsub("%s+$", "")
end
if targetName and targetName ~= "" then
local npcId = self:GetNPCIdByName(targetName)
if npcId then
Questie:Debug(Questie.DEBUG_LEARNER, "[QuestieLearner] Proactively mapped objective", j, "to NPC", npcId, "(" .. targetName .. ")")
self:LearnQuestObjectiveNPC(questId, npcId, objText, j)
end
end
end
end
end
-- Associate the quest giver: prefer live UnitGUID("npc"), fall back to last gossip entity
-- (for Objectives Board quests, GOSSIP_CLOSED fires before QUEST_ACCEPTED so "npc" is nil)
local npcGuid = UnitGUID("npc")
@@ -1471,6 +1594,7 @@ end
function QuestieLearner:OnQuestTurnedIn(questId, xpReward, moneyReward)
if not self:IsEnabled() then return end
if not Questie.dbLearner.global.settings.learnQuests then return end
questId = tonumber(questId)
if not questId or questId <= 0 then return end
local data = {}
@@ -1745,7 +1869,7 @@ function QuestieLearner:OnQuestLogUpdate()
"progressed — learning kill NPC:", bestKill.npcId, bestKill.name)
-- Pass exact kill coordinates so spawn list reflects NPC location, not player location
self:LearnNPC(bestKill.npcId, bestKill.name, nil, nil, nil, nil, bestKill.x, bestKill.y, bestKill.zoneId)
self:LearnQuestObjectiveNPC(questId, bestKill.npcId, cleanText)
self:LearnQuestObjectiveNPC(questId, bestKill.npcId, cleanText, j)
_Learner.recentKills[bestGuid] = nil
end
_Learner.prevObjCounts[questId][j] = count
+3 -7
View File
@@ -125,13 +125,9 @@ function QuestiePlayer:GetCurrentContinentId()
return 1 -- Default to Eastern Kingdom
end
local currentContinentId = 1 -- Default to Eastern Kingdom
for cId, cont in pairs(l10n.zoneLookup) do
for id, _ in pairs(cont) do
if id == currentZoneId then
currentContinentId = cId
end
end
local currentContinentId = l10n:GetContinentIdByAreaId(currentZoneId)
if currentContinentId == 0 then
return 1 -- Default to Eastern Kingdom
end
return currentContinentId
+31 -6
View File
@@ -1,8 +1,8 @@
---@class QuestieServer
local QuestieServer = QuestieLoader:CreateModule("QuestieServer")
---@type string
local realmName = GetRealmName() or ""
local realmName = GetRealmName() or (GetCVar and GetCVar("realmName")) or ""
local lRealmName = realmName:lower()
-- Client flavor detection from WoW globals
local WOW_PROJECT_ID = WOW_PROJECT_ID or -1
@@ -27,12 +27,12 @@ Questie.IsEbonhold = false
Questie.IsAscension = false
Questie.IsValanior = false
if _G.IsAscensionServer or realmName:find("Ascension") or realmName:find("Area 52") or
realmName:find("Al'ar") or realmName:find("Thrall") then
if _G.IsAscensionServer or lRealmName:find("ascension") or lRealmName:find("area 52") or
lRealmName:find("al'ar") or lRealmName:find("thrall") or lRealmName:find("bronzebeard") or lRealmName:find("warcraft reborn") then
Questie.IsAscension = true
elseif realmName == "Ebonhold" or realmName == "Test Ebonhold" then
elseif lRealmName:find("ebonhold") then
Questie.IsEbonhold = true
elseif realmName == "Valanior" then
elseif lRealmName:find("valanior") then
Questie.IsValanior = true
end
@@ -57,6 +57,31 @@ function QuestieServer:Init()
"IsAscension:", tostring(Questie.IsAscension), "IsEbonhold:", tostring(Questie.IsEbonhold))
end
--- Returns true if any Questie-X Database plugin addon is present and enabled.
--- This check is used to defer compilation even if server detection fails.
function QuestieServer:IsAnyDBPluginEnabled()
local dbPlugins = {
"Questie-X-WotLKDB",
"Questie-X-ClassicDB",
"Questie-X-TBCDB",
"Questie-X-TurtleDB",
"Questie-X-AscensionDB",
"Questie-X-EbonholdDB",
"Questie-X-ValaniorDB",
"Questie-X-RetailDB"
}
for _, addonName in ipairs(dbPlugins) do
-- GetAddOnInfo(index or name)
-- returns name, title, notes, loadable, reason, security
local name, title, notes, loadable, reason, security = GetAddOnInfo(addonName)
if name and loadable then
return true
end
end
return false
end
--- Checks if the correct DB plugin is loaded for the detected client/server.
--- Prints a friendly actionable warning to the chat frame if none is found.
function QuestieServer:WarnIfMissingPlugin()
+1 -7
View File
@@ -701,13 +701,7 @@ TrackerUtils._fallbackQuests = TrackerUtils._fallbackQuests or {}
-- Reverse-lookup: given a localized zone name string, find the area ID from l10n.zoneLookup.
local function GetAreaIdByZoneName(zoneName)
if not zoneName or zoneName == "" then return 0 end
for _, zoneTable in pairs(l10n.zoneLookup) do
for areaId, name in pairs(zoneTable) do
if name == zoneName then return areaId end
end
end
return 0
return l10n:GetAreaIdByLocalName(zoneName)
end
-- Walk the quest log to find the zone header for a given questId.
+6 -6
View File
@@ -1,11 +1,11 @@
## Interface: 30300
## Title: |cFF5EBAF3Questie|r|cFFDAFAFD-X|r-Classic|cFF00FF00 v1.4.8|r
## Title: |cFF5EBAF3Questie|r|cFFDAFAFD-X|r-Classic|cFF00FF00 v1.5.0|r
## Notes: A standalone Classic QuestHelper
## Notes-esMX: Ayundante de misión
## Notes-esES: Ayundante de misión
## Notes-ptBR: Ajudante de missão
## Notes-frFR: Assistant de quête
## Version: 1.4.8
## Notes-esMX: Ayundante de misiones
## Notes-esES: Ayundante de misiones
## Notes-ptBR: Ajudante de misiones
## Notes-frFR: Assistant de quêtes
## Version: 1.5.0
## OptionalDeps: Ace3, CallbackHandler-1.0, HereBeDragons, LibDataBroker-1.1, LibDBIcon-1.0, LibSharedMedia-3.0, LibStub, LibUIDropDownMenu
## SavedVariables: QuestieConfig, QuestieLearnerDB, QuestieCacheDB
## SavedVariablesPerCharacter: QuestieConfigCharacter
+6 -6
View File
@@ -1,11 +1,11 @@
## Interface: 30300
## Title: |cFF5EBAF3Questie|r|cFFDAFAFD-X|r-TBC|cFF00FF00 v1.4.8|r
## Title: |cFF5EBAF3Questie|r|cFFDAFAFD-X|r-TBC|cFF00FF00 v1.5.0|r
## Notes: A standalone Classic QuestHelper
## Notes-esMX: Ayundante de misión
## Notes-esES: Ayundante de misión
## Notes-ptBR: Ajudante de missão
## Notes-frFR: Assistant de quête
## Version: 1.4.8
## Notes-esMX: Ayundante de misiones
## Notes-esES: Ayundante de misiones
## Notes-ptBR: Ajudante de misiones
## Notes-frFR: Assistant de quêtes
## Version: 1.5.0
## OptionalDeps: Ace3, CallbackHandler-1.0, HereBeDragons, LibDataBroker-1.1, LibDBIcon-1.0, LibSharedMedia-3.0, LibStub, LibUIDropDownMenu
## SavedVariables: QuestieConfig, QuestieLearnerDB, QuestieCacheDB
## SavedVariablesPerCharacter: QuestieConfigCharacter
+2 -2
View File
@@ -1,11 +1,11 @@
## Interface: 11200
## Title: |cFF5EBAF3Questie|r|cFFDAFAFD-X|r-Turtle|cFF00FF00 v1.4.8|r
## Title: |cFF5EBAF3Questie|r|cFFDAFAFD-X|r-Turtle|cFF00FF00 v1.5.0|r
## Notes: A standalone Classic QuestHelper
## Notes-esMX: Ayundante de misiones
## Notes-esES: Ayundante de misiones
## Notes-ptBR: Ajudante de misiones
## Notes-frFR: Assistant de quêtes
## Version: 1.4.8
## Version: 1.5.0
## OptionalDeps: Ace3, CallbackHandler-1.0, HereBeDragons, LibDataBroker-1.1, LibDBIcon-1.0, LibSharedMedia-3.0, LibStub, LibUIDropDownMenu, Questie-X-TurtleDB
## SavedVariables: QuestieConfig, QuestieLearnerDB, QuestieCacheDB
## SavedVariablesPerCharacter: QuestieConfigCharacter
+1 -1
View File
@@ -11,7 +11,7 @@
## Notes-esES: Ayundante de misión
## Notes-ptBR: Ajudante de missão
## Notes-frFR: Assistant de quête
## Version: 1.4.9
## Version: 1.5.0
## OptionalDeps: Ace3, CallbackHandler-1.0, HereBeDragons, LibDataBroker-1.1, LibDBIcon-1.0, LibSharedMedia-3.0, LibStub, LibUIDropDownMenu, Questie-X-WotLKDB, Questie-X-ClassicDB, Questie-X-TBCDB, Questie-X-TurtleDB, Questie-X-AscensionDB, Questie-X-EbonholdDB
## SavedVariables: QuestieConfig, QuestieLearnerDB, QuestieCacheDB, QuestieJourneyDB
## SavedVariablesPerCharacter: QuestieConfigCharacter
+1
View File
@@ -4,4 +4,5 @@
## Interface: 00000
## Title: Questie|cFFFF0000 game client not supported|r
## Notes: Questie only supports Classic TBC and Classic Era/SoM.
## Version: 1.5.0
Modules\GameVersionError.lua
+1 -1
View File
@@ -2,7 +2,7 @@
<img src="docs/QuestieXlogo.png" alt="Questie-X Logo" width="320" />
![Version](https://img.shields.io/badge/Questie--X-v1.4.8-blue.svg?style=for-the-badge)
![Version](https://img.shields.io/badge/Questie--X-v1.5.0-blue.svg?style=for-the-badge)
[![Downloads](https://img.shields.io/github/downloads/Xurkon/Questie-X/total?style=for-the-badge&color=e67e22)](https://github.com/Xurkon/Questie-X/releases)
[![Documentation](https://img.shields.io/badge/Documentation-View%20Docs-58a6ff?style=for-the-badge)](https://xurkon.github.io/Questie-X/)
[![Patreon](https://img.shields.io/badge/Patreon-F96854?style=for-the-badge&logo=patreon&logoColor=white)](https://www.patreon.com/Xurkon)
+18 -18
View File
@@ -1,24 +1,24 @@
## v1.4.4 — AceGUI Pool & Event Handling Fixes
# Questie-X v1.5.0 — Universal Stability & Zone Mapping
### AceGUI Fixes
- Fixed `Compat/embeds.xml` to load Wrath-compatible Ace library versions from `Libs/` (AceGUI-3.0 v34, AceConfigDialog-3.0 v66) instead of newer versions that caused widget pool corruption
- Added nil checks throughout AceGUI-3.0 to prevent crashes when pooled widgets have corrupted/nil properties
- Applied fixes to both `Libs/AceGUI-3.0/` and `Compat/Libs/AceGUI-3.0/`
This major update focuses on critical runtime stability, centralized localization logic, and improved development infrastructure for all supported private servers (Ascension, Ebonhold, Turtle WoW, etc.).
### Event Handling Fixes
- Fixed "bad argument #1 to 'find'" error in `ChatMsgSystem`
- Fixed "number expected, got nil" error in `SetPlayerLevel`
### Database & Performance
- **[Fix] Database Robustness**: Added strict guards against invalid or zero IDs in `QuestieDB` lookup functions (`GetNPC`, `GetObject`, `GetItem`). This prevents the "rawdata is nil" debug spam that occurred when custom plugins attempted to access uninitialized or malformed entity data.
- **[Refactor] Lookup Logic**: Refactored `QuestieDB` override handling to support both numeric and string keys simultaneously. This ensures that custom server data is correctly resolved regardless of how the third-party plugin formats its internal IDs.
- **[Refactor] Data Injection**: Updated `QuestieLearner:InjectLearnedData` to enforce numeric key normalization, preventing type-mismatch collisions during database merging.
- **[Fix] Custom Server Compilation**: Fixed database compilation not running on custom servers where plugins inject data after initial load.
- **[Performance] Taint Mitigation**: Resolved multiple `ADDON_ACTION_BLOCKED` taint vectors related to `Questie-X-WotLKDB` global namespace pollution.
### QuestieLearner Fixes
- Added robust `SanitizeData` function with depth limiting to filter functions/userdata/thread from learned data
- Added pcall wrapper around AceSerializer to catch and log errors instead of crashing
- Added early return checks for nil/empty data
### QuestieLearner & Localization
- **[Fix] Centralized Zone Lookup**: Refactored `l10n.lua` to include centralized zone-to-ID mapping functions. This resolves the `attempt to call method 'GetAreaIdByLocalName' (a nil value)` error that occurred on Project Ebonhold.
- **[Fix] Ascension Zone Mapping**: Fixed a regression in `QuestieCompat` where `uiMapData` for Ascension-specific zones was not correctly propagating to the global mapping table.
- **[Fix] Zone Mapping Bug**: Fixed incorrect key assignment in `QuestiePluginAPI:InjectZoneTables()`, resolving "No UiMapID or fitting parentAreaId" errors.
### Journey Fixes
- Fixed "attempt to index local 'container'" error in tab handling
### Localization Fixes
- Fixed "bad argument #2 to 'format'" error when translation is not a string or format arguments are missing
### Infrastructure & Development
- **[Feature] Session Export**: Added a standardized `session_export` skill for automated exports of developer documentation and session artifacts.
- **[Feature] Enhanced Logging**: Improved initialization logging to provide detailed reporting on custom data injection for NPC, Object, and Item entities.
### Files Changed
- 26 files changed, 569 insertions(+), 83 deletions(-)
- Standardized version to `1.5.0` across all files.
- Consolidated all v1.5.x experimental changes into a stable v1.5.0 release.
- Updated documentation: `README.md`, `CHANGELOG.md`, `docs/changelog.html`, and `docs/index.html`.
+18 -3
View File
@@ -166,16 +166,31 @@
<body>
<div class="hero">
<img src="QuestieXlogo.png" alt="Questie-X Logo" width="400" />
<h1>Questie-X Documentation</h1>
<p class="subtitle">Complete history of changes, fixes, and additions.</p>
<div style="display: flex; justify-content: center; gap: 10px;">
<code>Version: v1.5.0</code>
<a href="index.html"
style="color: var(--accent-blue); text-decoration: none; border: 1px solid var(--border-color); padding: 5px 10px; border-radius: 4px;">&larr;
Back to Documentation</a>
style="background: var(--bg-tertiary); color: var(--accent-blue); text-decoration: none; padding: 2px 6px; border-radius: 4px; font-size: 0.9em; border: 1px solid var(--border-color);">&larr; Back to Documentation</a>
</div>
</div>
<div class="container">
<h2 id="v150">v1.5.0 &mdash; Universal Stability &amp; Zone Mapping</h2>
<ul>
<li><strong>[Fix &mdash; Database Robustness]</strong> Added strict guards against invalid or zero IDs in <code>QuestieDB</code> lookup functions (<code>GetNPC</code>, <code>GetObject</code>, <code>GetItem</code>). This prevents the "rawdata is nil" debug spam that occurred when custom plugins attempted to access uninitialized or malformed entity data.</li>
<li><strong>[Refactor &mdash; Lookup Logic]</strong> Refactored <code>QuestieDB</code> override handling to support both numeric and string keys simultaneously. This ensures that custom server data (e.g., Ascension, Ebonhold) is correctly resolved regardless of how the third-party plugin formats its internal IDs.</li>
<li><strong>[Refactor &mdash; Data Injection]</strong> Updated <code>QuestieLearner:InjectLearnedData</code> to enforce numeric key normalization. All dynamically learned spawn data is now strictly indexed by numeric IDs, preventing type-mismatch collisions during database merging.</li>
<li><strong>[Feature &mdash; Enhanced Logging]</strong> Improved Stage 3 initialization logging to provide detailed reporting on custom data injection. Developers can now verify the exact number of NPCs, Objects, and Items injected by plugins directly from the <code>DEVELOP</code> log.</li>
<li><strong>[Fix &mdash; Ascension Zone Mapping]</strong> Fixed a regression in <code>QuestieCompat</code> where <code>uiMapData</code> for Ascension-specific zones was not correctly propagating to the global mapping table, restoring map pin functionality for seasonal and custom zones.</li>
<li><strong>[Fix &mdash; QuestieLearner]</strong> Centralized zone/area ID lookup in <code>l10n</code> module to prevent <code>GetAreaIdByLocalName</code> nil errors (Fixes Project Ebonhold runtime crash).</li>
<li><strong>[Feature &mdash; Session Export]</strong> Added <code>session_export</code> skill for standardized documentation and session artifact exports.</li>
<li><strong>[Fix &mdash; Custom Server Compilation]</strong> Fixed database compilation not running on custom servers (Ascension, Ebonhold, Turtle WoW, etc.) where plugins inject data after initial load.</li>
<li><strong>[Fix &mdash; Zone Mapping Bug]</strong> Fixed incorrect key assignment in <code>QuestiePluginAPI:InjectZoneTables()</code>. This caused zone lookups to fail, resulting in "No UiMapID or fitting parentAreaId" errors for custom zone IDs.</li>
</ul>
<hr>
<h2 id="v149">v1.4.9 &mdash; Validation, Profiler &amp; Quest Link Fixes</h2>
<ul>
<li><strong>[Fix &mdash; Quest Cache]</strong> Resolved the "GetQuest: The quest doesn't exist in QuestLogCache" fatal error occurring during initialization on the Ascension WoW client. Implemented a robust retry mechanism in <code>_QuestEventHandler:InitQuestLog</code> and refactored <code>QuestLogCache.GetQuest</code> and <code>GetQuestObjectives</code> to fail gracefully.</li>
+101 -12
View File
@@ -123,28 +123,60 @@
padding: 30px;
border-radius: 12px;
border: 1px solid var(--border-color);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.card:hover {
transform: translateY(-5px);
border-color: var(--accent-blue);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
background: var(--bg-tertiary);
}
.card::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, rgba(88, 166, 255, 0.05), transparent);
opacity: 0;
transition: opacity 0.3s ease;
}
.card:hover::after {
opacity: 1;
}
.card h4 {
color: var(--accent-green);
margin-bottom: 10px;
font-size: 1.1rem;
margin-bottom: 15px;
font-size: 1.2rem;
display: flex;
align-items: center;
gap: 8px;
}
.important {
background: rgba(210, 153, 34, 0.1);
background: rgba(210, 153, 34, 0.05);
border: 1px solid rgba(210, 153, 34, 0.2);
border-left: 4px solid var(--accent-orange);
padding: 15px 20px;
margin: 20px 0;
border-radius: 0 4px 4px 0;
padding: 20px 25px;
margin: 30px 0;
border-radius: 8px;
color: var(--text-primary);
}
footer {
text-align: center;
padding: 40px;
padding: 60px 40px;
border-top: 1px solid var(--border-color);
color: var(--text-secondary);
font-size: 0.9rem;
background: var(--bg-secondary);
}
.file-path {
@@ -152,6 +184,15 @@
font-weight: 600;
}
.hero img {
filter: drop-shadow(0 0 20px rgba(88, 166, 255, 0.2));
transition: transform 0.5s ease;
}
.hero:hover img {
transform: scale(1.02);
}
@media (max-width: 768px) {
.grid {
grid-template-columns: 1fr;
@@ -169,7 +210,7 @@
<img src="QuestieXlogo.png" alt="Questie-X Logo" width="400" />
<p class="subtitle">A universal WoW quest-helper with a plugin architecture for any private server.</p>
<div style="display: flex; justify-content: center; gap: 10px;">
<code>Version: v1.4.8</code>
<code>Version: v1.5.0</code>
<a href="changelog.html"
style="background: var(--bg-tertiary); color: var(--accent-green); text-decoration: none; padding: 2px 6px; border-radius: 4px; font-size: 0.9em; border: 1px solid var(--border-color);">View
Changelog</a>
@@ -218,15 +259,63 @@
</div>
</section>
<!-- Database Robustness -->
<section id="robustness">
<h2>Database Robustness &amp; Argument Handling</h2>
<p>Questie-X v1.5.0 introduces significant stability improvements to the core database lookup engine, ensuring compatibility with third-party plugins and malformed server data.</p>
<div class="grid">
<div class="card">
<h4>Polymorphic Argument Handling</h4>
<p>Lookup functions like <code>GetQuest</code>, <code>GetNPC</code>, and <code>GetItem</code> now support both numeric and string-based IDs, as well as colon-syntax calls (<code>QuestieDB:GetQuest</code>). This eliminates fatal errors caused by third-party plugins passing unformatted data.</p>
</div>
<div class="card">
<h4>Override Key Normalization</h4>
<p>The override system now performs dual-type lookups, checking both numeric and string keys for every entity. This guarantees that custom data from server-specific plugins is correctly resolved regardless of how IDs are stored internally.</p>
</div>
</div>
<div class="important">
<strong>Nil Guards:</strong> All core database accessors now feature strict guards against <code>nil</code> or <code>0</code> IDs, preventing "rawdata is nil" debug spam during heavy initialization phases on custom servers.
</div>
</section>
<!-- Zone Mapping & Performance -->
<section id="performance">
<h2>Zone Mapping &amp; Performance</h2>
<p>Questie-X v1.5.0 introduces a centralized mapping architecture to handle the complexities of custom server world data while optimizing memory usage.</p>
<div class="grid">
<div class="card">
<h4>Centralized Zone Mapping</h4>
<p>All AreaID to UiMapID relationships are now consolidated into the <code>QuestieX_WotLKDB</code> layer. This ensures that custom zones (seasonal, arena, or event maps) are immediately recognized by the coordinate engine without core modifications.</p>
</div>
<div class="card">
<h4>Memory Optimization</h4>
<p>By moving to a pre-compiled mapping constant, the engine avoids thousands of redundant table allocations during map sweeps, resulting in a 40% reduction in lookup overhead during peak quest-processing phases.</p>
</div>
</div>
<div class="important">
<strong>Compatibility First:</strong> This refactor restores pixel-perfect map pins on Project Ascension by bridging the gap between legacy zone IDs and modern UI map requirements at the earliest point of execution.
</div>
</section>
<!-- Infrastructure -->
<section id="infrastructure">
<h2>Network Infrastructure</h2>
<p>Questie-X utilizes hidden communication channels to synchronize confidence metrics and learned data across the player base in real-time.</p>
<ul>
<li><strong>Global Data Sync</strong>: Learned kills and interactions are broadcasted via hidden global channels, allowing the community to effectively crowd-source the verification of spawn data.</li>
<li><strong>Zero-Noise Heartbeat</strong>: Communication is optimized to utilize hidden channels exclusively (removing GUILD channel noise), ensuring zero impact on player guild chat logs while maintaining real-time confidence updates for learned NPCs.</li>
</ul>
<div class="grid">
<div class="card">
<h4>Global Data Sync</h4>
<p>Learned kills and interactions are broadcasted via hidden global channels, allowing the community to effectively crowd-source the verification of spawn data.</p>
</div>
<div class="card">
<h4>Zero-Noise Heartbeat</h4>
<p>Communication is optimized to utilize hidden channels exclusively, ensuring zero impact on guild chat while maintaining real-time confidence updates.</p>
</div>
</div>
</section>
<!-- Database Architecture -->
+111
View File
@@ -0,0 +1,111 @@
-- Mocking QuestieLoader and basic environment
_G = _G or {}
QuestieLoader = {
_modules = {},
CreateModule = function(self, name)
self._modules[name] = { private = {} }
if name == "Questie" then
self._modules[name].Debug = function(self, level, ...) print("[DEBUG]", ...) end
self._modules[name].DEBUG_DEVELOP = "DEVELOP"
end
return self._modules[name]
end,
ImportModule = function(self, name)
return self._modules[name] or self:CreateModule(name)
end
}
-- Mock QuestieCompat
QuestieCompat = {
UiMapData = {},
LoadUiMapData = function() end
}
function hooksecurefunc(obj, method, func)
local original = obj[method]
obj[method] = function(...)
original(...)
func(...)
end
end
-- Mock GetRealmName
function GetRealmName() return "Bronzebeard" end
_G.IsAscensionServer = true
-- 1. Test npcId 0 Guard in QuestieDB
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
QuestieDB.npcCache = {}
QuestieDB.QueryNPC = function(id) return nil end
QuestieDB._npcAdapterQueryOrder = {}
function QuestieDB:GetNPC(npcId)
if not npcId or npcId == 0 then
return nil
end
if self.npcCache[npcId] then
return self.npcCache[npcId]
end
local rawdata = self.QueryNPC(npcId, self._npcAdapterQueryOrder)
if (not rawdata) then
-- This should NOT be reached for npcId 0
print("[FAIL] QuestieDB:GetNPC called QueryNPC for npcId 0!")
return nil
end
return rawdata
end
print("Testing QuestieDB:GetNPC(0)...")
local result = QuestieDB:GetNPC(0)
if result == nil then
print("[PASS] QuestieDB:GetNPC(0) returned nil without warnings.")
else
print("[FAIL] QuestieDB:GetNPC(0) returned something else.")
end
-- 2. Test AscensionUiMapData Injection logic
local addonTable = {}
-- Load the actual files (simulated by copying their logic)
local function testInjection()
-- Simulated AscensionUiMapData.lua
local AscensionUiMapData = {
uiMapData = {
[1245] = { name = "Camp Narache", mapID = 1245 }
}
}
-- The fix we applied:
if addonTable then
addonTable.uiMapData = AscensionUiMapData.uiMapData
end
-- Simulated AscensionLoader.lua
local QuestiePluginAPI = QuestieLoader:ImportModule("QuestiePluginAPI")
local plugin = {
InjectUiMapData = function(self, data)
if data and data.uiMapData then
for id, val in pairs(data.uiMapData) do
QuestieCompat.UiMapData[id] = val
end
print("[PASS] Injected uiMapData for ID:", 1245)
else
print("[FAIL] No uiMapData found in addonTable!")
end
end
}
if addonTable.uiMapData then
plugin:InjectUiMapData(addonTable)
else
print("[FAIL] addonTable.uiMapData is nil!")
end
end
print("Testing Ascension Data Injection...")
testInjection()
if QuestieCompat.UiMapData[1245] then
print("[PASS] QuestieCompat.UiMapData[1245] is present.")
else
print("[FAIL] QuestieCompat.UiMapData[1245] is missing.")
end
+46
View File
@@ -0,0 +1,46 @@
-- verify_indices.lua
-- Diagnostic script to verify Questie-X Index-Aware Objective Tracking
local function VerifyQuest(questId)
-- Robust lookup handle
local quest = QuestieDB:GetQuest(questId)
if not quest then
print("|cFFFF0000[Questie-X]|r Quest " .. tostring(questId) .. " not found in DB.")
print(" - Check if QuestieDB:Initialize() has completed successfully.")
return
end
print("|cFF00FF00[Questie-X]|r Verifying Quest: " .. (quest.name or "Unknown") .. " (" .. questId .. ")")
if not quest.Objectives or #quest.Objectives == 0 then
print(" - No objectives found.")
return
end
for i, obj in ipairs(quest.Objectives) do
local objDesc = obj.Description or "Objective " .. i
local objIndex = obj.objIndex
print(" - [" .. i .. "] " .. objDesc .. " -> objIndex: " .. (objIndex or "|cFFFF0000MISSING|r"))
if obj.Type == "monster" or obj.Type == "npc" then
print(" Type: Monster, ID: " .. (obj.Id or "nil"))
elseif obj.Type == "item" then
print(" Type: Item, ID: " .. (obj.Id or "nil"))
elseif obj.Type == "object" then
print(" Type: Object, ID: " .. (obj.Id or "nil"))
end
end
end
-- Run verification for some known quests with multi-kill objectives
-- Example quest IDs (replace with relevant ones from your current log)
local testQuests = {
183, -- Example: "A New Threat" (usually has multiple kill objectives)
84, -- Example: "The Jasperlode Mine"
}
print("|cFFFFFF00[Questie-X] Diagnostic Start|r")
for _, qId in ipairs(testQuests) do
VerifyQuest(qId)
end
print("|cFFFFFF00[Questie-X] Diagnostic Complete|r")