1044 lines
35 KiB
Lua
1044 lines
35 KiB
Lua
--[[ RelationshipsAuctionPrice
|
|
Fast AH scanner + tooltip showing prices high->low.
|
|
Slash: /rap scan | /rap fast | /rap stop | /rap clear | /rap show <id> | /rap stats
|
|
/rap name <substr> -> next scan only matches this name
|
|
/rap class <index|clear> -> next scan filters by class index
|
|
/rap export -> dumps PriceData.lua-shaped string to SavedVariables
|
|
]]--
|
|
|
|
RelationshipsAuctionPriceDB = RelationshipsAuctionPriceDB or { items = {}, lastScan = 0, realm = nil }
|
|
|
|
local db
|
|
local scanning = false
|
|
local scanPage = 0
|
|
local scanStart = 0
|
|
local scanMode = "full"
|
|
local pendingQuery = false
|
|
local queryTime = 0
|
|
local scanSeen = 0
|
|
local scanSaved = 0
|
|
local scanNoLink = 0
|
|
local scanNoBuyout = 0
|
|
local scanExpectedTotal = 0
|
|
local NUM_PER_PAGE = 50
|
|
local THROTTLE = 0.35
|
|
local MAX_WAIT = 20.0
|
|
local MAX_RETRIES = 3
|
|
local scanRetries = 0
|
|
|
|
-- current scan filter (rebuilt on BeginScan)
|
|
local filterName = "" -- substring; "" = any
|
|
local filterClass = nil -- class index into {GetAuctionItemClasses()} or nil
|
|
local filterSubclass = nil
|
|
local filterQuality = 0 -- 0 = any (poor+)
|
|
local filterMinLvl = nil
|
|
local filterMaxLvl = nil
|
|
|
|
-- ============ USER CONFIG: AH UI layout ================================
|
|
-- The addon lives in its own little panel that attaches just above the
|
|
-- Auction House frame. Tweak the panel to move the whole box; tweak the
|
|
-- child elements to nudge things inside the box.
|
|
--
|
|
-- Anchor points: "TOPLEFT","TOP","TOPRIGHT","LEFT","CENTER","RIGHT",
|
|
-- "BOTTOMLEFT","BOTTOM","BOTTOMRIGHT"
|
|
-- relativeTo : "AuctionFrame" | "panel" | "scanBtn" | "classDD" | "nameBox"
|
|
-- x, y : pixel offset (positive x = right, positive y = up)
|
|
--
|
|
-- After editing, /reload in-game.
|
|
RAP_UI_CONFIG = {
|
|
panel = { -- the little box attached above the AH
|
|
point = "BOTTOMRIGHT",
|
|
relativeTo = "AuctionFrame",
|
|
relativePoint = "TOPRIGHT",
|
|
x = 1.5, y = -17, -- negative y overlaps onto the AH top so the box "connects"
|
|
width = 403.1, height = 68,
|
|
padding = 0.5,
|
|
|
|
bgColor = { 0.20, 0.19, 0.16, 90.00 },
|
|
borderColor = { 0.85, 0.85, 0.87, 5.00 },
|
|
},
|
|
scanButton = {
|
|
point = "RIGHT",
|
|
relativeTo = "panel",
|
|
relativePoint = "RIGHT",
|
|
x = -13, y = 2,
|
|
width = 70, height = 22,
|
|
},
|
|
classDropdown = {
|
|
point = "RIGHT",
|
|
relativeTo = "scanBtn",
|
|
relativePoint = "LEFT",
|
|
x = -7, y = -2,
|
|
width = 120, -- dropdown menu width
|
|
},
|
|
nameBox = {
|
|
point = "RIGHT",
|
|
relativeTo = "classDD",
|
|
relativePoint = "LEFT",
|
|
x = -8, y = 2,
|
|
width = 120, height = 20,
|
|
},
|
|
nameLabel = { -- "Name filter" caption under the name box, centered
|
|
point = "TOP",
|
|
relativeTo = "nameBox",
|
|
relativePoint = "BOTTOM",
|
|
x = -3, y = -5,
|
|
text = "Name filter",
|
|
},
|
|
statusText = { -- status line under the scan button
|
|
point = "TOPRIGHT",
|
|
relativeTo = "scanBtn",
|
|
relativePoint = "BOTTOMRIGHT",
|
|
x = -11, y = -5,
|
|
},
|
|
}
|
|
-- =======================================================================
|
|
|
|
-- ============ util =====================================================
|
|
|
|
local function Print(msg)
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cff66ccffRAP|r: "..tostring(msg))
|
|
end
|
|
|
|
local function ItemIDFromLink(link)
|
|
if not link then return nil end
|
|
local _,_,id = string.find(link, "item:(%d+)")
|
|
if id then return tonumber(id) end
|
|
return nil
|
|
end
|
|
|
|
local function GetRealmKey()
|
|
local r = GetRealmName() or "Unknown"
|
|
local f = (UnitFactionGroup and UnitFactionGroup("player")) or "N"
|
|
return r.."-"..string.sub(f,1,1)
|
|
end
|
|
|
|
local function EnsureRealm()
|
|
local key = GetRealmKey()
|
|
if db.realm ~= key then
|
|
db.realm = key
|
|
db.items = db.items or {}
|
|
end
|
|
db.items[key] = db.items[key] or {}
|
|
return db.items[key]
|
|
end
|
|
|
|
local function FmtMoney(c)
|
|
if not c or c <= 0 then return "0c" end
|
|
local g = floor(c / 10000)
|
|
local s = floor(mod(c, 10000) / 100)
|
|
local cc = mod(c, 100)
|
|
local out = ""
|
|
if g > 0 then out = out..g.."|cffffd700g|r " end
|
|
if s > 0 or g > 0 then out = out..s.."|cffc7c7cfs|r " end
|
|
out = out..cc.."|cffeda55fc|r"
|
|
return out
|
|
end
|
|
|
|
-- ============ storage ==================================================
|
|
|
|
local function StorePriceIn(store, itemID, buyout, count, seller)
|
|
if not store or not itemID or not buyout or buyout <= 0 or not count or count <= 0 then return false end
|
|
store[itemID] = store[itemID] or { scans = {} }
|
|
local unit = floor(buyout / count)
|
|
table.insert(store[itemID].scans, { u = unit, q = count, s = seller or "?" })
|
|
return true
|
|
end
|
|
|
|
local function StorePrice(itemID, buyout, count, seller)
|
|
return StorePriceIn(EnsureRealm(), itemID, buyout, count, seller)
|
|
end
|
|
|
|
-- Filtered scans replace only the touched items. Full scans are written to a
|
|
-- temporary buffer first, then swapped in only when the scan completes cleanly.
|
|
-- This prevents a failed full scan from wiping good saved data into empty shells.
|
|
local scanTouched -- set of itemIDs seen this scan when filtered
|
|
local scanFull
|
|
local scanBuffer
|
|
local scanResume -- true when the current run is resuming a prior scan
|
|
|
|
local function BeginScanStorage()
|
|
scanTouched = {}
|
|
scanFull = (filterName == "" and not filterClass and filterQuality == 0
|
|
and not filterMinLvl and not filterMaxLvl)
|
|
if scanResume then
|
|
-- resuming: append directly to the saved store. No wholesale wipe,
|
|
-- no per-item wipe. Duplicated rows on the boundary page are possible
|
|
-- but harmless (same market snapshot).
|
|
scanFull = false
|
|
scanTouched = nil
|
|
scanBuffer = nil
|
|
elseif scanFull then
|
|
scanBuffer = {}
|
|
else
|
|
scanBuffer = nil
|
|
end
|
|
end
|
|
|
|
local function StorePriceFiltered(itemID, buyout, count, seller)
|
|
if not itemID then return false end
|
|
if scanFull and scanBuffer then
|
|
return StorePriceIn(scanBuffer, itemID, buyout, count, seller)
|
|
end
|
|
local store = EnsureRealm()
|
|
if scanTouched and not scanTouched[itemID] then
|
|
-- first time this scan sees this id under a filter -> wipe its old rows
|
|
if store[itemID] then store[itemID].scans = {} end
|
|
scanTouched[itemID] = true
|
|
end
|
|
return StorePrice(itemID, buyout, count, seller)
|
|
end
|
|
|
|
local function CountRowsInStore(store)
|
|
local itemCount = 0
|
|
local listingCount = 0
|
|
if not store then return 0, 0 end
|
|
for id, entry in pairs(store) do
|
|
local n = 0
|
|
if entry.scans then n = table.getn(entry.scans) end
|
|
if n > 0 then
|
|
itemCount = itemCount + 1
|
|
listingCount = listingCount + n
|
|
end
|
|
end
|
|
return itemCount, listingCount
|
|
end
|
|
|
|
local function FinalizeScan(success)
|
|
local key = GetRealmKey()
|
|
local store
|
|
if scanFull and scanBuffer then
|
|
if success then
|
|
-- clean full scan: buffer replaces prior data wholesale
|
|
db.items[key] = scanBuffer
|
|
store = scanBuffer
|
|
else
|
|
-- partial/failed full scan: merge what we DID scan into existing
|
|
-- store so nothing that was captured this run is lost. Items we
|
|
-- didn't reach keep their previous prices.
|
|
store = db.items[key] or {}
|
|
db.items[key] = store
|
|
for id, entry in pairs(scanBuffer) do
|
|
if entry.scans and table.getn(entry.scans) > 0 then
|
|
store[id] = entry
|
|
end
|
|
end
|
|
end
|
|
else
|
|
store = EnsureRealm()
|
|
end
|
|
for id, entry in pairs(store) do
|
|
if entry.scans and table.getn(entry.scans) > 0 then
|
|
table.sort(entry.scans, function(a,b) return a.u > b.u end)
|
|
entry.updated = time()
|
|
end
|
|
end
|
|
if success then
|
|
db.lastScan = time()
|
|
db.resume = nil -- clean finish clears any resume checkpoint
|
|
end
|
|
local itemCount, listingCount = CountRowsInStore(store)
|
|
scanTouched = nil
|
|
scanBuffer = nil
|
|
scanFull = nil
|
|
scanResume = nil
|
|
return itemCount, listingCount
|
|
end
|
|
|
|
-- Save a resume checkpoint. Called after every successfully-read page so
|
|
-- the next `/rap continue` picks up right where we stopped.
|
|
local function SaveResumeCheckpoint()
|
|
if not db then return end
|
|
db.resume = {
|
|
page = scanPage + 1, -- next page to fetch
|
|
mode = scanMode,
|
|
filterName = filterName,
|
|
filterClass = filterClass,
|
|
filterSubclass = filterSubclass,
|
|
filterQuality = filterQuality,
|
|
filterMinLvl = filterMinLvl,
|
|
filterMaxLvl = filterMaxLvl,
|
|
expectedTotal = scanExpectedTotal,
|
|
savedAt = time(),
|
|
}
|
|
end
|
|
|
|
-- ============ shared data (PriceData.lua) ==============================
|
|
|
|
local function GetSharedRows(itemID)
|
|
if not RAP_SHARED_DATA then return nil end
|
|
local realm = RAP_SHARED_DATA[GetRealmKey()]
|
|
if not realm or not realm.items then return nil end
|
|
return realm.items[itemID], realm.updated
|
|
end
|
|
|
|
local function GetMergedRows(itemID)
|
|
local out = {}
|
|
local sharedRows, sharedUpdated = GetSharedRows(itemID)
|
|
if sharedRows then
|
|
for i=1, table.getn(sharedRows) do
|
|
local r = sharedRows[i]
|
|
table.insert(out, { u = r.u, q = r.q, src = "shared" })
|
|
end
|
|
end
|
|
local store = db.items[GetRealmKey()]
|
|
local localUpdated
|
|
if store and store[itemID] and store[itemID].scans then
|
|
localUpdated = store[itemID].updated
|
|
for i=1, table.getn(store[itemID].scans) do
|
|
local r = store[itemID].scans[i]
|
|
table.insert(out, { u = r.u, q = r.q, src = "local" })
|
|
end
|
|
end
|
|
if table.getn(out) == 0 then return nil end
|
|
table.sort(out, function(a,b) return a.u > b.u end)
|
|
return out, localUpdated, sharedUpdated
|
|
end
|
|
|
|
-- ============ tooltip ==================================================
|
|
|
|
local function TooltipAlreadyHasRAP(tt)
|
|
local n = tt:NumLines()
|
|
if not n or n <= 0 then return false end
|
|
local name = tt:GetName()
|
|
for i=1, n do
|
|
local fs = getglobal(name.."TextLeft"..i)
|
|
local txt = fs and fs:GetText()
|
|
if txt and string.find(txt, "AH Prices", 1, true) then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function AddTooltipLines(tt, itemID)
|
|
if TooltipAlreadyHasRAP(tt) then return end
|
|
local rows, localUpdated, sharedUpdated = GetMergedRows(itemID)
|
|
if not rows then return end
|
|
|
|
tt:AddLine(" ")
|
|
tt:AddLine("|cff66ccffAH Prices|r")
|
|
|
|
-- Collect unique prices (rows are already sorted high -> low)
|
|
local unique = {}
|
|
local seen = {}
|
|
for i=1, table.getn(rows) do
|
|
local r = rows[i]
|
|
if not seen[r.u] then
|
|
seen[r.u] = true
|
|
table.insert(unique, r)
|
|
end
|
|
end
|
|
|
|
local n = table.getn(unique)
|
|
local picks = {}
|
|
if n == 1 then
|
|
picks = { { label = "Price", row = unique[1] } }
|
|
elseif n == 2 then
|
|
picks = {
|
|
{ label = "High", row = unique[1] },
|
|
{ label = "Low", row = unique[2] },
|
|
}
|
|
elseif n >= 3 then
|
|
local midIdx = floor((n + 1) / 2)
|
|
picks = {
|
|
{ label = "High", row = unique[1] },
|
|
{ label = "Mid", row = unique[midIdx] },
|
|
{ label = "Low", row = unique[n] },
|
|
}
|
|
end
|
|
|
|
for i=1, table.getn(picks) do
|
|
local p = picks[i]
|
|
local r = p.row
|
|
local tag = (r.src == "shared") and " |cff888888(shared)|r" or ""
|
|
tt:AddDoubleLine(
|
|
" "..p.label..tag,
|
|
FmtMoney(r.u).." ea",
|
|
0.8,0.9,1, 1,1,1
|
|
)
|
|
end
|
|
if localUpdated then
|
|
local mins = floor((time() - localUpdated) / 60)
|
|
tt:AddLine(" local scan "..mins.."m ago", 0.6,0.6,0.6)
|
|
end
|
|
if sharedUpdated then
|
|
local hrs = floor((time() - sharedUpdated) / 3600)
|
|
tt:AddLine(" shared data "..hrs.."h old", 0.5,0.5,0.5)
|
|
end
|
|
tt:Show()
|
|
end
|
|
|
|
local function HookTip(tt)
|
|
local orig_SetHyperlink = tt.SetHyperlink
|
|
tt.SetHyperlink = function(self, link)
|
|
orig_SetHyperlink(self, link)
|
|
local id = ItemIDFromLink(link)
|
|
if id then AddTooltipLines(self, id) end
|
|
end
|
|
end
|
|
|
|
local function InstallTooltipHooks()
|
|
HookTip(GameTooltip)
|
|
if ItemRefTooltip then HookTip(ItemRefTooltip) end
|
|
|
|
local orig_ContainerFrameItemButton_OnEnter = ContainerFrameItemButton_OnEnter
|
|
ContainerFrameItemButton_OnEnter = function()
|
|
orig_ContainerFrameItemButton_OnEnter()
|
|
local bag = this:GetParent():GetID()
|
|
local slot = this:GetID()
|
|
local link = GetContainerItemLink(bag, slot)
|
|
local id = ItemIDFromLink(link)
|
|
if id then AddTooltipLines(GameTooltip, id) end
|
|
end
|
|
|
|
local orig_PaperDollItemSlotButton_OnEnter = PaperDollItemSlotButton_OnEnter
|
|
if orig_PaperDollItemSlotButton_OnEnter then
|
|
PaperDollItemSlotButton_OnEnter = function()
|
|
orig_PaperDollItemSlotButton_OnEnter()
|
|
local link = GetInventoryItemLink("player", this:GetID())
|
|
local id = ItemIDFromLink(link)
|
|
if id then AddTooltipLines(GameTooltip, id) end
|
|
end
|
|
end
|
|
|
|
if MerchantItemButton_OnEnter then
|
|
local orig_MerchantItemButton_OnEnter = MerchantItemButton_OnEnter
|
|
MerchantItemButton_OnEnter = function()
|
|
orig_MerchantItemButton_OnEnter()
|
|
local link = GetMerchantItemLink(this:GetID())
|
|
local id = ItemIDFromLink(link)
|
|
if id then AddTooltipLines(GameTooltip, id) end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- ============ scanner ==================================================
|
|
|
|
local scanner = CreateFrame("Frame")
|
|
scanner:Hide()
|
|
|
|
local function StartQuery(page)
|
|
scanPage = page
|
|
pendingQuery = true
|
|
queryTime = GetTime()
|
|
-- name, minLvl, maxLvl, invType, class, subclass, page, isUsable, qualityIndex
|
|
QueryAuctionItems(
|
|
filterName or "",
|
|
filterMinLvl, filterMaxLvl,
|
|
nil,
|
|
filterClass, filterSubclass,
|
|
page, 0, filterQuality or 0
|
|
)
|
|
end
|
|
|
|
local function ReadCurrentPage()
|
|
local batch, total = GetNumAuctionItems("list")
|
|
if not batch or batch == 0 then return 0, total or 0 end
|
|
for i=1, batch do
|
|
local name, _, count, _, _, _, minBid, minInc, buyout, bidAmount, highBidder, owner =
|
|
GetAuctionItemInfo("list", i)
|
|
scanSeen = scanSeen + 1
|
|
local link = GetAuctionItemLink("list", i)
|
|
local id = ItemIDFromLink(link)
|
|
if not id then
|
|
scanNoLink = scanNoLink + 1
|
|
elseif not buyout or buyout <= 0 then
|
|
scanNoBuyout = scanNoBuyout + 1
|
|
elseif StorePriceFiltered(id, buyout, count or 1, owner) then
|
|
scanSaved = scanSaved + 1
|
|
end
|
|
end
|
|
return batch, total or 0
|
|
end
|
|
|
|
local function StopScan(reason)
|
|
scanning = false
|
|
pendingQuery = false
|
|
scanner:Hide()
|
|
local ok = (reason == "ok")
|
|
-- We no longer discard a full scan's captured rows just because it
|
|
-- didn't finish cleanly. On a clean finish we swap the buffer in
|
|
-- wholesale; on a partial/failed finish FinalizeScan merges what we DID
|
|
-- capture into the existing saved store, so nothing is lost.
|
|
local itemCount, listingCount = FinalizeScan(ok)
|
|
local elapsed = GetTime() - scanStart
|
|
Print(string.format("scan done (%s) in %.1fs.", reason or "ok", elapsed))
|
|
Print("saved "..listingCount.." listings across "..itemCount.." item IDs. Seen "..scanSeen.." rows; skipped "..scanNoBuyout.." no-buyout, "..scanNoLink.." no-link.")
|
|
if not ok then Print("partial scan: captured rows were merged into your saved data.") end
|
|
end
|
|
|
|
scanner:SetScript("OnUpdate", function()
|
|
if not scanning then return end
|
|
local now = GetTime()
|
|
if pendingQuery then
|
|
if now - queryTime > MAX_WAIT then
|
|
scanRetries = scanRetries + 1
|
|
if scanRetries > MAX_RETRIES then
|
|
Print("page "..scanPage.." timed out after "..MAX_RETRIES.." retries, stopping.")
|
|
StopScan("timeout"); return
|
|
end
|
|
Print("page "..scanPage.." slow, retry "..scanRetries.."/"..MAX_RETRIES.."...")
|
|
pendingQuery = false
|
|
queryTime = now
|
|
if CanSendAuctionQuery() then StartQuery(scanPage) end
|
|
return
|
|
end
|
|
return
|
|
end
|
|
local wait = (scanMode == "fast") and 0.15 or THROTTLE
|
|
if now - queryTime < wait then return end
|
|
if CanSendAuctionQuery() then
|
|
scanRetries = 0
|
|
StartQuery(scanPage + 1)
|
|
end
|
|
end)
|
|
|
|
scanner:RegisterEvent("AUCTION_ITEM_LIST_UPDATE")
|
|
scanner:SetScript("OnEvent", function()
|
|
if not scanning or not pendingQuery then return end
|
|
pendingQuery = false
|
|
scanRetries = 0
|
|
queryTime = GetTime()
|
|
local batch, total = ReadCurrentPage()
|
|
if total and total > scanExpectedTotal then scanExpectedTotal = total end
|
|
local pagesTotal = ceil(total / NUM_PER_PAGE)
|
|
if scanPage == 0 or (scanResume and scanPage == (db.resume and db.resume.page or scanPage)) then
|
|
Print("scanning "..total.." auctions across "..pagesTotal.." pages...")
|
|
end
|
|
-- Checkpoint after every successfully-read page so /rap continue can
|
|
-- resume from here if the session dies, times out, or is aborted.
|
|
SaveResumeCheckpoint()
|
|
-- Only stop when we've reached the last page currently reported by the
|
|
-- server. Do NOT stop on a short batch alone -- the AH sometimes returns
|
|
-- a short page mid-scan when listings shift; we'd rather query the next
|
|
-- page (which may now exist because new auctions were posted) than bail
|
|
-- out early. If the AH truly ran out of pages, pagesTotal will reflect
|
|
-- that on the next tick and we'll stop then.
|
|
if scanPage >= pagesTotal - 1 then
|
|
if batch < NUM_PER_PAGE then
|
|
StopScan("ok")
|
|
else
|
|
-- last known page was full -- there may be more now, keep going.
|
|
end
|
|
end
|
|
end)
|
|
|
|
local function BeginScan(mode, doResume)
|
|
if scanning then Print("already scanning."); return end
|
|
if not AuctionFrame or not AuctionFrame:IsVisible() then
|
|
Print("open the auction house first."); return
|
|
end
|
|
|
|
local resume = doResume and db.resume or nil
|
|
if doResume and not resume then
|
|
Print("no scan to continue. Start one with /rap scan.")
|
|
return
|
|
end
|
|
|
|
if resume then
|
|
-- restore filters from the checkpoint
|
|
filterName = resume.filterName or ""
|
|
filterClass = resume.filterClass
|
|
filterSubclass = resume.filterSubclass
|
|
filterQuality = resume.filterQuality or 0
|
|
filterMinLvl = resume.filterMinLvl
|
|
filterMaxLvl = resume.filterMaxLvl
|
|
scanMode = resume.mode or "full"
|
|
scanPage = (resume.page or 0) - 1
|
|
scanExpectedTotal = resume.expectedTotal or 0
|
|
scanResume = true
|
|
else
|
|
scanMode = mode or "full"
|
|
scanPage = -1
|
|
scanExpectedTotal = 0
|
|
scanResume = nil
|
|
end
|
|
|
|
scanning = true
|
|
scanStart = GetTime()
|
|
queryTime = 0
|
|
pendingQuery = false
|
|
scanRetries = 0
|
|
scanSeen = 0
|
|
scanSaved = 0
|
|
scanNoLink = 0
|
|
scanNoBuyout = 0
|
|
BeginScanStorage()
|
|
local desc = "all items"
|
|
if filterName ~= "" then desc = "name~'"..filterName.."'" end
|
|
if filterClass then
|
|
local classes = { GetAuctionItemClasses() }
|
|
desc = desc..", class="..(classes[filterClass] or ("#"..filterClass))
|
|
end
|
|
if resume then
|
|
Print("continuing scan ("..scanMode..") from page "..(resume.page or 0).." - "..desc..".")
|
|
else
|
|
Print("scan started ("..scanMode..") - "..desc..". Don't touch AH tabs.")
|
|
end
|
|
scanner:Show()
|
|
end
|
|
|
|
-- ============ export ===================================================
|
|
--
|
|
-- Two export paths:
|
|
--
|
|
-- 1) /rap export
|
|
-- Populates the SavedVariable RAP_SHARED_DATA_EXPORT with a real
|
|
-- Lua table (NOT a string). After you /reload, the file
|
|
-- WTF/Account/<You>/SavedVariables/RelationshipsAuctionPrice.lua
|
|
-- will contain a block that looks exactly like PriceData.lua wants:
|
|
--
|
|
-- RAP_SHARED_DATA_EXPORT = {
|
|
-- ["Realm-F"] = { updated = 12345, items = { [774]={{u=98,q=1},...}, ... } },
|
|
-- }
|
|
--
|
|
-- Open that file, copy the whole `RAP_SHARED_DATA_EXPORT = { ... }`
|
|
-- block, paste it into AddOns/RelationshipsAuctionPrice/PriceData.lua,
|
|
-- and rename the variable to RAP_SHARED_DATA (or just change the
|
|
-- leading identifier). That IS your new PriceData.lua.
|
|
--
|
|
-- 2) /rap exportstring
|
|
-- Old behaviour -- builds a compact single-string form under
|
|
-- RelationshipsAuctionPriceDB.export for people who want to paste
|
|
-- by hand. Kept for compatibility.
|
|
--
|
|
-- Note: you CANNOT copy the raw SavedVariables file (RelationshipsAuctionPriceDB)
|
|
-- into PriceData.lua. That table has a different shape (scans = {{q,s,u},...})
|
|
-- and a different global name; PriceData.lua reads RAP_SHARED_DATA with
|
|
-- items = { [id] = { {u,q}, ... } }.
|
|
|
|
local function BuildSharedTable()
|
|
local key = GetRealmKey()
|
|
local store = db.items[key] or {}
|
|
local items = {}
|
|
local count = 0
|
|
for id, entry in pairs(store) do
|
|
if entry.scans and table.getn(entry.scans) > 0 then
|
|
local rows = {}
|
|
for i=1, table.getn(entry.scans) do
|
|
local r = entry.scans[i]
|
|
table.insert(rows, { u = r.u, q = r.q })
|
|
end
|
|
items[id] = rows
|
|
count = count + 1
|
|
end
|
|
end
|
|
local out = {}
|
|
out[key] = { updated = db.lastScan or time(), items = items }
|
|
return out, key, count
|
|
end
|
|
|
|
local function ExportToSavedVariables()
|
|
local tbl, key, count = BuildSharedTable()
|
|
-- This global is registered in the .toc as a SavedVariable, so after
|
|
-- /reload it will be serialised into the WTF SavedVariables file as a
|
|
-- real Lua table -- ready to copy into PriceData.lua.
|
|
RAP_SHARED_DATA_EXPORT = tbl
|
|
db.exportedAt = time()
|
|
Print("exported "..count.." items for "..key..".")
|
|
Print("Now type /reload, then open:")
|
|
Print(" WTF/Account/<You>/SavedVariables/RelationshipsAuctionPrice.lua")
|
|
Print("Copy the whole RAP_SHARED_DATA_EXPORT = { ... } block into")
|
|
Print(" AddOns/RelationshipsAuctionPrice/PriceData.lua")
|
|
Print("and rename the variable to RAP_SHARED_DATA.")
|
|
end
|
|
|
|
-- Compact single-string form (legacy). Handy if you'd rather paste a string.
|
|
local function BuildExportString()
|
|
local tbl, key = BuildSharedTable()
|
|
local realm = tbl[key]
|
|
local lines = {}
|
|
table.insert(lines, "RAP_SHARED_DATA = {")
|
|
table.insert(lines, " [\""..key.."\"] = {")
|
|
table.insert(lines, " updated = "..(realm.updated or 0)..",")
|
|
table.insert(lines, " items = {")
|
|
for id, rows in pairs(realm.items) do
|
|
local parts = {}
|
|
for i=1, table.getn(rows) do
|
|
table.insert(parts, "{u="..rows[i].u..",q="..rows[i].q.."}")
|
|
end
|
|
table.insert(lines, " ["..id.."]={"..table.concat(parts, ",").."},")
|
|
end
|
|
table.insert(lines, " },")
|
|
table.insert(lines, " },")
|
|
table.insert(lines, "}")
|
|
return table.concat(lines, "\n")
|
|
end
|
|
|
|
local function ExportStringToSavedVariables()
|
|
db.export = BuildExportString()
|
|
db.exportedAt = time()
|
|
Print("string export written to RelationshipsAuctionPriceDB.export.")
|
|
Print("/reload, then copy that string into PriceData.lua.")
|
|
end
|
|
|
|
-- ============ AH UI: scan button + name box + class dropdown ===========
|
|
|
|
local ahPanel, ahBtn, ahBtnStatus, ahNameBox, ahClassDD
|
|
|
|
local function CurrentFilterLabel()
|
|
if filterName ~= "" and filterClass then
|
|
local classes = { GetAuctionItemClasses() }
|
|
return "'"..filterName.."' / "..(classes[filterClass] or "?")
|
|
elseif filterName ~= "" then
|
|
return "'"..filterName.."'"
|
|
elseif filterClass then
|
|
local classes = { GetAuctionItemClasses() }
|
|
return classes[filterClass] or "class"
|
|
end
|
|
return "All items"
|
|
end
|
|
|
|
local function UpdateAHButton()
|
|
if not ahBtn then return end
|
|
if scanning then ahBtn:SetText("Stop") else ahBtn:SetText("Scan") end
|
|
if ahBtnStatus then
|
|
if scanning then
|
|
local pagesTotal
|
|
local _, total = GetNumAuctionItems("list")
|
|
if total and total > 0 then pagesTotal = ceil(total / NUM_PER_PAGE) end
|
|
if pagesTotal then
|
|
ahBtnStatus:SetText("page "..(scanPage + 1).."/"..pagesTotal.." ("..scanMode..")")
|
|
else
|
|
ahBtnStatus:SetText("starting... ("..scanMode..")")
|
|
end
|
|
else
|
|
if db.lastScan and db.lastScan > 0 then
|
|
local mins = floor((time() - db.lastScan) / 60)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Dropdown initializer
|
|
local function InitClassDropdown()
|
|
local info
|
|
local classes = { GetAuctionItemClasses() }
|
|
|
|
-- "All items"
|
|
info = {}
|
|
info.text = "All items"
|
|
info.value = 0
|
|
info.checked = (filterClass == nil)
|
|
info.func = function()
|
|
filterClass = nil
|
|
filterSubclass = nil
|
|
UIDropDownMenu_SetSelectedValue(ahClassDD, 0)
|
|
UIDropDownMenu_SetText("All items", ahClassDD)
|
|
UpdateAHButton()
|
|
end
|
|
UIDropDownMenu_AddButton(info)
|
|
|
|
-- one entry per class
|
|
for i=1, table.getn(classes) do
|
|
local idx = i
|
|
local nm = classes[i]
|
|
info = {}
|
|
info.text = nm
|
|
info.value = idx
|
|
info.checked = (filterClass == idx)
|
|
info.func = function()
|
|
filterClass = idx
|
|
filterSubclass = nil
|
|
UIDropDownMenu_SetSelectedValue(ahClassDD, idx)
|
|
UIDropDownMenu_SetText(nm, ahClassDD)
|
|
UpdateAHButton()
|
|
end
|
|
UIDropDownMenu_AddButton(info)
|
|
end
|
|
end
|
|
|
|
-- Resolve a relativeTo name from config into an actual frame reference.
|
|
local function RAPResolveRelative(name)
|
|
if name == "AuctionFrame" then return AuctionFrame end
|
|
if name == "panel" then return ahPanel or AuctionFrame end
|
|
if name == "scanBtn" then return ahBtn end
|
|
if name == "classDD" then return ahClassDD end
|
|
if name == "nameBox" then return ahNameBox end
|
|
return ahPanel or AuctionFrame
|
|
end
|
|
|
|
local function RAPApplyPoint(frame, cfg)
|
|
if not frame or not cfg then return end
|
|
frame:ClearAllPoints()
|
|
frame:SetPoint(
|
|
cfg.point or "CENTER",
|
|
RAPResolveRelative(cfg.relativeTo),
|
|
cfg.relativePoint or cfg.point or "CENTER",
|
|
cfg.x or 0, cfg.y or 0
|
|
)
|
|
end
|
|
|
|
local function CreateAHUI()
|
|
if ahBtn or not AuctionFrame then return end
|
|
local C = RAP_UI_CONFIG or {}
|
|
|
|
-- Container panel: its own little box anchored just above the AH frame
|
|
local cPanel = C.panel or {}
|
|
ahPanel = CreateFrame("Frame", "RAPPanel", AuctionFrame)
|
|
ahPanel:SetWidth(cPanel.width or 380)
|
|
ahPanel:SetHeight(cPanel.height or 64)
|
|
RAPApplyPoint(ahPanel, cPanel)
|
|
ahPanel:SetBackdrop({
|
|
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
|
|
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
|
tile = true, tileSize = 16, edgeSize = 16,
|
|
insets = { left = 4, right = 4, top = 4, bottom = 4 },
|
|
})
|
|
local bg = cPanel.bgColor or { 0.09, 0.07, 0.05, 0.95 }
|
|
local bd = cPanel.borderColor or { 0.55, 0.42, 0.22, 1 }
|
|
ahPanel:SetBackdropColor(bg[1], bg[2], bg[3], bg[4])
|
|
ahPanel:SetBackdropBorderColor(bd[1], bd[2], bd[3], bd[4])
|
|
ahPanel:SetFrameStrata("MEDIUM") -- AH is DIALOG strata; keep panel below it
|
|
ahPanel:SetToplevel(false)
|
|
|
|
-- Draggable panel (only while AH is open)
|
|
ahPanel:SetMovable(true)
|
|
ahPanel:EnableMouse(true)
|
|
ahPanel:RegisterForDrag("LeftButton")
|
|
ahPanel:SetClampedToScreen(true)
|
|
ahPanel:SetScript("OnDragStart", function()
|
|
if AuctionFrame and AuctionFrame:IsVisible() then
|
|
this:StartMoving()
|
|
this.isMoving = true
|
|
end
|
|
end)
|
|
ahPanel:SetScript("OnDragStop", function()
|
|
if this.isMoving then
|
|
this:StopMovingOrSizing()
|
|
this.isMoving = false
|
|
local point, _, relPoint, x, y = this:GetPoint()
|
|
RelationshipsAuctionPriceDB.panelPos = {
|
|
point = point, relPoint = relPoint, x = x, y = y,
|
|
}
|
|
end
|
|
end)
|
|
|
|
-- Restore saved position (if any), otherwise keep the AH-anchored default
|
|
local savedPos = RelationshipsAuctionPriceDB and RelationshipsAuctionPriceDB.panelPos
|
|
if savedPos and savedPos.point then
|
|
ahPanel:ClearAllPoints()
|
|
ahPanel:SetPoint(savedPos.point, UIParent, savedPos.relPoint, savedPos.x, savedPos.y)
|
|
end
|
|
|
|
-- Scan button
|
|
local cBtn = C.scanButton or {}
|
|
ahBtn = CreateFrame("Button", "RAPScanButton", ahPanel, "UIPanelButtonTemplate")
|
|
ahBtn:SetWidth(cBtn.width or 70); ahBtn:SetHeight(cBtn.height or 22)
|
|
RAPApplyPoint(ahBtn, cBtn)
|
|
ahBtn:SetText("Scan")
|
|
ahBtn:SetScript("OnClick", function()
|
|
if scanning then
|
|
StopScan("aborted")
|
|
else
|
|
if IsControlKeyDown() then
|
|
BeginScan(nil, true) -- continue prior scan
|
|
elseif IsShiftKeyDown() then
|
|
BeginScan("fast")
|
|
else
|
|
BeginScan("full")
|
|
end
|
|
end
|
|
UpdateAHButton()
|
|
end)
|
|
ahBtn:SetScript("OnEnter", function()
|
|
GameTooltip:SetOwner(this, "ANCHOR_BOTTOMLEFT")
|
|
GameTooltip:AddLine("Relationships Auction Price")
|
|
GameTooltip:AddLine("Click: scan with current filter", 1,1,1)
|
|
GameTooltip:AddLine("Shift-click: fast scan", 1,1,1)
|
|
GameTooltip:AddLine("Ctrl-click: continue previous scan", 1,1,1)
|
|
GameTooltip:AddLine("Filter: "..CurrentFilterLabel(), 0.7,0.9,1)
|
|
if db and db.resume then
|
|
GameTooltip:AddLine("Resume available: page "..(db.resume.page or 0).." ("..(db.resume.mode or "full")..")", 0.6,1,0.6)
|
|
end
|
|
GameTooltip:AddLine("Empty filter = scan everything.", 0.7,0.7,0.7)
|
|
GameTooltip:Show()
|
|
end)
|
|
ahBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)
|
|
|
|
-- Class dropdown (to the left of scan by default)
|
|
local cDD = C.classDropdown or {}
|
|
ahClassDD = CreateFrame("Frame", "RAPClassDropDown", ahPanel, "UIDropDownMenuTemplate")
|
|
RAPApplyPoint(ahClassDD, cDD)
|
|
UIDropDownMenu_SetWidth(cDD.width or 120, ahClassDD)
|
|
UIDropDownMenu_Initialize(ahClassDD, InitClassDropdown)
|
|
UIDropDownMenu_SetSelectedValue(ahClassDD, 0)
|
|
UIDropDownMenu_SetText("All items", ahClassDD)
|
|
-- Center the dropdown's selected-text label within the dropdown box.
|
|
local ddText = _G and _G["RAPClassDropDownText"] or getglobal("RAPClassDropDownText")
|
|
if ddText then
|
|
ddText:ClearAllPoints()
|
|
ddText:SetPoint("CENTER", ahClassDD, "CENTER", 0, 2)
|
|
ddText:SetJustifyH("CENTER")
|
|
end
|
|
|
|
-- Name filter EditBox (to the left of dropdown by default)
|
|
local cName = C.nameBox or {}
|
|
ahNameBox = CreateFrame("EditBox", "RAPNameFilter", ahPanel, "InputBoxTemplate")
|
|
ahNameBox:SetAutoFocus(false)
|
|
ahNameBox:SetJustifyH("CENTER")
|
|
ahNameBox:SetWidth(cName.width or 120); ahNameBox:SetHeight(cName.height or 20)
|
|
RAPApplyPoint(ahNameBox, cName)
|
|
ahNameBox:SetScript("OnTextChanged", function()
|
|
filterName = this:GetText() or ""
|
|
UpdateAHButton()
|
|
end)
|
|
ahNameBox:SetScript("OnEnterPressed", function()
|
|
this:ClearFocus()
|
|
if not scanning then BeginScan("full"); UpdateAHButton() end
|
|
end)
|
|
ahNameBox:SetScript("OnEscapePressed", function() this:ClearFocus() end)
|
|
|
|
-- Placeholder overlay ("Name Filter") shown while box is empty and unfocused.
|
|
local ph = ahNameBox:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
|
|
ph:SetPoint("CENTER", ahNameBox, "CENTER", -3, 0)
|
|
ph:SetJustifyH("CENTER")
|
|
ph:SetText("Name Filter")
|
|
ph:SetTextColor(0.6, 0.6, 0.6)
|
|
ahNameBox.placeholder = ph
|
|
local function RAPUpdateNamePlaceholder()
|
|
local txt = ahNameBox:GetText() or ""
|
|
if txt == "" and not ahNameBox.hasFocus then
|
|
ph:Show()
|
|
else
|
|
ph:Hide()
|
|
end
|
|
end
|
|
ahNameBox:SetScript("OnEditFocusGained", function()
|
|
this.hasFocus = true; RAPUpdateNamePlaceholder()
|
|
end)
|
|
ahNameBox:SetScript("OnEditFocusLost", function()
|
|
this.hasFocus = false; RAPUpdateNamePlaceholder()
|
|
end)
|
|
local prevOnTextChanged = ahNameBox:GetScript("OnTextChanged")
|
|
ahNameBox:SetScript("OnTextChanged", function()
|
|
if prevOnTextChanged then prevOnTextChanged() end
|
|
RAPUpdateNamePlaceholder()
|
|
end)
|
|
RAPUpdateNamePlaceholder()
|
|
|
|
-- Status text under the row
|
|
local cSt = C.statusText or {}
|
|
ahBtnStatus = ahBtn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
|
|
RAPApplyPoint(ahBtnStatus, cSt)
|
|
ahBtnStatus:SetTextColor(0.8, 0.8, 0.8)
|
|
|
|
-- Show/hide the panel with the auction house
|
|
ahPanel:SetScript("OnShow", function() end)
|
|
end
|
|
|
|
local btnTicker = CreateFrame("Frame")
|
|
btnTicker.acc = 0
|
|
btnTicker:SetScript("OnUpdate", function()
|
|
btnTicker.acc = btnTicker.acc + arg1
|
|
if btnTicker.acc < 0.25 then return end
|
|
btnTicker.acc = 0
|
|
if ahBtn and ahBtn:IsVisible() then UpdateAHButton() end
|
|
end)
|
|
|
|
-- ============ slash ====================================================
|
|
|
|
SLASH_RAP1 = "/rap"
|
|
SLASH_RAP2 = "/relprice"
|
|
SlashCmdList["RAP"] = function(msg)
|
|
msg = msg or ""
|
|
local low = string.lower(msg)
|
|
if low == "" or low == "scan" then
|
|
BeginScan("full")
|
|
elseif low == "fast" then
|
|
BeginScan("fast")
|
|
elseif low == "continue" or low == "resume" then
|
|
BeginScan(nil, true)
|
|
elseif low == "continue clear" or low == "resume clear" then
|
|
db.resume = nil
|
|
Print("resume checkpoint cleared.")
|
|
elseif low == "stop" then
|
|
if scanning then StopScan("aborted") else Print("not scanning.") end
|
|
elseif low == "clear" then
|
|
db.items[GetRealmKey()] = {}
|
|
Print("cleared prices for "..GetRealmKey())
|
|
elseif low == "export" then
|
|
ExportToSavedVariables()
|
|
elseif low == "exportstring" then
|
|
ExportStringToSavedVariables()
|
|
elseif string.sub(low,1,5) == "name " then
|
|
filterName = string.sub(msg, 6)
|
|
if ahNameBox then ahNameBox:SetText(filterName) end
|
|
Print("name filter set to '"..filterName.."'"); UpdateAHButton()
|
|
elseif low == "name" or low == "name clear" then
|
|
filterName = ""
|
|
if ahNameBox then ahNameBox:SetText("") end
|
|
Print("name filter cleared."); UpdateAHButton()
|
|
elseif string.sub(low,1,6) == "class " then
|
|
local arg = string.sub(low, 7)
|
|
if arg == "clear" or arg == "" then
|
|
filterClass = nil; filterSubclass = nil
|
|
Print("class filter cleared.")
|
|
else
|
|
filterClass = tonumber(arg)
|
|
local classes = { GetAuctionItemClasses() }
|
|
Print("class filter -> "..(classes[filterClass] or ("#"..tostring(filterClass))))
|
|
end
|
|
UpdateAHButton()
|
|
elseif low == "stats" then
|
|
local localItems, localListings = CountRowsInStore(EnsureRealm())
|
|
local sharedItems = 0
|
|
local sharedListings = 0
|
|
local shared = RAP_SHARED_DATA and RAP_SHARED_DATA[GetRealmKey()] and RAP_SHARED_DATA[GetRealmKey()].items
|
|
if shared then
|
|
for id, rows in pairs(shared) do
|
|
sharedItems = sharedItems + 1
|
|
sharedListings = sharedListings + table.getn(rows)
|
|
end
|
|
end
|
|
Print("local: "..localListings.." listings across "..localItems.." item IDs.")
|
|
Print("shared: "..sharedListings.." listings across "..sharedItems.." item IDs.")
|
|
elseif string.sub(low,1,4) == "show" then
|
|
local q = string.sub(low, 6)
|
|
local store = EnsureRealm()
|
|
local hits = 0
|
|
for id, entry in pairs(store) do
|
|
if entry.scans and table.getn(entry.scans) > 0 then
|
|
if q == "" or string.find(tostring(id), q, 1, true) then
|
|
Print(id..": "..table.getn(entry.scans).." listings, low "..FmtMoney(entry.scans[table.getn(entry.scans)].u))
|
|
hits = hits + 1
|
|
if hits >= 15 then break end
|
|
end
|
|
end
|
|
end
|
|
if hits == 0 then Print("no data. /rap scan at the AH first.") end
|
|
else
|
|
Print("commands: /rap scan | fast | continue | stop | clear | stats | show <id> | export")
|
|
Print(" /rap name <text> | /rap name clear")
|
|
Print(" /rap class <index> | /rap class clear")
|
|
Print(" /rap continue clear (drops the saved resume checkpoint)")
|
|
end
|
|
end
|
|
|
|
-- ============ init =====================================================
|
|
|
|
local init = CreateFrame("Frame")
|
|
init:RegisterEvent("VARIABLES_LOADED")
|
|
init:RegisterEvent("PLAYER_LOGIN")
|
|
init:RegisterEvent("AUCTION_HOUSE_SHOW")
|
|
init:SetScript("OnEvent", function()
|
|
if event == "AUCTION_HOUSE_SHOW" then
|
|
CreateAHUI()
|
|
UpdateAHButton()
|
|
return
|
|
end
|
|
db = RelationshipsAuctionPriceDB
|
|
db.items = db.items or {}
|
|
EnsureRealm()
|
|
InstallTooltipHooks()
|
|
local sharedCount = 0
|
|
if RAP_SHARED_DATA and RAP_SHARED_DATA[GetRealmKey()] and RAP_SHARED_DATA[GetRealmKey()].items then
|
|
for _ in pairs(RAP_SHARED_DATA[GetRealmKey()].items) do sharedCount = sharedCount + 1 end
|
|
end
|
|
Print("loaded. /rap scan at the AH. Shared items for this realm: "..sharedCount)
|
|
if db.resume then
|
|
Print("resume available: page "..(db.resume.page or 0).." ("..(db.resume.mode or "full").."). /rap continue at the AH.")
|
|
end
|
|
end)
|