Files
RelationshipsQuestAndItemBr…/RelationshipsQuestAndItemBrowser.lua
2026-07-14 14:12:09 +01:00

2749 lines
112 KiB
Lua

-- Relationships Quest And Item Browser
local RelationshipsQuestAndItemBrowser = CreateFrame("Frame", "RelationshipsQuestAndItemBrowserCore")
RelationshipsQuestAndItemBrowser:RegisterEvent("PLAYER_LOGIN")
RelationshipsQuestAndItemBrowser:RegisterEvent("ADDON_LOADED")
RelationshipsQuestAndItemBrowser:RegisterEvent("PLAYER_LOGOUT")
RelationshipsQuestAndItemBrowserDB = RelationshipsQuestAndItemBrowserDB or {}
local RQL_ORIGINAL_SET_ITEM_REF = nil
local RQL_ORIGINAL_CHAT_HYPERLINK_SHOW = nil
local RQL_INDEX_READY = nil
local RQL_SEARCH_RESULTS = {}
local RQL_SELECTED_QUEST_ID = nil
local RQL_MODE = "browser"
local RQL_SEARCH_TEXT = ""
local RQL_LEVEL_FILTER = 0
local RQL_TYPE_FILTER = 0
local RQL_FACTION_FILTER = 0
local RQL_SOURCE_FILTER = 0
local RQL_MAX_RESULTS = 5000
local RQL_ROWS_VISIBLE = 16
local RQL_ROW_HEIGHT = 20
local RQL_PAGE = 1
local RQL_DEFAULT_BACKGROUND = "Light Sepia"
local RQL_SUPPRESS_SETITEMREF_ROUTE = nil
local RQL_SUPPRESS_CHATFRAME_ROUTE = nil
local function RQL_GetStartupBackground()
local saved = RelationshipsQuestAndItemBrowserDB and RelationshipsQuestAndItemBrowserDB.bgColor
if saved and saved ~= "" then return saved end
return RQL_DEFAULT_BACKGROUND
end
-- Item browser state (Item tab)
local RQL_TAB = "quests" -- "quests" | "items"
local RQL_ITEM_SEARCH_TEXT = ""
local RQL_ITEM_SOURCE_FILTER = 0
local RQL_ITEM_SEARCH_RESULTS = {}
local RQL_ITEM_INDEX_READY = nil
local RQL_ITEM_PAGE = 1
local RQL_SELECTED_ITEM_ID = nil
local RQL_LEVELS = {
"All levels", "1-9", "10-19", "20-29", "30-39", "40-49", "50-59", "60+"
}
local RQL_TYPES = {
"All types", "Normal", "Elite/Group", "Dungeon/Raid", "PvP", "Class", "Profession", "Seasonal/Event"
}
local RQL_FACTIONS = { "All factions", "Alliance", "Horde", "Neutral/Both" }
local RQL_SOURCES = { "All sources", "Vanilla", "Turtle/OctoWoW" }
-- ---------------------------------------------------------------------------
-- Dungeon / Raid classification
-- ---------------------------------------------------------------------------
-- Keyword-based detection is far more reliable than zone-id lookups because
-- pfQuest coord data for dungeon quests is inconsistent, and Turtle/OctoWoW
-- adds new dungeons/raids that aren't in vanilla's AreaTable at all.
--
-- A quest is considered Dungeon/Raid if its title, objective text or
-- description contains any of these keywords. Keywords are matched with
-- plain (non-pattern) string.find and are lower-case.
--
-- Sources: Wowhead Classic dungeon/raid quest lists, the Turtle WoW wiki
-- (turtle-wow.fandom.com) custom dungeon/raid pages, and OctoWoW patch notes.
local RQL_DUNGEON_KEYWORDS = {
-- Vanilla dungeons
"ragefire chasm",
"wailing caverns",
"the deadmines", "deadmines",
"shadowfang keep",
"blackfathom deeps",
"the stockade", "the stockades", "stormwind stockade",
"gnomeregan",
"razorfen kraul",
"razorfen downs",
"scarlet monastery",
"uldaman",
"zul'farrak", "zulfarrak", "zul\'farrak",
"maraudon",
"sunken temple", "temple of atal'hakkar", "atal\'hakkar", "atalhakkar",
"blackrock depths",
"lower blackrock spire", "upper blackrock spire", "blackrock spire", "lbrs", "ubrs",
"dire maul",
"stratholme",
"scholomance",
-- Vanilla raids
"molten core",
"onyxia's lair", "onyxia\'s lair", "onyxias lair", "lair of onyxia",
"blackwing lair", "nefarian",
"zul'gurub", "zul\'gurub", "zulgurub",
"ruins of ahn'qiraj", "ruins of ahn\'qiraj", "aq20",
"temple of ahn'qiraj", "temple of ahn\'qiraj", "aq40", "ahn'qiraj", "ahn\'qiraj",
"naxxramas",
-- Turtle WoW / OctoWoW dungeons & raids
"emerald sanctum",
"karazhan crypt", "karazhan halls", "lower karazhan", "upper karazhan", "karazhan",
"hateforge quarry", "hateforge",
"gilneas city", "gilneas",
"stormwind vault",
"crescent grove",
"black morass",
"caverns of time",
"tower of karazhan",
"kara10", "kara40",
}
-- ---------- helpers ----------
local function RQL_TableSize(t)
local n = 0
if type(t) == "table" then for _ in pairs(t) do n = n + 1 end end
return n
end
local function RQL_GetDB(section, key)
if RelationshipsQuestAndItemBrowserData and RelationshipsQuestAndItemBrowserData[section] then
return RelationshipsQuestAndItemBrowserData[section][key]
end
return nil
end
local function RQL_PatchTable(base, diff)
if type(diff) ~= "table" then return base end
if type(base) ~= "table" then base = {} end
for k, v in pairs(diff) do
if type(v) == "string" and v == "_" then
base[k] = nil
elseif type(v) == "table" and type(base[k]) == "table" then
RQL_PatchTable(base[k], v)
else
base[k] = v
end
end
return base
end
local function RQL_CopyTable(source)
local dest = {}
if type(source) ~= "table" then return dest end
for k, v in pairs(source) do
if type(v) == "table" then dest[k] = RQL_CopyTable(v) else dest[k] = v end
end
return dest
end
local function RQL_CleanText(text)
if not text then return "" end
text = tostring(text)
text = string.gsub(text, "%$B", "\n")
text = string.gsub(text, "%$N", UnitName("player") or "player")
text = string.gsub(text, "%$C", UnitClass("player") or "class")
text = string.gsub(text, "%$R", UnitRace("player") or "race")
text = string.gsub(text, "%$G([^:;]*):([^;]*);", "%1/%2")
text = string.gsub(text, "%$T", "")
text = string.gsub(text, "\\'", "'")
text = string.gsub(text, '\\"', '"')
return text
end
local function RQL_ParseQuestLink(link)
if not link then return nil end
local _, _, qid, qlevel, qname = string.find(link, "quest:(%d+):([%-%d]*)|h%[(.-)%]|h")
if qid then return tonumber(qid), tonumber(qlevel) or 0, qname end
_, _, qid = string.find(link, "quest:(%d+)")
if qid then return tonumber(qid), 0, nil end
return nil
end
local function RQL_ColorByLevel(questLevel)
if not questLevel or questLevel == 0 then return "|cFFFFFFFF" end
local playerLevel = UnitLevel("player") or 1
local diff = questLevel - playerLevel
if diff >= 5 then return "|cFFFF0000"
elseif diff >= 3 then return "|cFFFF8040"
elseif diff >= -2 then return "|cFFFFFF00"
elseif diff >= -4 then return "|cFF40BF40" end
return "|cFF808080"
end
local function RQL_EntryIsDeleted(v)
return type(v) == "string" and v == "_"
end
-- Detect dev/test/unused quest titles that should not appear in the browser.
local function RQL_IsDevQuestTitle(title)
if not title or title == "" then return 1 end
local low = string.lower(title)
if string.find(low, "%[dep%]", 1) then return 1 end
if string.find(low, "%[dev%]", 1) then return 1 end
if string.find(low, "%[nyi%]", 1) then return 1 end
if string.find(low, "%[phase", 1) then return 1 end
if string.find(low, "%(dev%)", 1) then return 1 end
if string.find(low, "%(test%)", 1) then return 1 end
if string.find(low, "test quest", 1, true) then return 1 end
if string.find(low, "internal", 1, true) then return 1 end
if string.find(low, "unused", 1, true) then return 1 end
if string.find(low, "deprecated", 1, true) then return 1 end
if string.find(low, "placeholder", 1, true) then return 1 end
if string.find(low, "^qa ", 1) then return 1 end
if string.find(low, "^gm ", 1) then return 1 end
if string.find(low, "do not use", 1, true) then return 1 end
if string.find(low, "debug", 1, true) then return 1 end
if title == "NYI" or title == "TEST" or title == "OLD" then return 1 end
return nil
end
local function RQL_GetQuestLocale(id)
local vanilla = RQL_GetDB("quests", "enUS")
local turtle = RQL_GetDB("quests", "enUS-turtle")
local base = nil
if vanilla and type(vanilla[id]) == "table" then base = RQL_CopyTable(vanilla[id]) end
if turtle then
local tv = turtle[id]
if RQL_EntryIsDeleted(tv) then base = nil
elseif type(tv) == "table" then base = RQL_PatchTable(base, tv) end
end
return base
end
local function RQL_GetQuestMeta(id)
local vanilla = RQL_GetDB("quests", "data")
local turtle = RQL_GetDB("quests", "data-turtle")
local base = nil
if vanilla and type(vanilla[id]) == "table" then base = RQL_CopyTable(vanilla[id]) end
if turtle then
local tv = turtle[id]
if RQL_EntryIsDeleted(tv) then base = nil
elseif type(tv) == "table" then base = RQL_PatchTable(base, tv) end
end
return base
end
local function RQL_GetName(section, id)
local vanilla = RQL_GetDB(section, "enUS")
local turtle = RQL_GetDB(section, "enUS-turtle")
local name = nil
if vanilla and vanilla[id] and vanilla[id] ~= "_" then name = vanilla[id] end
if turtle and turtle[id] then
if turtle[id] == "_" then name = nil else name = turtle[id] end
end
if type(name) ~= "string" then return nil end
return name
end
local function RQL_GetData(section, id)
local vanilla = RQL_GetDB(section, "data")
local turtle = RQL_GetDB(section, "data-turtle")
local base = nil
if vanilla and type(vanilla[id]) == "table" then base = RQL_CopyTable(vanilla[id]) end
if turtle then
local tv = turtle[id]
if RQL_EntryIsDeleted(tv) then base = nil
elseif type(tv) == "table" then base = RQL_PatchTable(base, tv) end
end
return base
end
local function RQL_GetZoneName(zoneId)
if not zoneId then return "Unknown zone" end
return RQL_GetName("zones", zoneId) or ("Zone #" .. zoneId)
end
local function RQL_GetFirstCoord(section, id)
local data = RQL_GetData(section, id)
if data and type(data.coords) == "table" then
for _, coord in pairs(data.coords) do
if type(coord) == "table" and coord[1] and coord[2] and coord[3] then
return coord[1], coord[2], coord[3]
end
end
end
return nil
end
local function RQL_FormatCoord(section, id)
local x, y, zone = RQL_GetFirstCoord(section, id)
if x and y and zone then
return RQL_GetZoneName(zone) .. " " .. string.format("%.1f, %.1f", x, y)
end
return "Location unknown"
end
local function RQL_FormatEntityLines(section, ids, limit)
local out = {}
if type(ids) ~= "table" then return out end
local count = 0
local shown = 0
for _, id in pairs(ids) do
count = count + 1
if shown < (limit or 12) then
shown = shown + 1
local prefix
if section == "units" then prefix = "NPC #"
elseif section == "objects" then prefix = "Object #"
else prefix = "Item #" end
local name = RQL_GetName(section, id) or (prefix .. id)
local coord = ""
if section == "units" or section == "objects" then
coord = " |cFF808080" .. RQL_FormatCoord(section, id) .. "|r"
end
table.insert(out, " |cFFFFFFFF" .. name .. "|r|cFF808080 (" .. id .. ")|r" .. coord)
end
end
if count > shown then
table.insert(out, " |cFF808080...and " .. (count - shown) .. " more.|r")
end
return out
end
local function RQL_GetQuestSource(id)
local turtleLocale = RQL_GetDB("quests", "enUS-turtle")
local turtleData = RQL_GetDB("quests", "data-turtle")
if (turtleLocale and type(turtleLocale[id]) == "table") or (turtleData and type(turtleData[id]) == "table") then
return "Turtle/OctoWoW"
end
return "Vanilla"
end
-- Text-based dungeon detection: check title / objective / description
-- against a curated keyword list covering every vanilla dungeon + raid
-- and every Turtle/OctoWoW custom dungeon/raid.
local function RQL_TextMentionsDungeon(searchable)
if not searchable or searchable == "" then return nil end
for _, kw in ipairs(RQL_DUNGEON_KEYWORDS) do
if string.find(searchable, kw, 1, true) then return 1 end
end
return nil
end
local function RQL_GetQuestFlags(id, meta)
local flags = {}
local isElite, isDungeon, isPvp, isClass, isProfession, isEvent = nil,nil,nil,nil,nil,nil
if meta then
if meta.class then isClass = 1 end
if meta.skill then isProfession = 1 end
if meta.event then isEvent = 1 end
local function checkUnits(ids)
if type(ids) ~= "table" then return end
for _, uid in pairs(ids) do
local u = RQL_GetData("units", uid)
if u and u.rnk and tonumber(u.rnk) and tonumber(u.rnk) >= 1 then isElite = 1 end
end
end
if type(meta.start) == "table" then checkUnits(meta.start.U) end
if type(meta["end"]) == "table" then checkUnits(meta["end"].U) end
if type(meta.obj) == "table" then checkUnits(meta.obj.U) end
end
-- Text search over title + objectives + description for PvP/dungeon
local loc = RQL_GetQuestLocale(id)
local searchable = ""
if loc then
searchable = string.lower((loc.T or "") .. " " .. (loc.O or "") .. " " .. (loc.D or ""))
end
if string.find(searchable, "warsong gulch", 1, true)
or string.find(searchable, "arathi basin", 1, true)
or string.find(searchable, "alterac valley", 1, true)
or string.find(searchable, "battleground", 1, true) then
isPvp = 1
end
if RQL_TextMentionsDungeon(searchable) then
isDungeon = 1
end
-- PvP wins over Dungeon (Alterac Valley grave quests etc. are PvP)
if isPvp then isDungeon = nil end
if isDungeon then table.insert(flags, "Dungeon/Raid") end
if isElite and not isDungeon then table.insert(flags, "Elite/Group") end
if isPvp then table.insert(flags, "PvP") end
if isClass then table.insert(flags, "Class") end
if isProfession then table.insert(flags, "Profession") end
if isEvent then table.insert(flags, "Event") end
if table.getn(flags) == 0 then table.insert(flags, "Normal") end
return flags, isElite, isDungeon, isPvp, isClass, isProfession, isEvent
end
local function RQL_JoinFlags(flags)
local text = ""
for _, value in ipairs(flags) do
if text ~= "" then text = text .. ", " end
text = text .. value
end
return text
end
local function RQL_GetFaction(meta)
if not meta or not meta.race then return "Neutral/Both" end
local race = tonumber(meta.race) or 0
local ALLIANCE = 77
local HORDE = 178
if race == ALLIANCE then return "Alliance" end
if race == HORDE then return "Horde" end
if race == (ALLIANCE + HORDE) or race == 255 then return "Neutral/Both" end
return "Faction/race locked"
end
local function RQL_GetQuestData(id, linkLevel, linkName)
local locale = RQL_GetQuestLocale(id)
local meta = RQL_GetQuestMeta(id)
local data = {}
data.id = id
data.title = linkName or (locale and locale.T) or ("Quest #" .. id)
data.objectives = locale and locale.O or nil
data.description = locale and locale.D or nil
data.level = (meta and meta.lvl) or linkLevel or 0
data.min = (meta and meta.min) or 0
data.meta = meta
data.locale = locale
data.source = RQL_GetQuestSource(id)
data.flags = RQL_GetQuestFlags(id, meta)
data.faction = RQL_GetFaction(meta)
return data
end
-- ---------- indexing / filtering ----------
local function RQL_IndexQuests()
if RQL_INDEX_READY then return end
RQL_INDEX_READY = {}
local vanilla = RQL_GetDB("quests", "enUS") or {}
local turtle = RQL_GetDB("quests", "enUS-turtle") or {}
local ids = {}
for id, v in pairs(vanilla) do if type(v) == "table" then ids[id] = 1 end end
for id, v in pairs(turtle) do if type(v) == "table" then ids[id] = 1 end end
for id in pairs(ids) do
local loc = RQL_GetQuestLocale(id)
local meta = RQL_GetQuestMeta(id)
if loc and loc.T and loc.T ~= "_" then
local title = RQL_CleanText(loc.T)
if not RQL_IsDevQuestTitle(title) then
local obj = RQL_CleanText(loc.O or "")
local flags, isElite, isDungeon, isPvp, isClass, isProfession, isEvent = RQL_GetQuestFlags(id, meta)
table.insert(RQL_INDEX_READY, {
id = id, title = title,
lower = string.lower(title .. " " .. obj .. " " .. id),
level = (meta and meta.lvl) or 0,
min = (meta and meta.min) or 0,
flags = flags,
faction = RQL_GetFaction(meta),
source = RQL_GetQuestSource(id),
isElite = isElite, isDungeon = isDungeon, isPvp = isPvp,
isClass = isClass, isProfession = isProfession, isEvent = isEvent
})
end
end
end
table.sort(RQL_INDEX_READY, function(a, b)
if a.level == b.level then return a.title < b.title end
return a.level < b.level
end)
end
local function RQL_LevelMatches(level)
if RQL_LEVEL_FILTER == 0 then return 1 end
level = tonumber(level) or 0
if RQL_LEVEL_FILTER == 1 then return level >= 1 and level <= 9 end
if RQL_LEVEL_FILTER == 2 then return level >= 10 and level <= 19 end
if RQL_LEVEL_FILTER == 3 then return level >= 20 and level <= 29 end
if RQL_LEVEL_FILTER == 4 then return level >= 30 and level <= 39 end
if RQL_LEVEL_FILTER == 5 then return level >= 40 and level <= 49 end
if RQL_LEVEL_FILTER == 6 then return level >= 50 and level <= 59 end
if RQL_LEVEL_FILTER == 7 then return level >= 60 end
return 1
end
local function RQL_TypeMatches(row)
if RQL_TYPE_FILTER == 0 then return 1 end
if RQL_TYPE_FILTER == 1 then return not row.isElite and not row.isDungeon and not row.isPvp and not row.isClass and not row.isProfession and not row.isEvent end
if RQL_TYPE_FILTER == 2 then return row.isElite and not row.isDungeon end
if RQL_TYPE_FILTER == 3 then return row.isDungeon end
if RQL_TYPE_FILTER == 4 then return row.isPvp end
if RQL_TYPE_FILTER == 5 then return row.isClass end
if RQL_TYPE_FILTER == 6 then return row.isProfession end
if RQL_TYPE_FILTER == 7 then return row.isEvent end
return 1
end
local function RQL_FactionMatches(row)
if RQL_FACTION_FILTER == 0 then return 1 end
if RQL_FACTION_FILTER == 1 then return row.faction == "Alliance" or row.faction == "Neutral/Both" end
if RQL_FACTION_FILTER == 2 then return row.faction == "Horde" or row.faction == "Neutral/Both" end
if RQL_FACTION_FILTER == 3 then return row.faction == "Neutral/Both" end
return 1
end
local function RQL_SourceMatches(row)
if RQL_SOURCE_FILTER == 0 then return 1 end
if RQL_SOURCE_FILTER == 1 then return row.source == "Vanilla" end
if RQL_SOURCE_FILTER == 2 then return row.source == "Turtle/OctoWoW" end
return 1
end
local function RQL_SearchQuests()
RQL_IndexQuests()
RQL_SEARCH_RESULTS = {}
local query = string.lower(RQL_SEARCH_TEXT or "")
for _, row in ipairs(RQL_INDEX_READY) do
if (query == "" or string.find(row.lower, query, 1, true))
and RQL_LevelMatches(row.level) and RQL_TypeMatches(row)
and RQL_FactionMatches(row) and RQL_SourceMatches(row) then
table.insert(RQL_SEARCH_RESULTS, row)
if table.getn(RQL_SEARCH_RESULTS) >= RQL_MAX_RESULTS then break end
end
end
RQL_PAGE = 1
end
local RQL_UI = RQL_UI or {}
-- ---------- Item metadata / tooltip scanner ----------
-- Vanilla 1.12: GetItemInfo returns name, link, quality, minLevel, itemType,
-- itemSubType, maxStack, itemEquipLoc, itemTexture. Uncached items return nil;
-- probing via a hidden tooltip warms the client-side cache.
local RQL_ITEM_META_CACHE = {}
local function RQL_EnsureScanTip()
if not RQL_UI.scanTip then
RQL_UI.scanTip = CreateFrame("GameTooltip", "RelationshipsQuestAndItemBrowserScanTip", UIParent, "GameTooltipTemplate")
end
-- IMPORTANT: re-owner every call. In 1.12 a GameTooltip that has been
-- hidden / cleared returns 0 lines from SetHyperlink until SetOwner runs
-- again, which was the root cause of "item details never show" even after
-- the client had cached the item.
RQL_UI.scanTip:SetOwner(UIParent, "ANCHOR_NONE")
return RQL_UI.scanTip
end
local RQL_QUALITY_HEX = {
[0] = "|cff9d9d9d", [1] = "|cffffffff", [2] = "|cff1eff00",
[3] = "|cff0070dd", [4] = "|cffa335ee", [5] = "|cffff8000",
[6] = "|cffe6cc80", [7] = "|cffe6cc80",
}
local function RQL_GetItemMeta(id)
if not id then return nil end
local cached = RQL_ITEM_META_CACHE[id]
if cached and cached.name then return cached end
-- Warm the cache via the scanning tooltip.
local tip = RQL_EnsureScanTip()
tip:ClearLines()
tip:SetHyperlink("item:" .. id .. ":0:0:0")
-- GetItemInfo return order differs across 1.12-based clients:
-- * Vanilla 1.12 (9 values):
-- name, link, quality, minLevel, iType, iSubType, stack, equipLoc, texture
-- * Turtle / OctoWoW / TBC-onward (10 values, adds iLevel):
-- name, link, quality, iLevel, minLevel, iType, iSubType, stack, equipLoc, texture
-- If we blindly unpack 10 vars on a 9-value client, every field after
-- `quality` shifts left by one and `texture` receives `equipLoc`
-- (e.g. "INVTYPE_WEAPON"), which SetTexture renders as the red "?"
-- placeholder. Detect the shape from the last argument instead.
local r1, r2, r3, r4, r5, r6, r7, r8, r9, r10 = GetItemInfo(id)
local name, link, quality = r1, r2, r3
if not name then
if not cached then RQL_ITEM_META_CACHE[id] = { pending = 1 } end
return RQL_ITEM_META_CACHE[id]
end
local function RQL_IsTexturePath(v)
if type(v) ~= "string" then return nil end
if string.find(v, "\\") then return 1 end
local lv = string.lower(v)
if string.find(lv, "^interface") then return 1 end
return nil
end
local minLevel, iType, iSubType, maxStack, equipLoc, texture
if RQL_IsTexturePath(r10) then
-- 10-value form (has iLevel we don't need)
minLevel, iType, iSubType, maxStack, equipLoc, texture = r5, r6, r7, r8, r9, r10
elseif RQL_IsTexturePath(r9) then
-- 9-value vanilla form
minLevel, iType, iSubType, maxStack, equipLoc, texture = r4, r5, r6, r7, r8, r9
else
-- Unknown shape: prefer 9-value defaults, leave texture nil so the
-- caller falls back to the question-mark placeholder.
minLevel, iType, iSubType, maxStack, equipLoc = r4, r5, r6, r7, r8
texture = nil
end
if texture and not RQL_IsTexturePath(texture) then texture = nil end
local m = {
name = name, link = link, quality = quality or 1,
minLevel = minLevel or 0, iType = iType, iSubType = iSubType,
equipLoc = equipLoc, texture = texture,
}
RQL_ITEM_META_CACHE[id] = m
return m
end
local function RQL_ScanItemTooltipLines(id)
local tip = RQL_EnsureScanTip()
tip:ClearLines()
tip:SetHyperlink("item:" .. id .. ":0:0:0")
local n = tip:NumLines() or 0
local out = {}
-- We already display the weapon/armor subtype ("Axe", "Bow", ...) in
-- the meta header line, and vanilla renders that same word on the right
-- side of the slot line AND the "Binds when picked up/equipped" line.
-- Without suppression the panel reads: "Binds when picked up Axe" and
-- "Two-Hand Bow" on every weapon, which duplicates the header.
--
-- Suppress when the right-side text matches meta.iSubType / meta.iType
-- (case-insensitive) OR any known weapon/armor subtype word, so it works
-- even when the meta cache hasn't populated yet on the first scan.
local meta = RQL_ITEM_META_CACHE[id]
local dupA = meta and meta.iSubType or nil
local dupB = meta and meta.iType or nil
local dupSet = {
["axe"]=1,["axes"]=1,["bow"]=1,["bows"]=1,["gun"]=1,["guns"]=1,
["mace"]=1,["maces"]=1,["polearm"]=1,["polearms"]=1,
["staff"]=1,["staves"]=1,["sword"]=1,["swords"]=1,
["dagger"]=1,["daggers"]=1,["fist weapon"]=1,["fist weapons"]=1,
["crossbow"]=1,["crossbows"]=1,["wand"]=1,["wands"]=1,
["thrown"]=1,["cloth"]=1,["leather"]=1,["mail"]=1,["plate"]=1,
["shield"]=1,["shields"]=1,["idol"]=1,["libram"]=1,["totem"]=1,
["miscellaneous"]=1,
}
for i = 1, n do
local lfs = _G and _G["RelationshipsQuestAndItemBrowserScanTipTextLeft" .. i]
or getglobal("RelationshipsQuestAndItemBrowserScanTipTextLeft" .. i)
local rfs = _G and _G["RelationshipsQuestAndItemBrowserScanTipTextRight" .. i]
or getglobal("RelationshipsQuestAndItemBrowserScanTipTextRight" .. i)
local ltext = lfs and lfs:GetText() or ""
local rtext = rfs and rfs:GetText() or ""
local r, g, b = 1, 1, 1
if lfs and lfs.GetTextColor then r, g, b = lfs:GetTextColor() end
local hex = string.format("|cff%02x%02x%02x", math.floor((r or 1)*255), math.floor((g or 1)*255), math.floor((b or 1)*255))
local line = hex .. (ltext or "") .. "|r"
local rtrim = rtext and string.gsub(rtext, "^%s*(.-)%s*$", "%1") or ""
local rlow = string.lower(rtrim)
local isDup = (rtrim == "")
or (dupA and rlow == string.lower(dupA))
or (dupB and rlow == string.lower(dupB))
or dupSet[rlow]
if not isDup then
local rr, gg, bb = 1, 1, 1
if rfs and rfs.GetTextColor then rr, gg, bb = rfs:GetTextColor() end
local rhex = string.format("|cff%02x%02x%02x", math.floor((rr or 1)*255), math.floor((gg or 1)*255), math.floor((bb or 1)*255))
line = line .. " " .. rhex .. rtext .. "|r"
end
table.insert(out, line)
end
return out
end
-- ---------- Item browser (Items tab) ----------
local function RQL_GetItemSource(id)
local turtle = RQL_GetDB("items", "enUS-turtle")
if turtle and turtle[id] and turtle[id] ~= "_" then return "Turtle/OctoWoW" end
return "Vanilla"
end
-- Detect dev/test/unused item names that should be hidden from the browser.
local function RQL_IsDevItemName(name)
if not name or name == "" then return 1 end
local low = string.lower(name)
if string.find(low, "monster %-", 1) then return 1 end
if string.find(low, "qatest", 1, true) then return 1 end
if string.find(low, "^old", 1) then return 1 end
if string.find(low, "%[dep%]", 1) then return 1 end
if string.find(low, "%[dev%]", 1) then return 1 end
if string.find(low, "%[nyi%]", 1) then return 1 end
if string.find(low, "test item", 1, true) then return 1 end
if string.find(low, "unused", 1, true) then return 1 end
if string.find(low, "deprecated", 1, true) then return 1 end
if string.find(low, "placeholder", 1, true) then return 1 end
if string.find(low, "debug", 1, true) then return 1 end
return nil
end
-- Find quests that reference an item (as start / turn-in / objective).
local function RQL_ItemFindQuests(itemId)
local out = {}
local seen = {}
local function scan(dbKey)
local db = RQL_GetDB("quests", dbKey)
if type(db) ~= "table" then return end
for qid, meta in pairs(db) do
if type(meta) == "table" then
local hit
local function has(list)
if type(list) ~= "table" then return nil end
for _, v in pairs(list) do
if v == itemId then return 1 end
end
return nil
end
if type(meta.start) == "table" and has(meta.start.I) then hit = "Starts" end
if not hit and type(meta["end"]) == "table" and has(meta["end"].I) then hit = "Turn-in" end
if not hit and type(meta.obj) == "table" and has(meta.obj.I) then hit = "Objective" end
if hit and not seen[qid] then
seen[qid] = 1
table.insert(out, { id = qid, role = hit })
end
end
end
end
scan("data")
scan("data-turtle")
return out
end
local function RQL_IndexItems()
if RQL_ITEM_INDEX_READY then return end
RQL_ITEM_INDEX_READY = {}
local vanilla = RQL_GetDB("items", "enUS") or {}
local turtle = RQL_GetDB("items", "enUS-turtle") or {}
local ids = {}
for id, v in pairs(vanilla) do if type(v) == "string" and v ~= "_" then ids[id] = 1 end end
for id, v in pairs(turtle) do
if v == "_" then ids[id] = nil
elseif type(v) == "string" then ids[id] = 1 end
end
for id in pairs(ids) do
local name = RQL_GetName("items", id)
if name and not RQL_IsDevItemName(name) then
table.insert(RQL_ITEM_INDEX_READY, {
id = id,
name = name,
lower = string.lower(name .. " " .. id),
source = RQL_GetItemSource(id),
})
end
end
table.sort(RQL_ITEM_INDEX_READY, function(a, b) return a.name < b.name end)
end
local function RQL_ItemSourceMatches(row)
if RQL_ITEM_SOURCE_FILTER == 0 then return 1 end
if RQL_ITEM_SOURCE_FILTER == 1 then return row.source == "Vanilla" end
if RQL_ITEM_SOURCE_FILTER == 2 then return row.source == "Turtle/OctoWoW" end
return 1
end
local function RQL_SearchItems()
RQL_IndexItems()
RQL_ITEM_SEARCH_RESULTS = {}
local query = string.lower(RQL_ITEM_SEARCH_TEXT or "")
for _, row in ipairs(RQL_ITEM_INDEX_READY) do
if (query == "" or string.find(row.lower, query, 1, true))
and RQL_ItemSourceMatches(row) then
table.insert(RQL_ITEM_SEARCH_RESULTS, row)
if table.getn(RQL_ITEM_SEARCH_RESULTS) >= RQL_MAX_RESULTS then break end
end
end
RQL_ITEM_PAGE = 1
end
-- ---------- UI ----------
function RQL_LinkQuestToChat(id, level, title)
if not id then return end
local meta = RQL_GetQuestMeta(id)
local loc = RQL_GetQuestLocale(id)
local lvl = tonumber(level) or (meta and tonumber(meta.lvl)) or 0
local name = title or (loc and loc.T) or ("Quest #" .. id)
local link = "|cffffff00|Hquest:" .. id .. ":" .. lvl .. "|h[" .. name .. "]|h|r"
if not ChatFrameEditBox then return end
if not ChatFrameEditBox:IsVisible() then
ChatFrameEditBox:Show()
end
ChatFrameEditBox:SetFocus()
ChatFrameEditBox:Insert(link)
end
local function RQL_TotalPages()
local results = (RQL_TAB == "items") and RQL_ITEM_SEARCH_RESULTS or RQL_SEARCH_RESULTS
local n = table.getn(results)
if n <= 0 then return 1 end
return math.floor((n - 1) / RQL_ROWS_VISIBLE) + 1
end
local function RQL_CurrentPage()
if RQL_TAB == "items" then return RQL_ITEM_PAGE end
return RQL_PAGE
end
local function RQL_SetCurrentPage(p)
if RQL_TAB == "items" then RQL_ITEM_PAGE = p else RQL_PAGE = p end
end
local function RQL_UpdateStatus()
if RQL_TAB == "items" then
if RQL_UI.itemStatus then
local total = RQL_ITEM_INDEX_READY and table.getn(RQL_ITEM_INDEX_READY) or 0
local n = table.getn(RQL_ITEM_SEARCH_RESULTS)
RQL_UI.itemStatus:SetText("Matches: " .. n .. " / " .. total .. " items")
end
else
if RQL_UI.status then
local total = RQL_INDEX_READY and table.getn(RQL_INDEX_READY) or 0
local n = table.getn(RQL_SEARCH_RESULTS)
RQL_UI.status:SetText("Matches: " .. n .. " / " .. total .. " quests")
end
end
if RQL_UI.pageLabel then
RQL_UI.pageLabel:SetText("Page " .. RQL_CurrentPage() .. " / " .. RQL_TotalPages())
end
end
local function RQL_UpdateList()
if not RQL_UI.rows then return end
local total = RQL_TotalPages()
local page = RQL_CurrentPage()
if page < 1 then page = 1 end
if page > total then page = total end
RQL_SetCurrentPage(page)
local offset = (page - 1) * RQL_ROWS_VISIBLE
local results = (RQL_TAB == "items") and RQL_ITEM_SEARCH_RESULTS or RQL_SEARCH_RESULTS
for i = 1, RQL_ROWS_VISIBLE do
local row = RQL_UI.rows[i]
local idx = i + offset
local r = results[idx]
if r then
if RQL_TAB == "items" then
local meta = RQL_GetItemMeta(r.id)
local qcol = "|cFFFFFFFF"
local lvlText = "?"
if meta and meta.name then
qcol = RQL_QUALITY_HEX[meta.quality or 1] or "|cFFFFFFFF"
local minLvl = tonumber(meta.minLevel)
lvlText = (minLvl and minLvl > 0) and tostring(minLvl) or "-"
end
row.left:SetText(qcol .. "[" .. lvlText .. "] " .. r.name .. "|r")
row.right:SetText("|cFF808080(#" .. r.id .. ", " .. r.source .. ")|r")
row.itemId = r.id
row.itemName = r.name
row.questId = nil
else
local lvlNum = tonumber(r.level)
local lvl = lvlNum and lvlNum > 0 and tostring(lvlNum) or "?"
local color = RQL_ColorByLevel(r.level)
row.left:SetText(color .. "[" .. lvl .. "]|r " .. r.title)
row.right:SetText("|cFF808080(" .. RQL_JoinFlags(r.flags) .. ", " .. r.source .. ")|r")
row.questId = r.id
row.questLevel = r.level
row.questTitle = r.title
row.itemId = nil
end
row:Show()
else
row:Hide()
row.questId = nil
row.itemId = nil
end
end
if RQL_UI.pageLabel then
RQL_UI.pageLabel:SetText("Page " .. page .. " / " .. total)
end
if RQL_UI.btnPrev then
if page <= 1 then RQL_UI.btnPrev:Disable() else RQL_UI.btnPrev:Enable() end
end
if RQL_UI.btnNext then
if page >= total then RQL_UI.btnNext:Disable() else RQL_UI.btnNext:Enable() end
end
end
-- ---------- Detail view (Quest-log style) ----------
-- Header gap (space above a section header) and body gap (space between the
-- header and its body text) are separated so titles never sit tight against
-- the previous paragraph, matching the real quest log layout.
local RQL_HEADER_GAP = 20 -- space above a section header
local RQL_BODY_GAP = 8 -- space between header and its body
local RQL_DETAIL_PAD = 18 -- inner left/right/top padding of the parchment
local function RQL_DetailSetSection(fs, label, body, prev)
if not body or body == "" then
fs.header:Hide(); fs.text:Hide()
return prev
end
fs.header:SetText(label)
fs.text:SetText(body)
fs.header:ClearAllPoints()
if prev then
fs.header:SetPoint("TOPLEFT", prev, "BOTTOMLEFT", 0, -RQL_HEADER_GAP)
else
fs.header:SetPoint("TOPLEFT", RQL_UI.detailContent, "TOPLEFT", 0, 0)
end
fs.text:ClearAllPoints()
fs.text:SetPoint("TOPLEFT", fs.header, "BOTTOMLEFT", 0, -RQL_BODY_GAP)
fs.text:SetWidth(RQL_UI.detailContent:GetWidth())
fs.header:Show(); fs.text:Show()
return fs.text
end
local function RQL_MakeDetailSection(parent)
-- Header: gold quest-log header font
local h = parent:CreateFontString(nil, "OVERLAY", "QuestTitleFont")
h:SetJustifyH("LEFT"); h:Hide()
-- Body: the standard quest-log body font
local b = parent:CreateFontString(nil, "OVERLAY", "QuestFont")
b:SetJustifyH("LEFT"); b:SetJustifyV("TOP"); b:SetSpacing(2); b:Hide()
return { header = h, text = b }
end
local RQL_ShowQuest -- forward declaration; defined below
local function RQL_RenderQuestDetail(id, linkLevel, linkName)
local data = RQL_GetQuestData(id, linkLevel, linkName)
-- Title
RQL_UI.dTitle:SetText(data.title)
RQL_UI.dTitle:SetWidth(RQL_UI.detailContent:GetWidth())
-- Meta line under title (with a bit of breathing room below the title)
local color = RQL_ColorByLevel(data.level)
local lvlText = (data.level and data.level > 0) and tostring(data.level) or "?"
local minText = (data.min and data.min > 0) and tostring(data.min) or "any"
local metaLine = color .. "Level " .. lvlText .. "|r"
.. " |cFFFFD100Requires:|r " .. minText
.. " |cFFFFD100Type:|r " .. RQL_JoinFlags(data.flags)
.. " |cFFFFD100Faction:|r " .. data.faction
.. " |cFFFFD100Source:|r " .. data.source
.. " |cFF808080#" .. id .. "|r"
RQL_UI.dMeta:SetText(metaLine)
RQL_UI.dMeta:SetWidth(RQL_UI.detailContent:GetWidth())
RQL_UI.dMeta:ClearAllPoints()
RQL_UI.dMeta:SetPoint("TOPLEFT", RQL_UI.dTitle, "BOTTOMLEFT", 0, -10)
local prev = RQL_UI.dMeta
prev = RQL_DetailSetSection(RQL_UI.dObjSection, "Objective",
data.objectives and RQL_CleanText(data.objectives) or nil, prev)
prev = RQL_DetailSetSection(RQL_UI.dDescSection, "Description",
data.description and RQL_CleanText(data.description) or nil, prev)
-- Start / End / Objective NPC + Object lists (items handled separately below)
local locBody = ""
if data.meta then
local function add(label, lines)
if table.getn(lines) == 0 then return end
if locBody ~= "" then locBody = locBody .. "\n\n" end
locBody = locBody .. "|cFFFFD100" .. label .. "|r\n\n" .. table.concat(lines, "\n")
end
if type(data.meta.start) == "table" then
add("Quest Giver (NPC)", RQL_FormatEntityLines("units", data.meta.start.U))
add("Quest Giver (Object)", RQL_FormatEntityLines("objects", data.meta.start.O))
end
if type(data.meta["end"]) == "table" then
add("Turn-in (NPC)", RQL_FormatEntityLines("units", data.meta["end"].U))
add("Turn-in (Object)", RQL_FormatEntityLines("objects", data.meta["end"].O))
end
if type(data.meta.obj) == "table" then
add("Objective NPCs", RQL_FormatEntityLines("units", data.meta.obj.U))
add("Objective Objects", RQL_FormatEntityLines("objects", data.meta.obj.O))
end
end
prev = RQL_DetailSetSection(RQL_UI.dLocSection, "Locations",
locBody ~= "" and locBody or nil, prev)
-- ---------- Clickable Items section ----------
RQL_UI.dItemButtons = RQL_UI.dItemButtons or {}
for i = 1, table.getn(RQL_UI.dItemButtons) do RQL_UI.dItemButtons[i]:Hide() end
local itemEntries = {}
local seenItem = {}
local function collectItems(label, ids)
if type(ids) ~= "table" then return end
for _, iid in pairs(ids) do
if iid and not seenItem[iid] then
seenItem[iid] = 1
local name = RQL_GetName("items", iid) or ("Item #" .. iid)
table.insert(itemEntries, { id = iid, name = name, role = label })
end
end
end
if data.meta then
if type(data.meta.start) == "table" then collectItems("Starts quest", data.meta.start.I) end
if type(data.meta["end"]) == "table" then collectItems("Turn-in", data.meta["end"].I) end
if type(data.meta.obj) == "table" then collectItems("Objective", data.meta.obj.I) end
end
if table.getn(itemEntries) > 0 then
local sec = RQL_UI.dItemSection
sec.header:SetText("Items (click to open)")
sec.header:ClearAllPoints()
sec.header:SetPoint("TOPLEFT", prev, "BOTTOMLEFT", 0, -RQL_HEADER_GAP)
sec.header:Show()
sec.text:Hide()
local anchor = sec.header
local gap = RQL_BODY_GAP
for i = 1, table.getn(itemEntries) do
local entry = itemEntries[i]
local btn = RQL_UI.dItemButtons[i]
if not btn then
btn = CreateFrame("Button", nil, RQL_UI.detailContent)
btn:SetHeight(16)
local fs = btn:CreateFontString(nil, "OVERLAY", "QuestFont")
fs:SetPoint("LEFT", btn, "LEFT", 0, 0)
fs:SetJustifyH("LEFT")
btn.fs = fs
local hl = btn:CreateTexture(nil, "HIGHLIGHT")
hl:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
hl:SetBlendMode("ADD")
hl:SetAllPoints(btn)
btn:SetScript("OnEnter", function()
if this.fs then this.fs:SetTextColor(1, 1, 0.4) end
end)
btn:SetScript("OnLeave", function()
if this.fs then this.fs:SetTextColor(1, 1, 1) end
end)
btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
btn:SetScript("OnClick", function()
if this.itemId then
RQL_ShowItem(this.itemId, this.itemName)
if RQL_UI.detailScroll then RQL_UI.detailScroll:SetVerticalScroll(0) end
end
end)
RQL_UI.dItemButtons[i] = btn
end
btn.itemId = entry.id
btn.itemName = entry.name
btn.fs:SetText(" |cFFFFFFFF" .. entry.name .. "|r |cFF808080(#" .. entry.id .. ", " .. entry.role .. ")|r |cFFAAAAFF[click to open]|r")
btn.fs:SetTextColor(1, 1, 1)
btn:SetWidth(RQL_UI.detailContent:GetWidth())
btn:ClearAllPoints()
btn:SetPoint("TOPLEFT", anchor, (anchor == sec.header) and "BOTTOMLEFT" or "BOTTOMLEFT", 0, -gap)
btn:Show()
anchor = btn
gap = 4
end
prev = anchor
else
RQL_UI.dItemSection.header:Hide()
RQL_UI.dItemSection.text:Hide()
end
-- Prerequisites (clickable — jump straight to the required quest)
RQL_UI.dPreButtons = RQL_UI.dPreButtons or {}
-- Hide any previously-shown prereq buttons before re-rendering
for i = 1, table.getn(RQL_UI.dPreButtons) do
RQL_UI.dPreButtons[i]:Hide()
end
local preList = (data.meta and type(data.meta.pre) == "table") and data.meta.pre or nil
if preList and table.getn(preList) > 0 then
local sec = RQL_UI.dPreSection
sec.header:SetText("Prerequisites (click to open)")
sec.header:ClearAllPoints()
sec.header:SetPoint("TOPLEFT", prev, "BOTTOMLEFT", 0, -RQL_HEADER_GAP)
sec.header:Show()
sec.text:Hide()
local anchor = sec.header
local gap = RQL_BODY_GAP
for i = 1, table.getn(preList) do
local preId = preList[i]
local preLocale = RQL_GetQuestLocale(preId)
local name = (preLocale and preLocale.T) or ("Quest #" .. preId)
local btn = RQL_UI.dPreButtons[i]
if not btn then
btn = CreateFrame("Button", nil, RQL_UI.detailContent)
btn:SetHeight(16)
local fs = btn:CreateFontString(nil, "OVERLAY", "QuestFont")
fs:SetPoint("LEFT", btn, "LEFT", 0, 0)
fs:SetJustifyH("LEFT")
btn.fs = fs
local hl = btn:CreateTexture(nil, "HIGHLIGHT")
hl:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
hl:SetBlendMode("ADD")
hl:SetAllPoints(btn)
btn:SetScript("OnEnter", function()
if this.fs then this.fs:SetTextColor(1, 1, 0.4) end
end)
btn:SetScript("OnLeave", function()
if this.fs then this.fs:SetTextColor(1, 1, 1) end
end)
btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
btn:SetScript("OnClick", function()
if this.preId then
RQL_ShowQuest(this.preId, 0, this.preName)
if RQL_UI.detailScroll then RQL_UI.detailScroll:SetVerticalScroll(0) end
end
end)
RQL_UI.dPreButtons[i] = btn
end
btn.preId = preId
btn.preName = name
btn.fs:SetText(" " .. name .. " |cFF808080(#" .. preId .. ")|r |cFFAAAAFF[click to open]|r")
btn.fs:SetTextColor(1, 1, 1)
btn:SetWidth(RQL_UI.detailContent:GetWidth())
btn:ClearAllPoints()
btn:SetPoint("TOPLEFT", anchor, (anchor == sec.header) and "BOTTOMLEFT" or "BOTTOMLEFT", 0, -gap)
btn:Show()
anchor = btn
gap = 4
end
prev = anchor
else
-- No prerequisites — hide the section entirely.
RQL_UI.dPreSection.header:Hide()
RQL_UI.dPreSection.text:Hide()
end
if not data.meta then
RQL_UI.dFooter:SetText("|cFFFF8080No bundled database entry found. Showing link-only data.|r")
RQL_UI.dFooter:ClearAllPoints()
RQL_UI.dFooter:SetPoint("TOPLEFT", prev, "BOTTOMLEFT", 0, -RQL_HEADER_GAP)
RQL_UI.dFooter:SetWidth(RQL_UI.detailContent:GetWidth())
RQL_UI.dFooter:Show()
else
RQL_UI.dFooter:Hide()
end
-- Recompute scroll-child height so the ScrollFrame knows how far to scroll.
-- Without this the child stays at its initial height (1) and the wheel /
-- scrollbar have no range on longer quests.
local function _rql_h(fs)
if not fs then return 0 end
if fs.IsShown and not fs:IsShown() then return 0 end
-- GetStringHeight may be missing on some 1.12 clients / non-FontString
-- regions. Guard the call and fall back to GetHeight so a nil method
-- can't abort the whole layout (which would leave the ScrollFrame
-- with a stale/zero range and break the scrollbar).
local h
if type(fs.GetStringHeight) == "function" then
local ok, v = pcall(fs.GetStringHeight, fs)
if ok and v and v > 0 then h = v end
end
if (not h) and type(fs.GetHeight) == "function" then
local ok, v = pcall(fs.GetHeight, fs)
if ok and v then h = v end
end
return h or 0
end
local total = _rql_h(RQL_UI.dTitle) + 10 + _rql_h(RQL_UI.dMeta)
local function _rql_addSec(sec)
if sec and sec.header and sec.header:IsShown() then
total = total + RQL_HEADER_GAP + _rql_h(sec.header)
if sec.text and sec.text:IsShown() then
total = total + RQL_BODY_GAP + _rql_h(sec.text)
end
end
end
_rql_addSec(RQL_UI.dObjSection)
_rql_addSec(RQL_UI.dDescSection)
_rql_addSec(RQL_UI.dLocSection)
if RQL_UI.dPreSection and RQL_UI.dPreSection.header:IsShown() then
total = total + RQL_HEADER_GAP + _rql_h(RQL_UI.dPreSection.header)
local btns = RQL_UI.dPreButtons or {}
local shown = 0
for i = 1, table.getn(btns) do
if btns[i]:IsShown() then
total = total + (shown == 0 and RQL_BODY_GAP or 4) + (btns[i]:GetHeight() or 16)
shown = shown + 1
end
end
end
if RQL_UI.dItemSection and RQL_UI.dItemSection.header:IsShown() then
total = total + RQL_HEADER_GAP + _rql_h(RQL_UI.dItemSection.header)
local btns = RQL_UI.dItemButtons or {}
local shown = 0
for i = 1, table.getn(btns) do
if btns[i]:IsShown() then
total = total + (shown == 0 and RQL_BODY_GAP or 4) + (btns[i]:GetHeight() or 16)
shown = shown + 1
end
end
end
if RQL_UI.dFooter:IsShown() then
total = total + RQL_HEADER_GAP + _rql_h(RQL_UI.dFooter)
end
RQL_UI.detailContent:SetHeight(total + 24)
if RQL_UI.detailScroll and RQL_UI.detailScroll.UpdateScrollChildRect then
RQL_UI.detailScroll:UpdateScrollChildRect()
-- Nudge the scroll position so the ScrollFrame recomputes its
-- vertical range immediately. Without this, the scrollbar thumb
-- occasionally stays disabled on longer quests until the user
-- resizes or reopens the frame.
local cur = RQL_UI.detailScroll:GetVerticalScroll() or 0
local maxs = RQL_UI.detailScroll:GetVerticalScrollRange() or 0
if cur > maxs then cur = maxs end
RQL_UI.detailScroll:SetVerticalScroll(cur)
end
end
-- ---------- Show/hide screens ----------
local function RQL_UpdateTabVisuals()
if RQL_UI.tabQuests and RQL_UI.tabItems then
if RQL_TAB == "items" then
RQL_UI.tabItems:LockHighlight()
RQL_UI.tabQuests:UnlockHighlight()
else
RQL_UI.tabQuests:LockHighlight()
RQL_UI.tabItems:UnlockHighlight()
end
end
end
local function RQL_ShowBrowser()
RQL_TAB = "quests"
RQL_MODE = "browser"
RQL_UI.iRetryFor = nil
if RelationshipsQuestAndItemBrowserFrameTitle then RelationshipsQuestAndItemBrowserFrameTitle:SetText("Relationships Quest And Item Browser") end
if RQL_UI.detail then RQL_UI.detail:Hide() end
if RQL_UI.itemDetail then RQL_UI.itemDetail:Hide() end
if RQL_UI.itemControls then RQL_UI.itemControls:Hide() end
if RQL_UI.listArea then RQL_UI.listArea:Show() end
if RQL_UI.controls then RQL_UI.controls:Show() end
if RQL_UI.pager then RQL_UI.pager:Show() end
RQL_UpdateTabVisuals()
RQL_SearchQuests()
RQL_UpdateStatus()
RQL_UpdateList()
RelationshipsQuestAndItemBrowserFrame:Show()
end
local function RQL_ShowItemBrowser()
RQL_TAB = "items"
RQL_MODE = "browser"
RQL_UI.iRetryFor = nil
if RelationshipsQuestAndItemBrowserFrameTitle then RelationshipsQuestAndItemBrowserFrameTitle:SetText("Relationships Quest And Item Browser") end
if RQL_UI.detail then RQL_UI.detail:Hide() end
if RQL_UI.itemDetail then RQL_UI.itemDetail:Hide() end
if RQL_UI.controls then RQL_UI.controls:Hide() end
if RQL_UI.itemControls then RQL_UI.itemControls:Show() end
if RQL_UI.listArea then RQL_UI.listArea:Show() end
if RQL_UI.pager then RQL_UI.pager:Show() end
RQL_UpdateTabVisuals()
RQL_SearchItems()
RQL_UpdateStatus()
RQL_UpdateList()
RelationshipsQuestAndItemBrowserFrame:Show()
end
function RQL_ShowQuest(id, level, name)
RQL_MODE = "details"
RQL_SELECTED_QUEST_ID = id
-- Cancel any pending item-detail tooltip retry from a previous screen.
RQL_UI.iRetryFor = nil
if RelationshipsQuestAndItemBrowserFrameTitle then RelationshipsQuestAndItemBrowserFrameTitle:SetText("Quest Details") end
if RQL_UI.controls then RQL_UI.controls:Hide() end
if RQL_UI.listArea then RQL_UI.listArea:Hide() end
if RQL_UI.pager then RQL_UI.pager:Hide() end
-- Hide item screens too, otherwise navigating quest -> item -> related
-- quest leaves the item detail rendered underneath the quest text.
if RQL_UI.itemControls then RQL_UI.itemControls:Hide() end
if RQL_UI.itemDetail then RQL_UI.itemDetail:Hide() end
RQL_RenderQuestDetail(id, level, name)
if RQL_UI.detailScroll then RQL_UI.detailScroll:SetVerticalScroll(0) end
if RQL_UI.detail then RQL_UI.detail:Show() end
RelationshipsQuestAndItemBrowserFrame:Show()
end
-- Item detail renderer + shower
function RQL_ShowItem(id, name)
if not id then return end
RQL_MODE = "itemDetails"
RQL_SELECTED_ITEM_ID = id
if not RQL_UI.itemDetail then return end
if RelationshipsQuestAndItemBrowserFrameTitle then RelationshipsQuestAndItemBrowserFrameTitle:SetText("Item Details") end
if RQL_UI.controls then RQL_UI.controls:Hide() end
if RQL_UI.itemControls then RQL_UI.itemControls:Hide() end
if RQL_UI.listArea then RQL_UI.listArea:Hide() end
if RQL_UI.pager then RQL_UI.pager:Hide() end
if RQL_UI.detail then RQL_UI.detail:Hide() end
local resolvedName = name or RQL_GetName("items", id) or ("Item #" .. id)
local source = RQL_GetItemSource(id)
local meta = RQL_GetItemMeta(id)
local qcol = "|cFFFFFFFF"
local lvlText = "-"
local extra = ""
if meta and meta.name then
qcol = RQL_QUALITY_HEX[meta.quality or 1] or "|cFFFFFFFF"
local ml = tonumber(meta.minLevel)
if ml and ml > 0 then lvlText = tostring(ml) end
if meta.iType then
extra = " |cFFFFD100Type:|r " .. meta.iType
if meta.iSubType and meta.iSubType ~= "" then extra = extra .. " (" .. meta.iSubType .. ")" end
end
end
RQL_UI.iTitle:SetText(qcol .. "[" .. lvlText .. "] " .. resolvedName .. "|r")
RQL_UI.iMeta:SetText("|cFFFFD100Item ID:|r " .. id .. " |cFFFFD100Source:|r " .. source .. extra)
-- Icon: use GetItemInfo texture when we have it; fall back to the
-- vanilla question-mark placeholder so the slot is never empty. Keep
-- it on top of the decorative slot art; UI-EmptySlot-Disabled is not
-- just a border and can otherwise cover the icon on 1.12 clients.
if RQL_UI.iIcon then
if meta and meta.texture then
RQL_UI.iIcon:SetTexture(meta.texture)
else
RQL_UI.iIcon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")
end
RQL_UI.iIcon:Show()
end
-- Scan full tooltip and render into the description FontString.
if not RQL_UI.iDesc then
-- Use the standard quest-log body font (larger + more legible than
-- GameFontHighlightSmall) and add generous line spacing so each
-- stat / enchant / requirement row is clearly separated.
RQL_UI.iDesc = RQL_UI.itemDetailContent:CreateFontString(nil, "OVERLAY", "QuestFont")
RQL_UI.iDesc:SetJustifyH("LEFT")
RQL_UI.iDesc:SetJustifyV("TOP")
RQL_UI.iDesc:SetSpacing(7)
end
RQL_UI.iDesc:ClearAllPoints()
-- Anchor description strictly BELOW the icon + meta block. Icon is 44
-- tall at y=0, meta line sits under the title to its right, so the
-- guaranteed clearance point is icon-height + a comfortable gap.
RQL_UI.iDesc:SetPoint("TOPLEFT", RQL_UI.itemDetailContent, "TOPLEFT",
0, -(44 + RQL_HEADER_GAP + 6))
RQL_UI.iDesc:SetWidth(RQL_UI.itemDetailContent:GetWidth())
local lines = RQL_ScanItemTooltipLines(id)
local desc
if lines and table.getn(lines) > 0 then
desc = table.concat(lines, "\n")
else
-- Fall back to whatever GetItemInfo gave us so the panel is never
-- empty. An OnUpdate retry (below) will replace this once the
-- client-side item cache is warm.
local parts = {}
table.insert(parts, qcol .. resolvedName .. "|r")
if meta and meta.name then
if meta.iType then
local t = meta.iType
if meta.iSubType and meta.iSubType ~= "" then t = t .. " (" .. meta.iSubType .. ")" end
table.insert(parts, "|cFFFFFFFF" .. t .. "|r")
end
if meta.minLevel and meta.minLevel > 0 then
table.insert(parts, "|cFFFFFFFFRequires Level " .. meta.minLevel .. "|r")
end
end
table.insert(parts, "|cFF808080Loading full item data from the server... this will refresh in a moment.|r")
desc = table.concat(parts, "\n")
-- Schedule a re-scan; the OnUpdate ticker in RQL_UI.retryTicker will
-- keep probing SetHyperlink until GameTooltip returns real lines.
RQL_UI.iRetryFor = id
RQL_UI.iRetryElapsed = 0
RQL_UI.iRetryTries = 0
end
RQL_UI.iDesc:SetText(desc)
RQL_UI.iDesc:Show()
local descH = RQL_UI.iDesc:GetHeight() or 0
-- Clear previous quest buttons
for i = 1, table.getn(RQL_UI.iQuestButtons) do RQL_UI.iQuestButtons[i]:Hide() end
local relatedQuests = RQL_ItemFindQuests(id)
local sec = RQL_UI.iQuestSection
local totalHeight = 40 + 20 + RQL_HEADER_GAP + descH
if table.getn(relatedQuests) > 0 then
sec.header:SetText("Related Quests (click to open)")
sec.header:ClearAllPoints()
sec.header:SetPoint("TOPLEFT", RQL_UI.iDesc, "BOTTOMLEFT", 0, -RQL_HEADER_GAP)
sec.header:Show()
sec.text:Hide()
totalHeight = totalHeight + RQL_HEADER_GAP + 20
local anchor = sec.header
local gap = RQL_BODY_GAP
for i = 1, table.getn(relatedQuests) do
local entry = relatedQuests[i]
local loc = RQL_GetQuestLocale(entry.id)
local meta = RQL_GetQuestMeta(entry.id)
local title = (loc and loc.T) or ("Quest #" .. entry.id)
local lvl = (meta and meta.lvl) or 0
local btn = RQL_UI.iQuestButtons[i]
if not btn then
btn = CreateFrame("Button", nil, RQL_UI.itemDetailContent)
btn:SetHeight(16)
local fs = btn:CreateFontString(nil, "OVERLAY", "QuestFont")
fs:SetPoint("LEFT", btn, "LEFT", 0, 0)
fs:SetJustifyH("LEFT")
btn.fs = fs
local hl = btn:CreateTexture(nil, "HIGHLIGHT")
hl:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
hl:SetBlendMode("ADD")
hl:SetAllPoints(btn)
btn:SetScript("OnEnter", function() if this.fs then this.fs:SetTextColor(1, 1, 0.4) end end)
btn:SetScript("OnLeave", function() if this.fs then this.fs:SetTextColor(1, 1, 1) end end)
btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
btn:SetScript("OnClick", function()
if this.qid then RQL_ShowQuest(this.qid, this.qlvl, this.qtitle) end
end)
RQL_UI.iQuestButtons[i] = btn
end
btn.qid = entry.id
btn.qlvl = lvl
btn.qtitle = title
local lvlText = (lvl and lvl > 0) and tostring(lvl) or "?"
local color = RQL_ColorByLevel(lvl)
btn.fs:SetText(" " .. color .. "[" .. lvlText .. "]|r " .. title .. " |cFF808080(#" .. entry.id .. ", " .. entry.role .. ")|r |cFFAAAAFF[click to open]|r")
btn.fs:SetTextColor(1, 1, 1)
btn:SetWidth(RQL_UI.itemDetailContent:GetWidth())
btn:ClearAllPoints()
btn:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", 0, -gap)
btn:Show()
anchor = btn
gap = 4
totalHeight = totalHeight + gap + 16
end
else
sec.header:SetText("Related Quests")
sec.header:ClearAllPoints()
sec.header:SetPoint("TOPLEFT", RQL_UI.iDesc, "BOTTOMLEFT", 0, -RQL_HEADER_GAP)
sec.header:Show()
sec.text:SetText("|cFF808080No quests reference this item in the bundled database.|r")
sec.text:ClearAllPoints()
sec.text:SetPoint("TOPLEFT", sec.header, "BOTTOMLEFT", 0, -RQL_BODY_GAP)
sec.text:SetWidth(RQL_UI.itemDetailContent:GetWidth())
sec.text:Show()
totalHeight = totalHeight + RQL_HEADER_GAP + RQL_BODY_GAP + 40
end
RQL_UI.itemDetailContent:SetHeight(totalHeight + 24)
if RQL_UI.itemDetailScroll and RQL_UI.itemDetailScroll.UpdateScrollChildRect then
RQL_UI.itemDetailScroll:UpdateScrollChildRect()
RQL_UI.itemDetailScroll:SetVerticalScroll(0)
end
RQL_UI.itemDetail:Show()
RelationshipsQuestAndItemBrowserFrame:Show()
end
-- Dropdown helper
local function RQL_MakeDropdown(name, parent, x, y, width, labelText, items, getIndex, setIndex)
local dd = CreateFrame("Frame", name, parent, "UIDropDownMenuTemplate")
dd:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
local label = dd:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
label:SetPoint("BOTTOMLEFT", dd, "TOPLEFT", 18, 2)
label:SetText(labelText)
UIDropDownMenu_SetWidth(width, dd)
local function init()
for i, txt in ipairs(items) do
local info = {}
info.text = txt
info.value = i - 1
info.checked = nil
info.func = function()
setIndex(this.value)
UIDropDownMenu_SetSelectedValue(dd, this.value)
UIDropDownMenu_SetText(items[this.value + 1], dd)
RQL_ShowBrowser()
end
UIDropDownMenu_AddButton(info)
end
end
UIDropDownMenu_Initialize(dd, init)
UIDropDownMenu_SetSelectedValue(dd, getIndex())
UIDropDownMenu_SetText(items[getIndex() + 1], dd)
return dd
end
local function RQL_BuildUI()
if RQL_UI.built then return end
RQL_UI.built = 1
local parent = RelationshipsQuestAndItemBrowserFrame
-- ---------- Tab bar (Quests / Items) ----------
local tabQuests = CreateFrame("Button", "RelationshipsQuestAndItemBrowserTabQuests", parent, "UIPanelButtonTemplate")
tabQuests:SetWidth(90) tabQuests:SetHeight(22)
tabQuests:SetPoint("TOPLEFT", parent, "TOPLEFT", 22, -42)
tabQuests:SetText("Quests")
tabQuests:SetScript("OnClick", function() RQL_ShowBrowser() end)
RQL_UI.tabQuests = tabQuests
local tabItems = CreateFrame("Button", "RelationshipsQuestAndItemBrowserTabItems", parent, "UIPanelButtonTemplate")
tabItems:SetWidth(90) tabItems:SetHeight(22)
tabItems:SetPoint("LEFT", tabQuests, "RIGHT", 6, 0)
tabItems:SetText("Items")
tabItems:SetScript("OnClick", function() RQL_ShowItemBrowser() end)
RQL_UI.tabItems = tabItems
-- ---------- Parchment background (quest-log style) ----------
-- The vanilla client ships Interface\QuestFrame\QuestBG as a 512x512
-- parchment. We tile a 2x2 grid so it covers the whole frame interior
-- regardless of frame size. Layered in BACKGROUND so it renders over
-- the plain dialog backdrop but under all content.
-- Darker parchment-brown background for better text contrast.
local bg = parent:CreateTexture(nil, "BACKGROUND")
-- Dark gold solid fill for the "Default" background. Chosen for good
-- contrast against white/yellow text without relying on a texture path
-- (some 1.12 clients fail to load Interface\QuestFrame\QuestBG and
-- would render fully transparent).
bg:SetTexture(0.32, 0.24, 0.10)
bg:SetPoint("TOPLEFT", parent, "TOPLEFT", 12, -12)
bg:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", -12, 12)
RQL_UI.bgParchment = bg
-- controls container
local controls = CreateFrame("Frame", nil, parent)
controls:SetPoint("TOPLEFT", parent, "TOPLEFT", 22, -84)
controls:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -22, -84)
controls:SetHeight(132)
RQL_UI.controls = controls
local searchLabel = controls:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
searchLabel:SetPoint("TOPLEFT", controls, "TOPLEFT", 0, 0)
searchLabel:SetText("Search:")
local edit = CreateFrame("EditBox", "RelationshipsQuestAndItemBrowserSearchBox", controls, "InputBoxTemplate")
edit:SetHeight(20)
edit:SetWidth(380)
edit:SetPoint("TOPLEFT", controls, "TOPLEFT", 60, 2)
edit:SetAutoFocus(nil)
edit:SetScript("OnEnterPressed", function()
RQL_SEARCH_TEXT = this:GetText() or ""
this:ClearFocus()
RQL_ShowBrowser()
end)
edit:SetScript("OnEscapePressed", function() this:ClearFocus() end)
edit:SetScript("OnTextChanged", function()
RQL_SEARCH_TEXT = this:GetText() or ""
RQL_SearchQuests()
RQL_UpdateStatus()
RQL_UpdateList()
end)
RQL_UI.search = edit
local btnSearch = CreateFrame("Button", nil, controls, "UIPanelButtonTemplate")
btnSearch:SetWidth(70) btnSearch:SetHeight(22)
btnSearch:SetPoint("LEFT", edit, "RIGHT", 8, 0)
btnSearch:SetText("Search")
btnSearch:SetScript("OnClick", function()
RQL_SEARCH_TEXT = edit:GetText() or ""
RQL_ShowBrowser()
end)
local btnReset = CreateFrame("Button", nil, controls, "UIPanelButtonTemplate")
btnReset:SetWidth(70) btnReset:SetHeight(22)
btnReset:SetPoint("LEFT", btnSearch, "RIGHT", 4, 0)
btnReset:SetText("Reset")
btnReset:SetScript("OnClick", function()
RQL_SEARCH_TEXT = ""
RQL_LEVEL_FILTER = 0; RQL_TYPE_FILTER = 0
RQL_FACTION_FILTER = 0; RQL_SOURCE_FILTER = 0
edit:SetText("")
UIDropDownMenu_SetSelectedValue(RQL_UI.ddLevel, 0); UIDropDownMenu_SetText(RQL_LEVELS[1], RQL_UI.ddLevel)
UIDropDownMenu_SetSelectedValue(RQL_UI.ddType, 0); UIDropDownMenu_SetText(RQL_TYPES[1], RQL_UI.ddType)
UIDropDownMenu_SetSelectedValue(RQL_UI.ddFaction, 0); UIDropDownMenu_SetText(RQL_FACTIONS[1], RQL_UI.ddFaction)
UIDropDownMenu_SetSelectedValue(RQL_UI.ddSource, 0); UIDropDownMenu_SetText(RQL_SOURCES[1], RQL_UI.ddSource)
RQL_ShowBrowser()
end)
-- ---------- Options: background color ----------
-- Colored overlay approach: leave the parchment backdrop untouched
-- (so "Default" is truly the original look) and lay a solid-color
-- texture over it inside the border. Hidden when Default is chosen.
RQL_BG_PRESETS = {
{ name = "Parchment", r = 0.00, g = 0.00, b = 0.00, a = 0.00 },
{ name = "Light Sepia", r = 0.8428, g = 0.7448, b = 0.5880, a = 1.00 },
{ name = "Sepia", r = 0.38, g = 0.29, b = 0.20, a = 1.00 },
{ name = "Charcoal", r = 0.25, g = 0.25, b = 0.26, a = 1.00 },
{ name = "Black", r = 0.14, g = 0.14, b = 0.15, a = 1.00 },
{ name = "Midnight Blue", r = 0.13, g = 0.20, b = 0.36, a = 1.00 },
{ name = "Forest Green", r = 0.12, g = 0.28, b = 0.18, a = 1.00 },
{ name = "Deep Red", r = 0.40, g = 0.13, b = 0.13, a = 1.00 },
{ name = "Royal Purple", r = 0.31, g = 0.15, b = 0.42, a = 1.00 },
}
-- Create the overlay once, parented to the main frame, sitting just
-- inside the DialogBox border insets (11/12/12/11) and BELOW all UI
-- content by using the BACKGROUND layer with a low sublevel.
if RelationshipsQuestAndItemBrowserFrame and not RQL_UI.bgOverlay then
-- Draw on BORDER layer (above the DialogBox parchment backdrop, below
-- ARTWORK/OVERLAY content) so the selected color fully covers the
-- parchment. Earlier this sat on BACKGROUND at sublevel 0, where on
-- some 1.12 / Turtle builds it rendered UNDER the parchment bgFile
-- and blended through it, making colors look muddy / "combined".
local ov = RelationshipsQuestAndItemBrowserFrame:CreateTexture(nil, "BORDER")
ov:SetTexture(0, 0, 0, 0)
ov:SetPoint("TOPLEFT", RelationshipsQuestAndItemBrowserFrame, "TOPLEFT", 11, -12)
ov:SetPoint("BOTTOMRIGHT", RelationshipsQuestAndItemBrowserFrame, "BOTTOMRIGHT", -12, 11)
ov:Hide()
RQL_UI.bgOverlay = ov
end
function RQL_ApplyBackground(name)
local preset = RQL_BG_PRESETS[1]
for i = 1, table.getn(RQL_BG_PRESETS) do
if RQL_BG_PRESETS[i].name == name then preset = RQL_BG_PRESETS[i] break end
end
local ov = RQL_UI.bgOverlay
if ov then
if preset.a and preset.a > 0 then
ov:SetTexture(preset.r, preset.g, preset.b, preset.a)
-- Belt-and-braces: some 1.12 clients cache the previous
-- color when SetTexture is called with new r,g,b on an
-- already-solid texture, producing a blended / wrong hue.
if ov.SetVertexColor then
ov:SetVertexColor(preset.r, preset.g, preset.b, preset.a)
end
ov:Show()
else
ov:Hide()
end
end
RelationshipsQuestAndItemBrowserDB = RelationshipsQuestAndItemBrowserDB or {}
RelationshipsQuestAndItemBrowserDB.bgColor = preset.name
end
local btnOptions = CreateFrame("Button", "RelationshipsQuestAndItemBrowserOptionsBtn", controls, "UIPanelButtonTemplate")
btnOptions:SetWidth(80) btnOptions:SetHeight(22)
btnOptions:SetPoint("LEFT", btnReset, "RIGHT", 4, 0)
btnOptions:SetText("Options")
-- A tiny custom menu is more reliable than UIDropDownMenu for this client.
-- Some 1.12/Turtle builds reuse dropdown button state in a way that makes
-- color clicks intermittently miss or call the wrong handler.
local optMenu = CreateFrame("Frame", "RelationshipsQuestAndItemBrowserOptionsMenu", parent)
optMenu:SetWidth(170)
optMenu:SetHeight(30 + (table.getn(RQL_BG_PRESETS) * 20))
optMenu:SetPoint("TOPLEFT", btnOptions, "BOTTOMLEFT", 0, -2)
optMenu:SetFrameStrata("DIALOG")
optMenu:SetBackdrop({
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
tile = true,
tileSize = 32,
edgeSize = 16,
insets = { left = 5, right = 5, top = 5, bottom = 5 }
})
optMenu:EnableMouse(true)
optMenu:Hide()
RQL_UI.optionsMenu = optMenu
local optTitle = optMenu:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
optTitle:SetPoint("TOPLEFT", optMenu, "TOPLEFT", 12, -10)
optTitle:SetText("Background Color")
for i = 1, table.getn(RQL_BG_PRESETS) do
local preset = RQL_BG_PRESETS[i]
local row = CreateFrame("Button", nil, optMenu)
row:SetWidth(146)
row:SetHeight(18)
row:SetPoint("TOPLEFT", optMenu, "TOPLEFT", 12, -10 - (i * 20))
row.presetName = preset.name
local swatch = row:CreateTexture(nil, "ARTWORK")
swatch:SetWidth(12)
swatch:SetHeight(12)
swatch:SetPoint("LEFT", row, "LEFT", 0, 0)
if preset.a and preset.a > 0 then
swatch:SetTexture(preset.r, preset.g, preset.b)
else
swatch:SetTexture(0.32, 0.24, 0.10)
end
local text = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
text:SetPoint("LEFT", swatch, "RIGHT", 6, 0)
text:SetText(preset.name)
row.text = text
row:SetScript("OnEnter", function()
if this.text then this.text:SetTextColor(1, 1, 0.4) end
end)
row:SetScript("OnLeave", function()
if this.text then this.text:SetTextColor(1, 1, 1) end
end)
row:SetScript("OnClick", function()
if this.presetName then RQL_ApplyBackground(this.presetName) end
if RQL_UI.optionsMenu then RQL_UI.optionsMenu:Hide() end
end)
end
btnOptions:SetScript("OnClick", function()
if RQL_UI.optionsMenu and RQL_UI.optionsMenu:IsShown() then
RQL_UI.optionsMenu:Hide()
elseif RQL_UI.optionsMenu then
RQL_UI.optionsMenu:Show()
end
end)
-- Filter dropdowns — spaced to fit inside a 720-wide frame
RQL_UI.ddLevel = RQL_MakeDropdown("RelationshipsQuestAndItemBrowserDDLevel", controls, -12, -72, 90, "Level", RQL_LEVELS, function() return RQL_LEVEL_FILTER end, function(v) RQL_LEVEL_FILTER = v end)
RQL_UI.ddType = RQL_MakeDropdown("RelationshipsQuestAndItemBrowserDDType", controls, 128, -72, 130, "Type", RQL_TYPES, function() return RQL_TYPE_FILTER end, function(v) RQL_TYPE_FILTER = v end)
RQL_UI.ddFaction = RQL_MakeDropdown("RelationshipsQuestAndItemBrowserDDFaction", controls, 298, -72, 120, "Faction", RQL_FACTIONS, function() return RQL_FACTION_FILTER end, function(v) RQL_FACTION_FILTER = v end)
RQL_UI.ddSource = RQL_MakeDropdown("RelationshipsQuestAndItemBrowserDDSource", controls, 458, -72, 130, "Source", RQL_SOURCES, function() return RQL_SOURCE_FILTER end, function(v) RQL_SOURCE_FILTER = v end)
local status = controls:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
status:SetPoint("TOPLEFT", controls, "TOPLEFT", 0, -114)
status:SetText("")
RQL_UI.status = status
-- pager (bottom)
local pager = CreateFrame("Frame", nil, parent)
pager:SetPoint("BOTTOMLEFT", parent, "BOTTOMLEFT", 22, 16)
pager:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", -22, 16)
pager:SetHeight(24)
RQL_UI.pager = pager
local btnPrev = CreateFrame("Button", nil, pager, "UIPanelButtonTemplate")
btnPrev:SetWidth(110) btnPrev:SetHeight(22)
btnPrev:SetPoint("LEFT", pager, "LEFT", 0, 0)
btnPrev:SetText("<< Prev Page")
btnPrev:SetScript("OnClick", function()
RQL_SetCurrentPage(RQL_CurrentPage() - 1)
RQL_UpdateList()
RQL_UpdateStatus()
end)
RQL_UI.btnPrev = btnPrev
local btnNext = CreateFrame("Button", nil, pager, "UIPanelButtonTemplate")
btnNext:SetWidth(110) btnNext:SetHeight(22)
btnNext:SetPoint("RIGHT", pager, "RIGHT", 0, 0)
btnNext:SetText("Next Page >>")
btnNext:SetScript("OnClick", function()
RQL_SetCurrentPage(RQL_CurrentPage() + 1)
RQL_UpdateList()
RQL_UpdateStatus()
end)
RQL_UI.btnNext = btnNext
local pageLabel = pager:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
pageLabel:SetPoint("CENTER", pager, "CENTER", 0, 0)
pageLabel:SetText("Page 1 / 1")
RQL_UI.pageLabel = pageLabel
-- list area
local listArea = CreateFrame("Frame", nil, parent)
listArea:SetPoint("TOPLEFT", parent, "TOPLEFT", 22, -224)
listArea:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", -22, 48)
RQL_UI.listArea = listArea
RQL_UI.rows = {}
for i = 1, RQL_ROWS_VISIBLE do
local row = CreateFrame("Button", nil, listArea)
row:SetHeight(RQL_ROW_HEIGHT)
if i == 1 then
row:SetPoint("TOPLEFT", listArea, "TOPLEFT", 0, 0)
row:SetPoint("TOPRIGHT", listArea, "TOPRIGHT", 0, 0)
else
row:SetPoint("TOPLEFT", RQL_UI.rows[i-1], "BOTTOMLEFT", 0, 0)
row:SetPoint("TOPRIGHT", RQL_UI.rows[i-1], "BOTTOMRIGHT", 0, 0)
end
local hl = row:CreateTexture(nil, "HIGHLIGHT")
hl:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
hl:SetBlendMode("ADD")
hl:SetAllPoints(row)
row:SetHighlightTexture(hl)
local right = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
right:SetPoint("RIGHT", row, "RIGHT", -4, 0)
right:SetJustifyH("RIGHT")
right:SetHeight(RQL_ROW_HEIGHT)
right:SetWidth(240)
right:SetNonSpaceWrap(false)
row.right = right
local left = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
left:SetPoint("LEFT", row, "LEFT", 4, 0)
left:SetPoint("RIGHT", right, "LEFT", -8, 0)
left:SetJustifyH("LEFT")
left:SetHeight(RQL_ROW_HEIGHT)
left:SetNonSpaceWrap(false)
row.left = left
row:RegisterForClicks("LeftButtonUp", "RightButtonUp")
row:SetScript("OnClick", function()
if this.itemId then
RQL_ShowItem(this.itemId, this.itemName)
return
end
if not this.questId then return end
RQL_ShowQuest(this.questId, this.questLevel, this.questTitle)
end)
RQL_UI.rows[i] = row
end
-- ---------- Detail view (quest-log style) ----------
local detail = CreateFrame("Frame", nil, parent)
detail:SetPoint("TOPLEFT", parent, "TOPLEFT", 22, -84)
detail:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", -22, 48)
detail:Hide()
RQL_UI.detail = detail
local dscroll = CreateFrame("ScrollFrame", "RelationshipsQuestAndItemBrowserDetailScroll", detail, "UIPanelScrollFrameTemplate")
dscroll:SetPoint("TOPLEFT", detail, "TOPLEFT", RQL_DETAIL_PAD, -RQL_DETAIL_PAD)
dscroll:SetPoint("BOTTOMRIGHT", detail, "BOTTOMRIGHT", -(RQL_DETAIL_PAD + 24), RQL_DETAIL_PAD)
RQL_UI.detailScroll = dscroll
dscroll:EnableMouseWheel(true)
dscroll:SetScript("OnMouseWheel", function()
local step = 24
local cur = this:GetVerticalScroll()
local maxs = this:GetVerticalScrollRange()
local nv = cur - (arg1 * step)
if nv < 0 then nv = 0 end
if nv > maxs then nv = maxs end
this:SetVerticalScroll(nv)
end)
local dcontent = CreateFrame("Frame", nil, dscroll)
-- Inner width: frame(720) - 2*margin(22) - 2*pad(18) - scrollbar(24) ≈ 616
dcontent:SetWidth(612) dcontent:SetHeight(1)
dscroll:SetScrollChild(dcontent)
RQL_UI.detailContent = dcontent
-- Title
local dTitle = dcontent:CreateFontString(nil, "OVERLAY", "QuestTitleFont")
dTitle:SetPoint("TOPLEFT", dcontent, "TOPLEFT", 0, 0)
dTitle:SetWidth(612)
dTitle:SetJustifyH("LEFT")
RQL_UI.dTitle = dTitle
-- Meta line
local dMeta = dcontent:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
dMeta:SetPoint("TOPLEFT", dTitle, "BOTTOMLEFT", 0, -10)
dMeta:SetWidth(612)
dMeta:SetJustifyH("LEFT")
RQL_UI.dMeta = dMeta
-- Sections
RQL_UI.dObjSection = RQL_MakeDetailSection(dcontent)
RQL_UI.dDescSection = RQL_MakeDetailSection(dcontent)
RQL_UI.dLocSection = RQL_MakeDetailSection(dcontent)
RQL_UI.dPreSection = RQL_MakeDetailSection(dcontent)
RQL_UI.dItemSection = RQL_MakeDetailSection(dcontent)
local dFooter = dcontent:CreateFontString(nil, "OVERLAY", "QuestFont")
dFooter:SetWidth(612)
dFooter:SetJustifyH("LEFT")
dFooter:Hide()
RQL_UI.dFooter = dFooter
local btnBack = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
btnBack:SetWidth(90) btnBack:SetHeight(22)
btnBack:SetPoint("BOTTOMLEFT", parent, "BOTTOMLEFT", 22, 16)
btnBack:SetText("Back")
btnBack:SetScript("OnClick", function()
local savedPage = RQL_PAGE
RQL_ShowBrowser()
RQL_PAGE = savedPage
RQL_UpdateList()
end)
local btnLink = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
btnLink:SetWidth(130) btnLink:SetHeight(22)
btnLink:SetPoint("LEFT", btnBack, "RIGHT", 6, 0)
btnLink:SetText("Link to Chat")
btnLink:SetScript("OnClick", function()
if RQL_SELECTED_QUEST_ID then
RQL_LinkQuestToChat(RQL_SELECTED_QUEST_ID, nil, nil)
end
end)
btnLink:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_TOP")
GameTooltip:AddLine("Insert a clickable quest link into the chat edit box.")
GameTooltip:AddLine("Tip: shift-click a quest in the list to link it directly.", 1,1,1)
GameTooltip:Show()
end)
btnLink:SetScript("OnLeave", function() GameTooltip:Hide() end)
detail:SetScript("OnShow", function() btnBack:Show(); btnLink:Show() end)
detail:SetScript("OnHide", function() btnBack:Hide(); btnLink:Hide() end)
btnBack:Hide()
btnLink:Hide()
-- ---------- Item controls (separate search bar for items) ----------
local iControls = CreateFrame("Frame", nil, parent)
iControls:SetPoint("TOPLEFT", parent, "TOPLEFT", 22, -84)
iControls:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -22, -84)
iControls:SetHeight(132)
iControls:Hide()
RQL_UI.itemControls = iControls
local iSearchLabel = iControls:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
iSearchLabel:SetPoint("TOPLEFT", iControls, "TOPLEFT", 0, 0)
iSearchLabel:SetText("Item Search:")
local iEdit = CreateFrame("EditBox", "RelationshipsQuestAndItemBrowserItemSearchBox", iControls, "InputBoxTemplate")
iEdit:SetHeight(20)
iEdit:SetWidth(380)
iEdit:SetPoint("TOPLEFT", iControls, "TOPLEFT", 78, 2)
iEdit:SetAutoFocus(nil)
iEdit:SetScript("OnEnterPressed", function()
RQL_ITEM_SEARCH_TEXT = this:GetText() or ""
this:ClearFocus()
RQL_ShowItemBrowser()
end)
iEdit:SetScript("OnEscapePressed", function() this:ClearFocus() end)
iEdit:SetScript("OnTextChanged", function()
RQL_ITEM_SEARCH_TEXT = this:GetText() or ""
RQL_SearchItems()
RQL_UpdateStatus()
RQL_UpdateList()
end)
RQL_UI.itemSearch = iEdit
local iBtnSearch = CreateFrame("Button", nil, iControls, "UIPanelButtonTemplate")
iBtnSearch:SetWidth(70) iBtnSearch:SetHeight(22)
iBtnSearch:SetPoint("LEFT", iEdit, "RIGHT", 8, 0)
iBtnSearch:SetText("Search")
iBtnSearch:SetScript("OnClick", function()
RQL_ITEM_SEARCH_TEXT = iEdit:GetText() or ""
RQL_ShowItemBrowser()
end)
local iBtnReset = CreateFrame("Button", nil, iControls, "UIPanelButtonTemplate")
iBtnReset:SetWidth(70) iBtnReset:SetHeight(22)
iBtnReset:SetPoint("LEFT", iBtnSearch, "RIGHT", 4, 0)
iBtnReset:SetText("Reset")
iBtnReset:SetScript("OnClick", function()
RQL_ITEM_SEARCH_TEXT = ""
RQL_ITEM_SOURCE_FILTER = 0
iEdit:SetText("")
if RQL_UI.ddItemSource then
UIDropDownMenu_SetSelectedValue(RQL_UI.ddItemSource, 0)
UIDropDownMenu_SetText(RQL_SOURCES[1], RQL_UI.ddItemSource)
end
RQL_ShowItemBrowser()
end)
RQL_UI.ddItemSource = RQL_MakeDropdown(
"RelationshipsQuestAndItemBrowserDDItemSource", iControls, -12, -72, 130, "Source", RQL_SOURCES,
function() return RQL_ITEM_SOURCE_FILTER end,
function(v) RQL_ITEM_SOURCE_FILTER = v end)
-- The RQL_MakeDropdown reset flow calls RQL_ShowBrowser after selection.
-- Rewrite the item-source dropdown init so it refreshes the ITEM browser.
UIDropDownMenu_Initialize(RQL_UI.ddItemSource, function()
for i, txt in ipairs(RQL_SOURCES) do
local info = {}
info.text = txt
info.value = i - 1
info.func = function()
RQL_ITEM_SOURCE_FILTER = this.value
UIDropDownMenu_SetSelectedValue(RQL_UI.ddItemSource, this.value)
UIDropDownMenu_SetText(RQL_SOURCES[this.value + 1], RQL_UI.ddItemSource)
RQL_ShowItemBrowser()
end
UIDropDownMenu_AddButton(info)
end
end)
UIDropDownMenu_SetSelectedValue(RQL_UI.ddItemSource, 0)
UIDropDownMenu_SetText(RQL_SOURCES[1], RQL_UI.ddItemSource)
local iStatus = iControls:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
iStatus:SetPoint("TOPLEFT", iControls, "TOPLEFT", 0, -114)
iStatus:SetText("")
RQL_UI.itemStatus = iStatus
-- ---------- Item detail view ----------
local iDetail = CreateFrame("Frame", nil, parent)
iDetail:SetPoint("TOPLEFT", parent, "TOPLEFT", 22, -84)
iDetail:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", -22, 48)
iDetail:Hide()
RQL_UI.itemDetail = iDetail
local iScroll = CreateFrame("ScrollFrame", "RelationshipsQuestAndItemBrowserItemDetailScroll", iDetail, "UIPanelScrollFrameTemplate")
iScroll:SetPoint("TOPLEFT", iDetail, "TOPLEFT", RQL_DETAIL_PAD, -RQL_DETAIL_PAD)
iScroll:SetPoint("BOTTOMRIGHT", iDetail, "BOTTOMRIGHT", -(RQL_DETAIL_PAD + 24), RQL_DETAIL_PAD)
iScroll:EnableMouseWheel(true)
iScroll:SetScript("OnMouseWheel", function()
local step = 24
local cur = this:GetVerticalScroll()
local maxs = this:GetVerticalScrollRange()
local nv = cur - (arg1 * step)
if nv < 0 then nv = 0 end
if nv > maxs then nv = maxs end
this:SetVerticalScroll(nv)
end)
RQL_UI.itemDetailScroll = iScroll
local iContent = CreateFrame("Frame", nil, iScroll)
iContent:SetWidth(612) iContent:SetHeight(1)
iScroll:SetScrollChild(iContent)
RQL_UI.itemDetailContent = iContent
-- Slot art goes behind the item icon. UI-EmptySlot-Disabled is a full
-- empty-slot texture (not just a transparent border), so using OVERLAY
-- made the icon area look like a blank box on Turtle/OctoWoW.
local iIconBorder = iContent:CreateTexture(nil, "BACKGROUND")
iIconBorder:SetTexture("Interface\\Buttons\\UI-EmptySlot-Disabled")
iIconBorder:SetWidth(66) iIconBorder:SetHeight(66)
iIconBorder:SetPoint("TOPLEFT", iContent, "TOPLEFT", -11, 11)
RQL_UI.iIconBorder = iIconBorder
-- Icon (populated from GetItemInfo in RQL_ShowItem). Sits top-left.
local iIcon = iContent:CreateTexture(nil, "ARTWORK")
iIcon:SetWidth(44) iIcon:SetHeight(44)
iIcon:SetPoint("TOPLEFT", iContent, "TOPLEFT", 0, 0)
iIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
iIcon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")
RQL_UI.iIcon = iIcon
local iTitle = iContent:CreateFontString(nil, "OVERLAY", "QuestTitleFont")
iTitle:SetPoint("TOPLEFT", iIcon, "TOPRIGHT", 14, -2)
iTitle:SetWidth(612 - 60)
iTitle:SetJustifyH("LEFT")
RQL_UI.iTitle = iTitle
local iMeta = iContent:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
iMeta:SetPoint("TOPLEFT", iTitle, "BOTTOMLEFT", 0, -8)
iMeta:SetWidth(612 - 60)
iMeta:SetJustifyH("LEFT")
RQL_UI.iMeta = iMeta
RQL_UI.iQuestSection = RQL_MakeDetailSection(iContent)
RQL_UI.iQuestButtons = {}
local iBtnBack = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
iBtnBack:SetWidth(90) iBtnBack:SetHeight(22)
iBtnBack:SetPoint("BOTTOMLEFT", parent, "BOTTOMLEFT", 22, 16)
iBtnBack:SetText("Back")
iBtnBack:SetScript("OnClick", function()
local savedPage = RQL_ITEM_PAGE
RQL_ShowItemBrowser()
RQL_ITEM_PAGE = savedPage
RQL_UpdateList()
end)
local iBtnLink = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
iBtnLink:SetWidth(130) iBtnLink:SetHeight(22)
iBtnLink:SetPoint("LEFT", iBtnBack, "RIGHT", 6, 0)
iBtnLink:SetText("Link to Chat")
iBtnLink:SetScript("OnClick", function()
if RQL_SELECTED_ITEM_ID then RQL_LinkItemToChat(RQL_SELECTED_ITEM_ID) end
end)
iDetail:SetScript("OnShow", function() iBtnBack:Show(); iBtnLink:Show() end)
iDetail:SetScript("OnHide", function() iBtnBack:Hide(); iBtnLink:Hide() end)
iBtnBack:Hide()
iBtnLink:Hide()
end
-- ---------- Item-detail async cache retry ----------
-- Vanilla 1.12 clients populate the item cache asynchronously the first
-- time an item is referenced. We keep probing GameTooltip for up to a few
-- seconds after opening an item panel and refresh the description as soon
-- as real tooltip lines come back.
function RQL_ItemDetailRetryTick(elapsed)
-- Deferred chat-link insertion: poll GetItemInfo until the client has
-- cached the real signed hyperlink, then drop it into the edit box.
if RQL_UI.iLinkPendingFor then
RQL_UI.iLinkPendingElapsed = (RQL_UI.iLinkPendingElapsed or 0) + (elapsed or 0)
if RQL_UI.iLinkPendingElapsed >= 0.2 then
RQL_UI.iLinkPendingElapsed = 0
RQL_UI.iLinkPendingTries = (RQL_UI.iLinkPendingTries or 0) + 1
local pid = RQL_UI.iLinkPendingFor
if RQL_TryInsertItemLink(pid) then
RQL_UI.iLinkPendingFor = nil
elseif RQL_UI.iLinkPendingTries >= 25 then
RQL_UI.iLinkPendingFor = nil
if DEFAULT_CHAT_FRAME then
DEFAULT_CHAT_FRAME:AddMessage("|cFFFF6060RQL|r Could not fetch item link from server. Try again in a moment.")
end
end
end
end
if not RQL_UI.iRetryFor then return end
RQL_UI.iRetryElapsed = (RQL_UI.iRetryElapsed or 0) + (elapsed or 0)
if RQL_UI.iRetryElapsed < 0.25 then return end
RQL_UI.iRetryElapsed = 0
RQL_UI.iRetryTries = (RQL_UI.iRetryTries or 0) + 1
local id = RQL_UI.iRetryFor
local lines = RQL_ScanItemTooltipLines(id)
if lines and table.getn(lines) > 0 then
RQL_UI.iRetryFor = nil
if RQL_UI.iDesc and RQL_UI.itemDetail and RQL_UI.itemDetail:IsShown()
and RQL_SELECTED_ITEM_ID == id then
RQL_UI.iDesc:SetText(table.concat(lines, "\n"))
if RQL_UI.itemDetailContent and RQL_UI.itemDetailScroll
and RQL_UI.itemDetailScroll.UpdateScrollChildRect then
local h = 40 + 20 + RQL_HEADER_GAP + (RQL_UI.iDesc:GetHeight() or 0) + 60
local btns = RQL_UI.iQuestButtons or {}
for i = 1, table.getn(btns) do
if btns[i]:IsShown() then h = h + 20 end
end
RQL_UI.itemDetailContent:SetHeight(h + 24)
RQL_UI.itemDetailScroll:UpdateScrollChildRect()
end
end
elseif RQL_UI.iRetryTries >= 20 then
RQL_UI.iRetryFor = nil
end
end
-- ---------- Item chat linking ----------
-- Insert a genuine client-cached item link into chat. Vanilla WoW strips
-- user-typed |Hitem:...|h strings before send -- ONLY the |cXXXX|Hitem:...
-- link that GetItemInfo returns after the client has cached the item is
-- treated as a real hyperlink. We warm the cache via SetHyperlink and
-- retry for a few seconds if needed.
local function RQL_TryInsertItemLink(id)
local name, link, quality = GetItemInfo(id)
if not name then return nil end
-- On some 1.12-based clients (Turtle WoW / OctoWoW) GetItemInfo can
-- return the bare itemString ("item:12345:0:0:0") instead of the fully
-- signed "|cXXXXXXXX|Hitem:...|h[Name]|h|r" hyperlink. If we don't
-- detect that we would drop raw text into chat that looks like
-- "item:0:0:0:12345" and is not clickable. Rebuild a proper hyperlink
-- from the pieces we do have.
if not link or not string.find(link, "|H") then
local color = RQL_QUALITY_HEX[quality or 1] or "|cffffffff"
link = color .. "|Hitem:" .. id .. ":0:0:0|h[" .. name .. "]|h|r"
end
if not ChatFrameEditBox then return 1 end
if not ChatFrameEditBox:IsVisible() then ChatFrameEditBox:Show() end
ChatFrameEditBox:SetFocus()
ChatFrameEditBox:Insert(link)
return 1
end
function RQL_LinkItemToChat(id)
if not id then return end
-- Force the client to cache the item so GetItemInfo returns the real
-- signed link on the next tick.
RQL_EnsureScanTip()
RQL_UI.scanTip:ClearLines()
RQL_UI.scanTip:SetHyperlink("item:" .. id .. ":0:0:0")
if RQL_TryInsertItemLink(id) then return end
-- Not cached yet -- schedule delayed insertion.
RQL_UI.iLinkPendingFor = id
RQL_UI.iLinkPendingElapsed = 0
RQL_UI.iLinkPendingTries = 0
if DEFAULT_CHAT_FRAME then
DEFAULT_CHAT_FRAME:AddMessage("|cFF33FF99RQL|r Fetching item data from server, link will drop into chat in a moment...")
end
end
-- ---------- minimap button ----------
local function RQL_SaveMinimapPos()
if not RQL_UI.minimap then return end
RelationshipsQuestAndItemBrowserDB.minimap = RelationshipsQuestAndItemBrowserDB.minimap or {}
local x, y = RQL_UI.minimap:GetCenter()
if x and y then
RelationshipsQuestAndItemBrowserDB.minimap.x = x
RelationshipsQuestAndItemBrowserDB.minimap.y = y
end
end
local function RQL_RestoreMinimapPos()
if not RQL_UI.minimap then return end
local pos = RelationshipsQuestAndItemBrowserDB.minimap
RQL_UI.minimap:ClearAllPoints()
if pos and pos.x and pos.y then
RQL_UI.minimap:SetPoint("CENTER", UIParent, "BOTTOMLEFT", pos.x, pos.y)
else
RQL_UI.minimap:SetPoint("TOPLEFT", Minimap, "TOPLEFT", -8, -8)
end
end
local function RQL_BuildMinimapButton()
if RQL_UI.minimap then return end
local btn = CreateFrame("Button", "RelationshipsQuestAndItemBrowserMinimap", UIParent)
btn:SetFrameStrata("MEDIUM")
btn:SetWidth(32) btn:SetHeight(32)
btn:SetMovable(true)
btn:EnableMouse(true)
btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
btn:RegisterForDrag("LeftButton")
local icon = btn:CreateTexture(nil, "BACKGROUND")
icon:SetTexture("Interface\\Icons\\INV_Misc_Book_09")
icon:SetWidth(20) icon:SetHeight(20)
icon:SetPoint("CENTER", btn, "CENTER", 0, 1)
local border = btn:CreateTexture(nil, "OVERLAY")
border:SetTexture("Interface\\Minimap\\MiniMap-TrackingBorder")
border:SetWidth(54) border:SetHeight(54)
border:SetPoint("TOPLEFT", btn, "TOPLEFT", 0, 0)
local hl = btn:CreateTexture(nil, "HIGHLIGHT")
hl:SetTexture("Interface\\Minimap\\UI-Minimap-ZoomButton-Highlight")
hl:SetBlendMode("ADD")
hl:SetAllPoints(btn)
btn:SetHighlightTexture(hl)
btn:SetScript("OnClick", function()
if arg1 == "RightButton" then
RelationshipsQuestAndItemBrowserDB.minimap = nil
RQL_RestoreMinimapPos()
else
if RelationshipsQuestAndItemBrowserFrame:IsVisible() then
RelationshipsQuestAndItemBrowserFrame:Hide()
else
RQL_ShowBrowser()
end
end
end)
btn:SetScript("OnDragStart", function() this:StartMoving() end)
btn:SetScript("OnDragStop", function() this:StopMovingOrSizing(); RQL_SaveMinimapPos() end)
btn:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_LEFT")
GameTooltip:AddLine("Relationships Quest And Item Browser")
GameTooltip:AddLine("Left-click: open browser", 1,1,1)
GameTooltip:AddLine("Right-click: reset icon to minimap", 1,1,1)
GameTooltip:AddLine("Drag: move anywhere", 1,1,1)
GameTooltip:Show()
end)
btn:SetScript("OnLeave", function() GameTooltip:Hide() end)
RQL_UI.minimap = btn
RQL_RestoreMinimapPos()
end
-- ---------- chat link plus-button overlay ----------
--
-- Behaviour:
-- * A tiny green [+] Button is overlaid directly on top of every quest /
-- item hyperlink that is currently visible in a chat frame.
-- * The [+] tracks the link as chat scrolls: it appears when the message
-- is on screen and hides when it scrolls off. Switching chat tabs also
-- hides / shows it, since inactive tabs' frames are hidden.
-- * Clicking the [+] opens the quest / item in the RQL browser.
-- * The original chat link is untouched, so default Blizzard tooltip,
-- quest popup and shift-insert still work.
--
-- Why an overlay button instead of an inline "|Hrqlopen:..." link:
-- Some Turtle / OctoWoW builds don't dispatch clicks for unknown link
-- types AND refuse to let addons attach an OnHyperlinkShow script to
-- chat frames. A real Button widget floated over the FontString is the
-- only mechanism that reliably works on those clients.
local RQL_PLUS_BTN_SIZE = 12
local RQL_PLUS_POOL = {}
local RQL_PoolIndex = 0
local RQL_PLUS_MEASURE -- hidden FontString reused to measure prefix widths
local function RQL_GetMeasurer(refFontString)
if not RQL_PLUS_MEASURE then
RQL_PLUS_MEASURE = UIParent:CreateFontString(nil, "BACKGROUND")
RQL_PLUS_MEASURE:Hide()
end
local obj = refFontString and refFontString.GetFontObject and refFontString:GetFontObject()
if obj then
RQL_PLUS_MEASURE:SetFontObject(obj)
else
-- Fall back to font path / size directly.
local path, size, flags
if refFontString and refFontString.GetFont then path, size, flags = refFontString:GetFont() end
if path and size then RQL_PLUS_MEASURE:SetFont(path, size, flags) end
end
return RQL_PLUS_MEASURE
end
local function RQL_StripCodes(s)
if type(s) ~= "string" then return "" end
s = string.gsub(s, "|c%x%x%x%x%x%x%x%x", "")
s = string.gsub(s, "|r", "")
s = string.gsub(s, "|T[^|]*|t", "")
-- Strip hyperlink openers/closers but keep the display text between them,
-- since that text renders and consumes width in the source FontString.
s = string.gsub(s, "|H[^|]*|h", "")
s = string.gsub(s, "|h", "")
return s
end
local function RQL_PlusOnClick()
local e = this and this.rqlEntry
if not e then return end
if e.kind == "quest" then
RQL_ShowQuest(e.id, e.level or 0, e.name)
elseif e.kind == "item" then
RQL_ShowItem(e.id, e.name)
end
end
local function RQL_PlusOnEnter()
if not (this and this.rqlEntry) then return end
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
if this.rqlEntry.kind == "quest" then
GameTooltip:AddLine("Open quest in RQL browser")
else
GameTooltip:AddLine("Open item in RQL browser")
end
if this.rqlEntry.name then
GameTooltip:AddLine(this.rqlEntry.name, 1, 1, 1)
end
GameTooltip:Show()
end
local function RQL_PlusOnLeave() GameTooltip:Hide() end
local function RQL_AcquirePlusButton()
RQL_PoolIndex = RQL_PoolIndex + 1
local btn = RQL_PLUS_POOL[RQL_PoolIndex]
if not btn then
btn = CreateFrame("Button", "RQL_PlusButton" .. RQL_PoolIndex, UIParent)
btn:SetFrameStrata("HIGH")
btn:SetWidth(RQL_PLUS_BTN_SIZE)
btn:SetHeight(RQL_PLUS_BTN_SIZE)
local fs = btn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
fs:SetAllPoints(btn)
fs:SetText("|cff33ff99+|r")
btn:SetFontString(fs)
local bg = btn:CreateTexture(nil, "BACKGROUND")
bg:SetAllPoints(btn)
bg:SetTexture(0, 0, 0, 0.6)
local hl = btn:CreateTexture(nil, "HIGHLIGHT")
hl:SetAllPoints(btn)
hl:SetTexture(1, 1, 1, 0.30)
btn:SetHighlightTexture(hl)
btn:SetScript("OnClick", RQL_PlusOnClick)
btn:SetScript("OnEnter", RQL_PlusOnEnter)
btn:SetScript("OnLeave", RQL_PlusOnLeave)
RQL_PLUS_POOL[RQL_PoolIndex] = btn
end
return btn
end
local function RQL_HideUnusedButtons()
local i = RQL_PoolIndex + 1
while RQL_PLUS_POOL[i] do
RQL_PLUS_POOL[i]:Hide()
RQL_PLUS_POOL[i].rqlEntry = nil
i = i + 1
end
end
-- Word-wrap `plain` using the same width the FontString wraps at, and
-- return (lineCount, lastLineWidth, lineHeight). Used so the [+] follows a
-- message onto its wrapped continuation line instead of flying off the right
-- edge of the chat frame at the end of the (never-rendered) full-width line.
local function RQL_WrapMetrics(fs, plain)
local measurer = RQL_GetMeasurer(fs)
local _, fontSize = fs:GetFont()
fontSize = fontSize or 12
local spacing = 0
if fs.GetSpacing then spacing = fs:GetSpacing() or 0 end
local lineHeight = fontSize + spacing
local maxW = 0
if fs.GetWidth then maxW = fs:GetWidth() or 0 end
if maxW <= 0 then
local L, R = fs:GetLeft(), fs:GetRight()
if L and R then maxW = R - L end
end
measurer:SetText(plain or "")
local totalW = measurer:GetStringWidth() or 0
if maxW <= 0 or totalW <= maxW then
return 1, totalW, lineHeight
end
local lines = 1
local lineText = ""
local lineW = totalW
local i = 1
local len = string.len(plain)
while i <= len do
local sp = string.find(plain, " ", i, true)
local word
if sp then
word = string.sub(plain, i, sp)
i = sp + 1
else
word = string.sub(plain, i)
i = len + 1
end
local candidate = lineText .. word
measurer:SetText(candidate)
local cw = measurer:GetStringWidth() or 0
if cw > maxW and lineText ~= "" then
lines = lines + 1
lineText = word
measurer:SetText(lineText)
lineW = measurer:GetStringWidth() or 0
else
lineText = candidate
lineW = cw
end
end
return lines, lineW, lineHeight
end
-- Place a [+] button at the end of the message's LAST rendered line (which
-- may be a wrapped continuation, not the source string's true end). Multiple
-- [+] icons for a message stagger horizontally past that anchor, and wrap
-- down to a further line if they would otherwise exit the chat frame's
-- right edge, so nothing gets clipped or pushed off the frame.
local function RQL_PlaceButton(fs, anchorText, entry, frame, stagger)
if not fs or not fs.GetLeft then return end
local left = fs:GetLeft()
local top = fs:GetTop()
if not left or not top then return end
local frameLeft = frame and frame:GetLeft()
local frameRight = frame and frame:GetRight()
local plain = RQL_StripCodes(anchorText)
local lines, lastW, lineH = RQL_WrapMetrics(fs, plain)
local gap = 2
local step = RQL_PLUS_BTN_SIZE + 2
local baseX = left + lastW + gap
local baseY = top - 1 - (lines - 1) * lineH
local x = baseX + (stagger or 0) * step
local y = baseY
-- If the staggered [+] would run past the chat frame's right edge,
-- wrap it to a new line beneath the message rather than clipping it.
if frameRight and (x + RQL_PLUS_BTN_SIZE) > frameRight then
local firstRowSlots = math.floor((frameRight - baseX) / step)
if firstRowSlots < 0 then firstRowSlots = 0 end
local wrapLeft = frameLeft or left
local wrapRowSlots = math.floor((frameRight - wrapLeft) / step)
if wrapRowSlots < 1 then wrapRowSlots = 1 end
local s = stagger or 0
if s < firstRowSlots then
x = baseX + s * step
y = baseY
else
local over = s - firstRowSlots
local row = 1 + math.floor(over / wrapRowSlots)
local col = over - (row - 1) * wrapRowSlots
x = wrapLeft + col * step
y = baseY - row * step
end
end
if frameLeft and x < frameLeft then x = frameLeft end
local btn = RQL_AcquirePlusButton()
btn:ClearAllPoints()
-- Position from UIParent BOTTOMLEFT using the FontString's absolute top.
btn:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", x, y)
btn.rqlEntry = entry
btn:Show()
end
local function RQL_ProcessChatFrame(frame)
if not frame or not frame.IsVisible or not frame:IsVisible() then return end
if not frame.GetRegions then return end
local regions = { frame:GetRegions() }
local n = table.getn(regions)
for i = 1, n do
local r = regions[i]
if r and r.GetObjectType and r:GetObjectType() == "FontString"
and r.IsVisible and r:IsVisible() then
local t = r:GetText()
if t and string.find(t, "|H", 1, 1) then
-- Collect every quest/item link with its span in `t`, then
-- place one [+] per link. Position rule:
-- * link is followed by more text in the same line -> put
-- the [+] after the FULL message text (last character)
-- * link is at the very end of the message -> put
-- the [+] immediately after the link's display text
local matches = {}
local pos = 1
while 1 do
local s, e, id, lvl, name = string.find(t,
"|Hquest:(%d+):(%-?%d+)|h%[([^%]]*)%]|h", pos)
if not s then break end
table.insert(matches, {
s = s, e = e,
entry = { kind = "quest", id = tonumber(id),
level = tonumber(lvl), name = name },
})
pos = e + 1
end
pos = 1
while 1 do
local s, e, id, name = string.find(t,
"|Hitem:(%d+)[^|]*|h%[([^%]]*)%]|h", pos)
if not s then break end
table.insert(matches, {
s = s, e = e,
entry = { kind = "item", id = tonumber(id), name = name },
})
pos = e + 1
end
table.sort(matches, function(a, b) return a.s < b.s end)
-- Place all [+] buttons grouped together at the end of the
-- message (after the last character / link) so multiple linked
-- quests / items get a run of plus icons like "+++" rather
-- than being spread throughout the line. `stagger` shifts each
-- subsequent button one icon-width to the right.
for j = 1, table.getn(matches) do
RQL_PlaceButton(r, t, matches[j].entry, frame, j - 1)
end
end
end
end
end
-- Event-driven [+] scanning.
--
-- The old implementation ran a full chat-frame scan ~33 times per second
-- (every 0.03s) regardless of whether chat had changed. On busy zones /
-- raids that constant `frame:GetRegions()` + regex work was a real source
-- of random lag spikes (GC pressure from throwaway region tables).
--
-- Now we scan only when:
-- * a CHAT_MSG_* event arrives (with a tiny delay so the chat frame has
-- laid out the new region before we measure it), or
-- * the user scrolls / resizes a chat frame (hooked below), or
-- * the addon just finished loading (initial pass over existing chat).
--
-- Scans are rate-limited to at most one every RQL_SCAN_MIN_INTERVAL.
-- Near-instant update: [+] icons must track new chat lines the same frame
-- they render. A 0.25s throttle was visibly slow when messages scrolled up
-- and the old [+] drifted with them. We now scan on the very next frame.
-- Tiny throttle: enough to coalesce bursts of chat events (raids, spammy
-- channels) into a single rescan per frame-ish window, but small enough
-- that the [+] still tracks new lines effectively instantly to the eye.
local RQL_SCAN_MIN_INTERVAL = 0.03
local RQL_SCAN_DELAY = 0.01
local RQL_SCAN_PENDING_AT = nil
local RQL_SCAN_LAST_AT = 0
function RQL_SchedulePlusScan(delay)
local now = GetTime()
local at = now + (delay or RQL_SCAN_DELAY)
local earliest = RQL_SCAN_LAST_AT + RQL_SCAN_MIN_INTERVAL
if at < earliest then at = earliest end
if not RQL_SCAN_PENDING_AT or at < RQL_SCAN_PENDING_AT then
RQL_SCAN_PENDING_AT = at
end
end
-- Called every frame from the main OnUpdate. Cheap when idle: two compares
-- and an early return. Only does real work when a scan is due.
function RQL_PlusTick(elapsed)
if not RQL_SCAN_PENDING_AT then return end
if GetTime() < RQL_SCAN_PENDING_AT then return end
RQL_SCAN_PENDING_AT = nil
RQL_SCAN_LAST_AT = GetTime()
RQL_PoolIndex = 0
local maxFrames = NUM_CHAT_WINDOWS or 7
for i = 1, maxFrames do
local f = getglobal("ChatFrame" .. i)
-- IsVisible() is false for docked tabs that aren't the front tab
-- and for hidden / minimized chat windows, so we naturally skip
-- all of those.
if f and f.IsVisible and f:IsVisible() then
RQL_ProcessChatFrame(f)
end
end
RQL_HideUnusedButtons()
end
-- Watcher frame that listens for chat traffic and requests a rescan.
local RQL_ChatWatch = CreateFrame("Frame", "RQL_ChatWatch")
local RQL_CHAT_EVENTS = {
"CHAT_MSG_SAY", "CHAT_MSG_YELL", "CHAT_MSG_EMOTE", "CHAT_MSG_TEXT_EMOTE",
"CHAT_MSG_WHISPER", "CHAT_MSG_WHISPER_INFORM",
"CHAT_MSG_PARTY", "CHAT_MSG_RAID", "CHAT_MSG_RAID_LEADER", "CHAT_MSG_RAID_WARNING",
"CHAT_MSG_GUILD", "CHAT_MSG_OFFICER",
"CHAT_MSG_CHANNEL",
"CHAT_MSG_SYSTEM", "CHAT_MSG_LOOT", "CHAT_MSG_TRADESKILLS",
"CHAT_MSG_BATTLEGROUND", "CHAT_MSG_BATTLEGROUND_LEADER",
}
for i = 1, table.getn(RQL_CHAT_EVENTS) do
RQL_ChatWatch:RegisterEvent(RQL_CHAT_EVENTS[i])
end
RQL_ChatWatch:SetScript("OnEvent", function()
RQL_SchedulePlusScan()
end)
-- Immediately hide every [+] button in the pool. Used on tab-switch so a
-- stale icon from the previous tab disappears the instant the user clicks
-- a new tab, without waiting for the next scan tick.
local function RQL_HideAllPlusButtons()
local i = 1
while RQL_PLUS_POOL[i] do
RQL_PLUS_POOL[i]:Hide()
RQL_PLUS_POOL[i].rqlEntry = nil
i = i + 1
end
RQL_PoolIndex = 0
end
local function RQL_HookChatLinks()
-- Chat scrolling / paging moves existing linked lines but doesn't fire
-- an event. Wrap the standard scroll helpers so [+] follows the text.
local names = { "ScrollUp", "ScrollDown", "ScrollToTop",
"ScrollToBottom", "PageUp", "PageDown" }
local maxFrames = NUM_CHAT_WINDOWS or 7
for i = 1, maxFrames do
local f = getglobal("ChatFrame" .. i)
if f then
for j = 1, table.getn(names) do
local n = names[j]
local flag = "_rql_hooked_" .. n
if f[n] and not f[flag] then
local orig = f[n]
f[flag] = orig
f[n] = function()
orig(f)
RQL_SchedulePlusScan()
end
end
end
end
end
-- Hook chat tab clicks so switching tabs instantly clears any stale [+]
-- from the previously-visible tab and re-scans the newly-visible one.
-- Also hook OnShow/OnHide on the frames themselves as a safety net for
-- undock/dock and other visibility changes.
for i = 1, maxFrames do
local tab = getglobal("ChatFrame" .. i .. "Tab")
if tab and not tab._rql_hooked_click then
local origClick = tab:GetScript("OnClick")
tab._rql_hooked_click = true
tab:SetScript("OnClick", function()
if origClick then origClick() end
RQL_HideAllPlusButtons()
RQL_SchedulePlusScan(0)
end)
end
local f = getglobal("ChatFrame" .. i)
if f and not f._rql_hooked_vis then
f._rql_hooked_vis = true
local origShow = f:GetScript("OnShow")
f:SetScript("OnShow", function()
if origShow then origShow() end
RQL_HideAllPlusButtons()
RQL_SchedulePlusScan(0)
end)
local origHide = f:GetScript("OnHide")
f:SetScript("OnHide", function()
if origHide then origHide() end
RQL_HideAllPlusButtons()
RQL_SchedulePlusScan(0)
end)
end
end
-- Do one pass shortly after login so already-visible chat gets [+].
RQL_SchedulePlusScan(0.3)
end
-- ---------- events / slash ----------
RelationshipsQuestAndItemBrowser:SetScript("OnEvent", function()
if event == "ADDON_LOADED" and arg1 == "RelationshipsQuestAndItemBrowser" then
RelationshipsQuestAndItemBrowserDB = RelationshipsQuestAndItemBrowserDB or {}
elseif event == "PLAYER_LOGOUT" then
if RQL_UI and RQL_UI.minimap then
RelationshipsQuestAndItemBrowserDB = RelationshipsQuestAndItemBrowserDB or {}
RelationshipsQuestAndItemBrowserDB.minimap = RelationshipsQuestAndItemBrowserDB.minimap or {}
local cx, cy = RQL_UI.minimap:GetCenter()
if cx and cy then
RelationshipsQuestAndItemBrowserDB.minimap.x = cx
RelationshipsQuestAndItemBrowserDB.minimap.y = cy
end
end
elseif event == "PLAYER_LOGIN" then
RQL_HookChatLinks()
RQL_BuildUI()
RQL_BuildMinimapButton()
-- Item-detail async tooltip retry ticker.
RelationshipsQuestAndItemBrowser:SetScript("OnUpdate", function()
RQL_ItemDetailRetryTick(arg1)
if RQL_PlusTick then RQL_PlusTick(arg1) end
end)
if RQL_ApplyBackground then
RQL_ApplyBackground(RQL_GetStartupBackground())
end
if DEFAULT_CHAT_FRAME then
DEFAULT_CHAT_FRAME:AddMessage("|cFF33FF99Relationships Quest And Item Browser|r loaded. Click quest links, /rql, or use the minimap book icon.")
end
end
end)
local function RQL_SetFilterFromWord(kind, value)
value = string.lower(value or "")
if kind == "level" then
RQL_LEVEL_FILTER = 0
if value == "1-9" or value == "1" then RQL_LEVEL_FILTER = 1
elseif value == "10-19" or value == "10" then RQL_LEVEL_FILTER = 2
elseif value == "20-29" or value == "20" then RQL_LEVEL_FILTER = 3
elseif value == "30-39" or value == "30" then RQL_LEVEL_FILTER = 4
elseif value == "40-49" or value == "40" then RQL_LEVEL_FILTER = 5
elseif value == "50-59" or value == "50" then RQL_LEVEL_FILTER = 6
elseif value == "60" or value == "60+" then RQL_LEVEL_FILTER = 7 end
elseif kind == "type" then
RQL_TYPE_FILTER = 0
if value == "normal" then RQL_TYPE_FILTER = 1
elseif value == "elite" or value == "group" then RQL_TYPE_FILTER = 2
elseif value == "dungeon" or value == "raid" then RQL_TYPE_FILTER = 3
elseif value == "pvp" then RQL_TYPE_FILTER = 4
elseif value == "class" then RQL_TYPE_FILTER = 5
elseif value == "profession" or value == "skill" then RQL_TYPE_FILTER = 6
elseif value == "event" or value == "seasonal" then RQL_TYPE_FILTER = 7 end
elseif kind == "faction" then
RQL_FACTION_FILTER = 0
if value == "alliance" or value == "a" then RQL_FACTION_FILTER = 1
elseif value == "horde" or value == "h" then RQL_FACTION_FILTER = 2
elseif value == "neutral" or value == "n" or value == "both" then RQL_FACTION_FILTER = 3 end
elseif kind == "source" then
RQL_SOURCE_FILTER = 0
if value == "vanilla" then RQL_SOURCE_FILTER = 1
elseif value == "turtle" or value == "octo" or value == "octowow" then RQL_SOURCE_FILTER = 2 end
end
end
SLASH_RELATIONSHIPSQUESTANDITEMBROWSER1 = "/rql"
SLASH_RELATIONSHIPSQUESTANDITEMBROWSER2 = "/questbrowser"
SLASH_RELATIONSHIPSQUESTANDITEMBROWSER3 = "/rqib"
SLASH_RELATIONSHIPSQUESTANDITEMBROWSER4 = "/questitembrowser"
SlashCmdList["RELATIONSHIPSQUESTANDITEMBROWSER"] = function(msg)
msg = msg or ""
local _, _, cmd, rest = string.find(msg, "^%s*(%S+)%s*(.-)%s*$")
cmd = cmd and string.lower(cmd) or ""
if cmd == "" then
RQL_ShowBrowser()
elseif cmd == "close" or cmd == "hide" then
RelationshipsQuestAndItemBrowserFrame:Hide()
elseif cmd == "search" then
RQL_SEARCH_TEXT = rest or ""
if RQL_UI.search then RQL_UI.search:SetText(RQL_SEARCH_TEXT) end
RQL_ShowBrowser()
elseif cmd == "level" or cmd == "type" or cmd == "faction" or cmd == "source" then
RQL_SetFilterFromWord(cmd, rest)
RQL_ShowBrowser()
elseif cmd == "reset" then
RQL_SEARCH_TEXT = ""
RQL_LEVEL_FILTER = 0; RQL_TYPE_FILTER = 0
RQL_FACTION_FILTER = 0; RQL_SOURCE_FILTER = 0
if RQL_UI.search then RQL_UI.search:SetText("") end
RQL_ShowBrowser()
elseif cmd == "quest" and rest then
local id = tonumber(rest)
if id then RQL_ShowQuest(id, 0, nil) end
elseif cmd == "items" then
RQL_ITEM_SEARCH_TEXT = rest or ""
if RQL_UI.itemSearch then RQL_UI.itemSearch:SetText(RQL_ITEM_SEARCH_TEXT) end
RQL_ShowItemBrowser()
elseif cmd == "item" and rest then
local id = tonumber(rest)
if id then RQL_ShowItem(id, nil) end
else
DEFAULT_CHAT_FRAME:AddMessage("|cFF33FF99RQL|r usage:")
DEFAULT_CHAT_FRAME:AddMessage(" /rql open browser")
DEFAULT_CHAT_FRAME:AddMessage(" /rql search <text> search")
DEFAULT_CHAT_FRAME:AddMessage(" /rql level 20-29 filter level")
DEFAULT_CHAT_FRAME:AddMessage(" /rql type dungeon filter type")
DEFAULT_CHAT_FRAME:AddMessage(" /rql faction alliance filter faction")
DEFAULT_CHAT_FRAME:AddMessage(" /rql source turtle filter source")
DEFAULT_CHAT_FRAME:AddMessage(" /rql quest <id> show a specific quest")
DEFAULT_CHAT_FRAME:AddMessage(" /rql items <text> open item browser and search")
DEFAULT_CHAT_FRAME:AddMessage(" /rql item <id> show a specific item")
DEFAULT_CHAT_FRAME:AddMessage(" /rql reset reset filters")
end
end