-- ------------- -- -- Price Tracker -- -- ------------- -- PriceTracker = { queryDelay = 3000, isSearching = false, settingsVersion = 0.2, icons = { gold = "EsoUI/Art/currency/currency_gold.dds" } } 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("OnTradingHouseClosed", EVENT_CLOSE_TRADING_HOUSE, function(...) self:OnTradingHouseClosed(...) end) ZO_PreHookHandler(ItemTooltip, "OnUpdate", function() self:OnUpdateTooltip(moc()) end) ZO_PreHookHandler(ItemTooltip, "OnHide", function() self:OnHideTooltip() end) 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 } -- Load saved settings self.settings = ZO_SavedVars:NewAccountWide("PriceTrackerSettings", self.settingsVersion, nil, defaults) -- 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 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)") end function PriceTracker:OnUpdateTooltip(item) if not item or not item.dataEntry or not item.dataEntry.data or self.selectedItem == item then return end self.selectedItem = 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(ItemTooltip) ItemTooltip:AddLine("Price Tracker", "ZoFontHeader2") local r, g, b = ZO_TOOLTIP_DEFAULT_COLOR:UnpackRGB() ItemTooltip: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 ItemTooltip: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) ItemTooltip:AddLine(self:FormatTooltipLine("Min (each / stack):", minPrice, minPrice * stackCount, minItem.guildName), "ZoFontGame", r, g, b, CENTER, MODIFY_TEXT_TYPE_NONE, CENTER, true) ItemTooltip: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 ItemTooltip:AddLine("Seen " .. #matches .. " times", "ZoFontGame", r, g, b, CENTER, MODIFY_TEXT_TYPE_NONE, CENTER, false) end end function PriceTracker:OnHideTooltip() self.selectedItem = nil end function PriceTracker:OnScanPrices() if self.isSearching then return end self.button:SetEnabled(false) self.isSearching = true self.numOfGuilds = GetNumTradingHouseGuilds() self.currentGuild = 0 while not CanSellOnTradingHouse(self.currentGuild) and self.currentGuild < self.numOfGuilds do self.currentGuild = self.currentGuild + 1 end zo_callLater(function() ExecuteTradingHouseSearch(0, TRADING_HOUSE_SORT_SALE_PRICE, true) end, self.queryDelay) 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 if hasMorePages then zo_callLater(function() ExecuteTradingHouseSearch(currentPage + 1, TRADING_HOUSE_SORT_SALE_PRICE, true) end, self.queryDelay) else if self.currentGuild < self.numOfGuilds then self.currentGuild = self.currentGuild + 1 SelectTradingHouseGuildId(self.currentGuild) zo_callLater(function() ExecuteTradingHouseSearch(0, TRADING_HOUSE_SORT_SALE_PRICE, true) end, self.queryDelay) else self:OnTradingHouseClosed() end end end function PriceTracker:OnTradingHouseClosed() self.isSearching = false self.button:SetEnabled(true) end function PriceTracker:AddItem(icon, itemName, quality, stackCount, sellerName, timeRemaining, purchasePrice) 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 matches = {} for k, v in pairs(self.settings.itemList[normalizedName]) do table.insert(matches, v) 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)