-- ------------- -- -- Price Tracker -- -- ------------- -- PriceTracker = { queryDelay = 3000, isSearching = false, settingsVersion = 0.2, icons = { gold = "EsoUI/Art/currency/currency_gold.dds" }, colors = { default = "|c" .. ZO_TOOLTIP_DEFAULT_COLOR:ToHex(), instructional = "|c" .. ZO_TOOLTIP_INSTRUCTIONAL_COLOR:ToHex(), title = "|c00B5FF", }, selectedItem = {}, } local PriceTracker = PriceTracker -- Addon initialization function PriceTracker:OnLoad(eventCode, addOnName) if(addOnName ~= "PriceTracker") then return end EVENT_MANAGER:RegisterForEvent("OnSearchResultsReceived", EVENT_TRADING_HOUSE_SEARCH_RESULTS_RECEIVED, function(...) self:OnSearchResultsReceived(...) end) EVENT_MANAGER:RegisterForEvent("OnSearchResultsError", EVENT_TRADING_HOUSE_ERROR, function(...) self:OnSearchResultsError(...) end) EVENT_MANAGER:RegisterForEvent("OnTradingHouseClosed", EVENT_CLOSE_TRADING_HOUSE, function(...) self:OnTradingHouseClosed(...) end) LINK_HANDLER:RegisterCallback(LINK_HANDLER.LINK_CLICKED_EVENT, PriceTracker.OnLinkClicked, self) ZO_PreHookHandler(ItemTooltip, "OnUpdate", function() self:OnUpdateTooltip(moc(), ItemTooltip) end) ZO_PreHookHandler(ItemTooltip, "OnHide", function() self:OnHideTooltip(ItemTooltip) end) ZO_PreHookHandler(PopupTooltip, "OnUpdate", function() self:OnUpdateTooltip(self.clickedItem, PopupTooltip) end) ZO_PreHookHandler(PopupTooltip, "OnHide", function() self:OnHideTooltip(PopupTooltip) end) PriceTracker.enchantingTable:OnLoad(eventCode, addOnName) SLASH_COMMANDS["/pt"] = function(...) self:CommandHandler(...) end SLASH_COMMANDS["/pricetracker"] = function(...) self:CommandHandler(...) end local defaults = { itemList = {}, algorithm = self.menu.algorithmTable[1], showMinMax = true, showSeen = true, keyPress = self.menu.keyTable[1], } -- Load saved settings self.settings = ZO_SavedVars:NewAccountWide("PriceTrackerSettings", self.settingsVersion, nil, defaults) -- Do some housekeeping and remove inparsable items self:Housekeeping() -- Create a button in the trading house window self.button = PriceTrackerControlButton self.button:SetParent(ZO_TradingHouseLeftPaneBrowseItemsCommon) self.button:SetWidth(ZO_TradingHouseLeftPaneBrowseItemsCommonQuality:GetWidth()) self.menu:InitAddonMenu(addOnName) end -- Handle slash commands function PriceTracker:CommandHandler(text) if not text or text == "" or text == "help" then self:ShowHelp() return end if text == "reset" or text == "clear" then self.settings.itemList = {} return end if text == "clean" then self:CleanItemList() return end -- Hidden option if text == "housekeeping" then self:Housekeeping() return end end function PriceTracker:ShowHelp() d("To scan all item prices in all guild stores, click the 'Scan Prices' button in the guild store window.") d(" ") d("/pt help - Show this help") d("/pt clear - Clear stored price values") d("/pt clean - Remove stale items (experimental)") d("/ptsetup - Open the addon setup menu") end -- This method makes sure the item list is intact and parsable, in order to avoid exceptions later on function PriceTracker:Housekeeping() if not self.settings.itemList then return end for k, v in pairs(self.settings.itemList) do -- Remove any empty items for p, q in pairs(v) do if not q.purchasePrice or not q.stackCount then v[p] = nil end end if not next(v) then self.settings.itemList[k] = nil end end end function PriceTracker:OnUpdateTooltip(item, tooltip) if not tooltip then tooltip = ItemTooltip end if not item or not item.dataEntry or not item.dataEntry.data or not self.menu:IsKeyPressed() or self.selectedItem[tooltip] == item then return end self.selectedItem[tooltip] = item local stackCount = item.dataEntry.data.stackCount or item.dataEntry.data.stack if not stackCount then return end local matches = self:GetMatches(item.dataEntry.data.name) if not matches then return end local item = self:SuggestPrice(matches) if not item then return end ZO_Tooltip_AddDivider(tooltip) tooltip:AddLine("Price Tracker", "ZoFontHeader2") local r, g, b = ZO_TOOLTIP_DEFAULT_COLOR:UnpackRGB() tooltip:AddLine(self:FormatTooltipLine("Suggested price:", math.floor(item.purchasePrice / item.stackCount)), "ZoFontGame", r, g, b, CENTER, MODIFY_TEXT_TYPE_NONE, CENTER, true) if stackCount > 1 then tooltip:AddLine(self:FormatTooltipLine("Stack price:", math.floor(item.purchasePrice / item.stackCount * stackCount)), "ZoFontGame", r, g, b, CENTER, MODIFY_TEXT_TYPE_NONE, CENTER, true) end if self.settings.showMinMax then local minItem = self.mathUtils:Min(matches) local maxItem = self.mathUtils:Max(matches) local minPrice = math.floor(minItem.purchasePrice / minItem.stackCount) local maxPrice = math.floor(maxItem.purchasePrice / maxItem.stackCount) tooltip:AddLine(self:FormatTooltipLine("Min (each / stack):", minPrice, minPrice * stackCount, minItem.guildName), "ZoFontGame", r, g, b, CENTER, MODIFY_TEXT_TYPE_NONE, CENTER, true) tooltip:AddLine(self:FormatTooltipLine("Max (each / stack):", maxPrice, maxPrice * stackCount, maxItem.guildName), "ZoFontGame", r, g, b, CENTER, MODIFY_TEXT_TYPE_NONE, CENTER, true) end if self.settings.showSeen then tooltip:AddLine("Seen " .. #matches .. " times", "ZoFontGame", r, g, b, CENTER, MODIFY_TEXT_TYPE_NONE, CENTER, false) end end function PriceTracker:OnHideTooltip(tooltip) self.selectedItem[tooltip] = nil self.clickedItem = nil end function PriceTracker:OnScanPrices() if self.isSearching then return end self.button:SetEnabled(false) self.isSearching = true self.numOfGuilds = GetNumTradingHouseGuilds() self.currentGuild = 0 self.currentPage = 0 while not CanSellOnTradingHouse(self.currentGuild) and self.currentGuild < self.numOfGuilds do self.currentGuild = self.currentGuild + 1 end SelectTradingHouseGuildId(self.currentGuild) zo_callLater(function() ExecuteTradingHouseSearch(0, TRADING_HOUSE_SORT_SALE_PRICE, true) end, GetTradingHouseCooldownRemaining() + 1000) end function PriceTracker:OnSearchResultsReceived(eventId, guildId, numItemsOnPage, currentPage, hasMorePages) if not self.isSearching then return end for i = 1, numItemsOnPage do self:AddItem(GetTradingHouseSearchResultItemInfo(i)) end self.currentPage = currentPage if hasMorePages then zo_callLater(function() ExecuteTradingHouseSearch(currentPage + 1, TRADING_HOUSE_SORT_SALE_PRICE, true) end, GetTradingHouseCooldownRemaining() + 1000) else if self.currentGuild < self.numOfGuilds then self.currentGuild = self.currentGuild + 1 while not CanSellOnTradingHouse(self.currentGuild) and self.currentGuild < self.numOfGuilds do self.currentGuild = self.currentGuild + 1 end zo_callLater(function() SelectTradingHouseGuildId(self.currentGuild) end, self.queryDelay) zo_callLater(function() ExecuteTradingHouseSearch(0, TRADING_HOUSE_SORT_SALE_PRICE, true) end, GetTradingHouseCooldownRemaining() + 1000) else self:OnTradingHouseClosed() end end end function PriceTracker:OnSearchResultsError(eventCode, errorCode) self:OnSearchResultsReceived(eventCode, self.currentGuild, 0, self.currentPage, false) zo_callLater(function() ExecuteTradingHouseSearch(self.currentPage, TRADING_HOUSE_SORT_SALE_PRICE, true) end, GetTradingHouseCooldownRemaining() + 1000) end function PriceTracker:OnTradingHouseClosed() self.isSearching = false self.button:SetEnabled(true) end function PriceTracker:OnLinkClicked(rawLink, mouseButton, linkText, color, linkType, itemId, ...) if linkType ~= "item" then return end local _, sellPrice, _, _, _ = GetItemLinkInfo(rawLink) local item = { dataEntry = { data = { name = self:NormalizeName(string.sub(linkText, 2, #linkText - 1)), stackCount = 1, purchasePrice = sellPrice } } } self.clickedItem = item end function PriceTracker:AddItem(icon, itemName, quality, stackCount, sellerName, timeRemaining, purchasePrice) if not purchasePrice or not stackCount then return end local item = {} item.expiry = timeRemaining + GetTimeStamp() item.icon = icon item.name = itemName item.normalizedName = self:NormalizeName(itemName) item.quality = quality item.stackCount = stackCount item.sellerName = sellerName item.purchasePrice = purchasePrice item.eachPrice = purchasePrice / stackCount item.guildId = self.currentGuild item.guildName = GetGuildName(item.guildId) if not self.settings.itemList[item.normalizedName] then self.settings.itemList[item.normalizedName] = {} end -- Do not add items that are already in the database if not self.settings.itemList[item.normalizedName][item.expiry] then self.settings.itemList[item.normalizedName][item.expiry] = item end end function PriceTracker:CleanItemList() local timestamp = GetTimeStamp() for k, v in pairs(PriceTracker.settings.itemList) do for itemK, itemV in pairs(v) do if itemV.expiry > timestamp then table.remove(v, itemK) end end end end function PriceTracker:GetMatches(itemName) local normalizedName = self:NormalizeName(itemName) if not self.settings.itemList or not self.settings.itemList[normalizedName] then return nil end local limitToGuild = self.settings.limitToGuild or 1 local matches = {} for k, v in pairs(self.settings.itemList[normalizedName]) do if limitToGuild == 1 or v.guildId == GetGuildId(limitToGuild - 1) then table.insert(matches, v) end end return matches end -- TODO: Base calculation on user preference function PriceTracker:SuggestPrice(matches) if self.settings.algorithm == self.menu.algorithmTable[1] then return self.mathUtils:WeightedAverage(matches) end if self.settings.algorithm == self.menu.algorithmTable[2] then return self.mathUtils:Median(matches) end if self.settings.algorithm == self.menu.algorithmTable[3] then return self.mathUtils:Mode(matches) end d("Error deciding how to calculate suggested price") return nil end function PriceTracker:FormatTooltipLine(title, price1, price2, guild) if price2 then price1 = price1 .. " / " .. price2 end local str if guild then str = "%-20.20s (%-10.10s)" else str = "%-40.40s %1.1s" end str = str .. " %12.12s%s" return string.format(str, title, guild or "", price1, zo_iconFormat(self.icons.gold, 16, 16)) end function PriceTracker:NormalizeName(name) return zo_strformat(SI_TOOLTIP_ITEM_NAME, name) end EVENT_MANAGER:RegisterForEvent("PriceTrackerLoaded", EVENT_ADD_ON_LOADED, function(...) PriceTracker:OnLoad(...) end)