-- Shopkeeper Main Addon File -- Last Updated August 19, 2014 -- Written July 2014 by Dan Stone (@khaibit) - dankitymao@gmail.com -- Released under terms in license accompanying this file. -- Distribution without license is prohibited! -- Sort the scan results by 'ordering' order (asc/desc). -- We sort both the search result table and the master scan results table because either we do it -- now or we sort at separate times and try to keep track of what state each is in. No thanks! function Shopkeeper.SortByPrice(ordering) Shopkeeper.curSort[1] = "price" Shopkeeper.curSort[2] = ordering if ordering == "asc" then -- If they're viewing prices per-unit, then we need to sort on price / quantity. if (Shopkeeper.acctSavedVariables.allSettingsAccount and Shopkeeper.acctSavedVariables.showUnitPrice) or (not self.acctSavedVariables.allSettingsAccount and Shopkeeper.savedVariables.showUnitPrice) then table.sort(Shopkeeper.SearchTable, function(sortA, sortB) -- In case quantity ends up 0 or nil somehow, let's not divide by it if sortA[5] and sortA[5] > 0 and sortB[5] and sortB[5] > 0 then return (sortA[7] / sortA[5]) > (sortB[7] / sortB[5]) else return sortA[7] > sortB[7] end end) table.sort(Shopkeeper.ScanResults, function(sortA, sortB) if sortA[5] and sortA[5] > 0 and sortB[5] and sortB[5] > 0 then return (sortA[7] / sortA[5]) > (sortB[7] / sortB[5]) else return sortA[7] > sortB[7] end end) table.sort(Shopkeeper.SelfSales, function(sortA, sortB) if sortA[5] and sortA[5] > 0 and sortB[5] and sortB[5] > 0 then return (sortA[7] / sortA[5]) > (sortB[7] / sortB[5]) else return sortA[7] > sortB[7] end end) -- Otherwise just sort on pure price. else table.sort(Shopkeeper.SearchTable, function(sortA, sortB) return sortA[7] > sortB[7] end) table.sort(Shopkeeper.ScanResults, function(sortA, sortB) return sortA[7] > sortB[7] end) table.sort(Shopkeeper.SelfSales, function(sortA, sortB) return sortA[7] > sortB[7] end) end ShopkeeperWindowSortPrice:SetTexture("/esoui/art/miscellaneous/list_sortheader_icon_sortup.dds") else -- And the same thing with descending sort if (Shopkeeper.acctSavedVariables.allSettingsAccount and Shopkeeper.acctSavedVariables.showUnitPrice) or (not self.acctSavedVariables.allSettingsAccount and Shopkeeper.savedVariables.showUnitPrice) then table.sort(Shopkeeper.SearchTable, function(sortA, sortB) return (sortA[7] / sortA[5]) < (sortB[7] / sortB[5]) end) table.sort(Shopkeeper.ScanResults, function(sortA, sortB) return (sortA[7] / sortA[5]) < (sortB[7] / sortB[5]) end) table.sort(Shopkeeper.SelfSales, function(sortA, sortB) return (sortA[7] / sortA[5]) < (sortB[7] / sortB[5]) end) else table.sort(Shopkeeper.SearchTable, function(sortA, sortB) return sortA[7] < sortB[7] end) table.sort(Shopkeeper.ScanResults, function(sortA, sortB) return sortA[7] < sortB[7] end) table.sort(Shopkeeper.SelfSales, function(sortA, sortB) return sortA[7] < sortB[7] end) end ShopkeeperWindowSortPrice:SetTexture("/esoui/art/miscellaneous/list_sortheader_icon_sortdown.dds") end ShopkeeperWindowSortTime:SetTexture("/esoui/art/miscellaneous/list_sortheader_icon_neutral.dds") Shopkeeper.DisplayRows() end -- Sort the scan results by 'ordering' order (asc/desc). -- We sort both the search result table and the master scan results table because either we do it -- now or we sort at separate times and try to keep track of what state each is in. No thanks! function Shopkeeper.SortByTime(ordering) Shopkeeper.curSort[1] = "time" Shopkeeper.curSort[2] = ordering if ordering == "asc" then table.sort(Shopkeeper.SearchTable, function(sortA, sortB) return sortA[6] < sortB[6] end) table.sort(Shopkeeper.ScanResults, function(sortA, sortB) return sortA[6] < sortB[6] end) table.sort(Shopkeeper.SelfSales, function(sortA, sortB) return sortA[6] < sortB[6] end) ShopkeeperWindowSortTime:SetTexture("/esoui/art/miscellaneous/list_sortheader_icon_sortup.dds") else table.sort(Shopkeeper.SearchTable, function(sortA, sortB) return sortA[6] > sortB[6] end) table.sort(Shopkeeper.ScanResults, function(sortA, sortB) return sortA[6] > sortB[6] end) table.sort(Shopkeeper.SelfSales, function(sortA, sortB) return sortA[6] > sortB[6] end) ShopkeeperWindowSortTime:SetTexture("/esoui/art/miscellaneous/list_sortheader_icon_sortdown.dds") end ShopkeeperWindowSortPrice:SetTexture("/esoui/art/miscellaneous/list_sortheader_icon_neutral.dds") Shopkeeper.DisplayRows() end -- A convenience function to switch betwteen ascending and descending sort -- as well as between price and time sorting if the other is currently active function Shopkeeper.PriceSort() if Shopkeeper.curSort[1] == "price" and Shopkeeper.curSort[2] == "desc" then Shopkeeper.SortByPrice("asc") else Shopkeeper.SortByPrice("desc") end end -- A convenience function to switch betwteen ascending and descending sort -- as well as between price and time sorting if the other is currently active function Shopkeeper.TimeSort() if Shopkeeper.curSort[1] == "time" and Shopkeeper.curSort[2] == "desc" then Shopkeeper.SortByTime("asc") else Shopkeeper.SortByTime("desc") end end -- Calculate some stats based on the player's sales -- And return them as a table. function Shopkeeper.SalesStats(statsDays) local itemsSold = {["SK_STATS_TOTAL"] = 0} local goldMade = {["SK_STATS_TOTAL"] = 0} local largestSingle = {["SK_STATS_TOTAL"] = {0, nil}} local oldestTime = 0 local newestTime = 0 local overallOldestTime = 0 local kioskSales = {["SK_STATS_TOTAL"] = 0} local guildDropdown = ZO_ComboBox_ObjectFromContainer(ShopkeeperStatsGuildChooser) guildDropdown:ClearItems() local allGuilds = guildDropdown:CreateItemEntry(GetString(SK_STATS_ALL_GUILDS), function() Shopkeeper.UpdateStatsWindow("SK_STATS_TOTAL") end) guildDropdown:AddItem(allGuilds) -- 86,400 seconds in a day; this will be the epoch time statsDays ago local statsDaysEpoch = GetTimeStamp() - (86400 * statsDays) -- Loop through the player's sales and create the stats as appropriate -- (everything or everything with a timestamp after statsDaysEpoch) for i = 1, #Shopkeeper.SelfSales do local theItem = Shopkeeper.SelfSales[i] local theItemGuild = theItem[2] if statsDays == 0 or theItem[6] > statsDaysEpoch then itemsSold["SK_STATS_TOTAL"] = itemsSold["SK_STATS_TOTAL"] + 1 if itemsSold[theItemGuild] ~= nil then itemsSold[theItemGuild] = itemsSold[theItemGuild] + 1 else itemsSold[theItemGuild] = 1 end if #theItem > 8 and theItem[9] then kioskSales["SK_STATS_TOTAL"] = kioskSales["SK_STATS_TOTAL"] + 1 if kioskSales[theItemGuild] ~= nil then kioskSales[theItemGuild] = kioskSales[theItemGuild] + 1 else kioskSales[theItemGuild] = 1 end end goldMade["SK_STATS_TOTAL"] = goldMade["SK_STATS_TOTAL"] + theItem[7] if goldMade[theItemGuild] ~= nil then goldMade[theItemGuild] = goldMade[theItemGuild] + theItem[7] else goldMade[theItemGuild] = theItem[7] end if oldestTime == 0 or theItem[6] < oldestTime then oldestTime = theItem[6] end if newestTime == 0 or theItem[6] > newestTime then newestTime = theItem[6] end if theItem[7] > largestSingle["SK_STATS_TOTAL"][1] then largestSingle["SK_STATS_TOTAL"] = {theItem[7], theItem[3]} end if largestSingle[theItemGuild] == nil or theItem[7] > largestSingle[theItemGuild][1] then largestSingle[theItemGuild] = {theItem[7], theItem[3]} end end if overallOldestTime == 0 or theItem[6] < overallOldestTime then overallOldestTime = theItem[6] end end -- Newest timestamp seen minus oldest timestamp seen is the number of seconds between -- them; divided by 86,400 it's the number of days (or at least close enough for this) local timeWindow = newestTime - oldestTime local dayWindow = 1 if timeWindow > 86400 then dayWindow = math.floor(timeWindow / 86400) + 1 end local overallTimeWindow = newestTime - overallOldestTime local overallDayWindow = 1 if overallTimeWindow > 86400 then overallDayWindow = math.floor(overallTimeWindow / 86400) + 1 end local goldPerDay = {} local kioskPercentage = {} local showFullPrice = Shopkeeper.savedVariables.showFullPrice if Shopkeeper.acctSavedVariables.allSettingsAccount then showFullPrice = Shopkeeper.acctSavedVariables.showFullPrice end for theGuildName, guildItemsSold in pairs(itemsSold) do goldPerDay[theGuildName] = math.floor(goldMade[theGuildName] / dayWindow) local kioskSalesTemp = 0 if kioskSales[theGuildName] ~= nil then kioskSalesTemp = kioskSales[theGuildName] end kioskPercentage[theGuildName] = math.floor((kioskSalesTemp / guildItemsSold) * 100) if theGuildName ~= "SK_STATS_TOTAL" then local guildEntry = guildDropdown:CreateItemEntry(theGuildName, function() Shopkeeper.UpdateStatsWindow(theGuildName) end) guildDropdown:AddItem(guildEntry) end -- If they have the option set to show prices post-cut, calculate that here if not showFullPrice then local cutMult = 1 - (GetTradingHouseCutPercentage() / 100) goldMade[theGuildName] = math.floor(goldMade[theGuildName] * cutMult + 0.5) goldPerDay[theGuildName] = math.floor(goldPerDay[theGuildName] * cutMult + 0.5) largestSingle[theGuildName][1] = math.floor(largestSingle[theGuildName][1] * cutMult + 0.5) end end -- Return the statistical data in a convenient table return { numSold = itemsSold, numDays = dayWindow, totalDays = overallDayWindow, totalGold = goldMade, avgGold = goldPerDay, biggestSale = largestSingle, kioskPercent = kioskPercentage, } end -- Update all the fields of the stats window based on the response from SalesStats() function Shopkeeper.UpdateStatsWindow(guildName) local sliderLevel = ShopkeeperStatsWindowSlider:GetValue() Shopkeeper.newStats = Shopkeeper.SalesStats(sliderLevel) -- Hide the slider if there's less than a day of data -- and set the slider's range for the new day range returned ShopkeeperStatsWindowSliderLabel:SetHidden(false) ShopkeeperStatsWindowSlider:SetHidden(false) if Shopkeeper.newStats['totalDays'] < 2 then ShopkeeperStatsWindowSlider:SetHidden(true) ShopkeeperStatsWindowSliderLabel:SetHidden(true) sliderLevel = 0 elseif sliderLevel > (Shopkeeper.newStats['totalDays'] - 1) then sliderLevel = 0 end ShopkeeperStatsWindowSlider:SetMinMax(0, (Shopkeeper.newStats['totalDays'] - 1)) -- Set the time range label appropriately if sliderLevel == 0 then ShopkeeperStatsWindowSliderSettingLabel:SetText(GetString(SK_STATS_TIME_ALL)) else ShopkeeperStatsWindowSliderSettingLabel:SetText(zo_strformat(GetString(SK_STATS_TIME_SOME), sliderLevel)) end -- Grab which guild is selected local guildSelected = GetString(SK_STATS_ALL_GUILDS) if guildName ~= "SK_STATS_TOTAL" then guildSelected = guildName end local guildDropdown = ZO_ComboBox_ObjectFromContainer(ShopkeeperStatsGuildChooser) guildDropdown:SetSelectedItem(guildSelected) -- And set the rest of the stats window up with data from the appropriate -- guild (or overall data) ShopkeeperStatsWindowItemsSoldLabel:SetText(string.format(GetString(SK_STATS_ITEMS_SOLD), Shopkeeper.localizedNumber(Shopkeeper.newStats['numSold'][guildName]), Shopkeeper.newStats['kioskPercent'][guildName])) ShopkeeperStatsWindowTotalGoldLabel:SetText(string.format(GetString(SK_STATS_TOTAL_GOLD), Shopkeeper.localizedNumber(Shopkeeper.newStats['totalGold'][guildName]), Shopkeeper.localizedNumber(Shopkeeper.newStats['avgGold'][guildName]))) ShopkeeperStatsWindowBiggestSaleLabel:SetText(string.format(GetString(SK_STATS_BIGGEST), zo_strformat("<<t:1>>", Shopkeeper.newStats['biggestSale'][guildName][2]), Shopkeeper.localizedNumber(Shopkeeper.newStats['biggestSale'][guildName][1]))) end -- LibAddon init code function Shopkeeper:LibAddonInit() local LAM = LibStub("LibAddonMenu-2.0") if LAM then local LMP = LibStub("LibMediaProvider-1.0") if LMP then local panelData = { type = "panel", name = "Shopkeeper", displayName = "Shopkeeper", author = "Khaibit", version = Shopkeeper.version, registerForDefaults = true, } LAM:RegisterAddonPanel("ShopkeeperOptions", panelData) local settingsToUse = Shopkeeper.savedVariables if Shopkeeper.acctSavedVariables.allSettingsAccount then settingsToUse = Shopkeeper.acctSavedVariables end local optionsData = { -- Sound and Alert options [1] = { type = "submenu", name = GetString(SK_ALERT_OPTIONS_NAME), tooltip = GetString(SK_ALERT_OPTIONS_TIP), controls = { -- On-Screen Alerts [1] = { type = "checkbox", name = GetString(SK_ALERT_ANNOUNCE_NAME), tooltip = GetString(SK_ALERT_ANNOUNCE_TIP), getFunc = function() if Shopkeeper.acctSavedVariables.allSettingsAccount then return Shopkeeper.acctSavedVariables.showAnnounceAlerts else return Shopkeeper.savedVariables.showAnnounceAlerts end end, setFunc = function(value) if Shopkeeper.acctSavedVariables.allSettingsAccount then Shopkeeper.acctSavedVariables.showAnnounceAlerts = value else Shopkeeper.savedVariables.showAnnounceAlerts = value end end, }, -- Chat Alerts [2] = { type = "checkbox", name = GetString(SK_ALERT_CHAT_NAME), tooltip = GetString(SK_ALERT_CHAT_TIP), getFunc = function() if Shopkeeper.acctSavedVariables.allSettingsAccount then return Shopkeeper.acctSavedVariables.showChatAlerts else return Shopkeeper.savedVariables.showChatAlerts end end, setFunc = function(value) if Shopkeeper.acctSavedVariables.allSettingsAccount then Shopkeeper.acctSavedVariables.showChatAlerts = value else Shopkeeper.savedVariables.showChatAlerts = value end end, }, -- Sound to use for alerts [3] = { type = "dropdown", name = GetString(SK_ALERT_TYPE_NAME), tooltip = GetString(SK_ALERT_TYPE_TIP), choices = Shopkeeper.soundKeys(), getFunc = function() if Shopkeeper.acctSavedVariables.allSettingsAccount then return Shopkeeper.searchSounds(Shopkeeper.acctSavedVariables.alertSoundName) else return Shopkeeper.searchSounds(Shopkeeper.savedVariables.alertSoundName) end end, setFunc = function(value) if Shopkeeper.acctSavedVariables.allSettingsAccount then Shopkeeper.acctSavedVariables.alertSoundName = Shopkeeper.searchSoundNames(value) else Shopkeeper.savedVariables.alertSoundName = Shopkeeper.searchSoundNames(value) end PlaySound(Shopkeeper.searchSoundNames(value)) end, }, -- Whether or not to show multiple alerts for multiple sales [4] = { type = "checkbox", name = GetString(SK_MULT_ALERT_NAME), tooltip = GetString(SK_MULT_ALERT_TIP), getFunc = function() if Shopkeeper.acctSavedVariables.allSettingsAccount then return Shopkeeper.acctSavedVariables.showMultiple else return Shopkeeper.savedVariables.showMultiple end end, setFunc = function(value) if Shopkeeper.acctSavedVariables.allSettingsAccount then Shopkeeper.acctSavedVariables.showMultiple = value else Shopkeeper.savedVariables.showMultiple = value end end, }, }, }, -- Open main window with mailbox scenes [2] = { type = "checkbox", name = GetString(SK_OPEN_MAIL_NAME), tooltip = GetString(SK_OPEN_MAIL_TIP), getFunc = function() if Shopkeeper.acctSavedVariables.allSettingsAccount then return Shopkeeper.acctSavedVariables.openWithMail else return Shopkeeper.savedVariables.openWithMail end end, setFunc = function(value) if Shopkeeper.acctSavedVariables.allSettingsAccount then Shopkeeper.acctSavedVariables.openWithMail = value else Shopkeeper.savedVariables.openWithMail = value end if value then -- Register for the mail scenes MAIL_INBOX_SCENE:AddFragment(Shopkeeper.uiFragment) MAIL_SEND_SCENE:AddFragment(Shopkeeper.uiFragment) else -- Unregister for the mail scenes MAIL_INBOX_SCENE:RemoveFragment(Shopkeeper.uiFragment) MAIL_SEND_SCENE:RemoveFragment(Shopkeeper.uiFragment) end end, }, -- Open main window with trading house scene [3] = { type = "checkbox", name = GetString(SK_OPEN_STORE_NAME), tooltip = GetString(SK_OPEN_STORE_TIP), getFunc = function() if Shopkeeper.acctSavedVariables.allSettingsAccount then return Shopkeeper.acctSavedVariables.openWithStore else return Shopkeeper.savedVariables.openWithStore end end, setFunc = function(value) if Shopkeeper.acctSavedVariables.allSettingsAccount then Shopkeeper.acctSavedVariables.openWithStore = value else Shopkeeper.savedVariables.openWithStore = value end if value then -- Register for the store scene TRADING_HOUSE_SCENE:AddFragment(Shopkeeper.uiFragment) else -- Unregister for the store scene TRADING_HOUSE_SCENE:RemoveFragment(Shopkeeper.uiFragment) end end, }, -- Show full sale price or post-tax price [4] = { type = "checkbox", name = GetString(SK_FULL_SALE_NAME), tooltip = GetString(SK_FULL_SALE_TIP), getFunc = function() if Shopkeeper.acctSavedVariables.allSettingsAccount then return Shopkeeper.acctSavedVariables.showFullPrice else return Shopkeeper.savedVariables.showFullPrice end end, setFunc = function(value) if Shopkeeper.acctSavedVariables.allSettingsAccount then Shopkeeper.acctSavedVariables.showFullPrice = value else Shopkeeper.savedVariables.showFullPrice = value end Shopkeeper.DisplayRows() end, }, -- Scan frequency (in seconds) [5] = { type = "slider", name = GetString(SK_SCAN_FREQ_NAME), tooltip = GetString(SK_SCAN_FREQ_TIP), min = 60, max = 600, getFunc = function() if Shopkeeper.acctSavedVariables.allSettingsAccount then return Shopkeeper.acctSavedVariables.scanFreq else return Shopkeeper.savedVariables.scanFreq end end, setFunc = function(value) if Shopkeeper.acctSavedVariables.allSettingsAccount then Shopkeeper.acctSavedVariables.scanFreq = value else Shopkeeper.savedVariables.scanFreq = value end EVENT_MANAGER:UnregisterForUpdate(Shopkeeper.name) local scanInterval = value * 1000 EVENT_MANAGER:RegisterForUpdate(Shopkeeper.name, scanInterval, function() Shopkeeper:ScanStores(false) end) end, }, -- Size of sales history [6] = { type = "slider", name = GetString(SK_HISTORY_DEPTH_NAME), tooltip = GetString(SK_HISTORY_DEPTH_TIP), min = 500, max = 7500, getFunc = function() if Shopkeeper.acctSavedVariables.allSettingsAccount then return Shopkeeper.acctSavedVariables.historyDepth else return Shopkeeper.savedVariables.historyDepth end end, setFunc = function(value) if Shopkeeper.acctSavedVariables.allSettingsAccount then Shopkeeper.acctSavedVariables.historyDepth = value else Shopkeeper.savedVariables.historyDepth = value end end, }, -- Font to use [7] = { type = "dropdown", name = GetString(SK_WINDOW_FONT_NAME), tooltip = GetString(SK_WINDOW_FONT_TIP), choices = LMP:List(LMP.MediaType.FONT), getFunc = function() if Shopkeeper.acctSavedVariables.allSettingsAccount then return Shopkeeper.acctSavedVariables.windowFont else return Shopkeeper.savedVariables.windowFont end end, setFunc = function(value) if Shopkeeper.acctSavedVariables.allSettingsAccount then Shopkeeper.acctSavedVariables.windowFont = value else Shopkeeper.savedVariables.windowFont = value end Shopkeeper.UpdateFonts() end, }, -- Make all settings account-wide (or not) [8] = { type = "checkbox", name = GetString(SK_ACCOUNT_WIDE_NAME), tooltip = GetString(SK_ACCOUNT_WIDE_TIP), getFunc = function() return Shopkeeper.acctSavedVariables.allSettingsAccount end, setFunc = function(value) if value then Shopkeeper.acctSavedVariables.showChatAlerts = Shopkeeper.savedVariables.showChatAlerts Shopkeeper.acctSavedVariables.showChatAlerts = Shopkeeper.savedVariables.showMultiple Shopkeeper.acctSavedVariables.openWithMail = Shopkeeper.savedVariables.openWithMail Shopkeeper.acctSavedVariables.openWithStore = Shopkeeper.savedVariables.openWithStore Shopkeeper.acctSavedVariables.showFullPrice = Shopkeeper.savedVariables.showFullPrice Shopkeeper.acctSavedVariables.winLeft = Shopkeeper.savedVariables.winLeft Shopkeeper.acctSavedVariables.winTop = Shopkeeper.savedVariables.winTop Shopkeeper.acctSavedVariables.miniWinLef = Shopkeeper.savedVariables.miniWinLeft Shopkeeper.acctSavedVariables.miniWinTop = Shopkeeper.savedVariables.miniWinTop Shopkeeper.acctSavedVariables.statsWinLeft = Shopkeeper.savedVariables.statsWinLeft Shopkeeper.acctSavedVariables.statsWinTop = Shopkeeper.savedVariables.statsWinTop Shopkeeper.acctSavedVariables.windowFont = Shopkeeper.savedVariables.windowFont Shopkeeper.acctSavedVariables.historyDepth = Shopkeeper.savedVariables.historyDepth Shopkeeper.acctSavedVariables.scanFreq = Shopkeeper.savedVariables.scanFreq Shopkeeper.acctSavedVariables.showAnnounceAlerts = Shopkeeper.savedVariables.showAnnounceAlerts Shopkeeper.acctSavedVariables.alertSoundName = Shopkeeper.savedVariables.alertSoundName Shopkeeper.acctSavedVariables.showUnitPrice = Shopkeeper.savedVariables.showUnitPrice Shopkeeper.acctSavedVariables.viewSize = Shopkeeper.savedVariables.viewSize else Shopkeeper.savedVariables.showChatAlerts = Shopkeeper.acctSavedVariables.showChatAlerts Shopkeeper.savedVariables.showChatAlerts = Shopkeeper.acctSavedVariables.showMultiple Shopkeeper.savedVariables.openWithMail = Shopkeeper.acctSavedVariables.openWithMail Shopkeeper.savedVariables.openWithStore = Shopkeeper.acctSavedVariables.openWithStore Shopkeeper.savedVariables.showFullPrice = Shopkeeper.acctSavedVariables.showFullPrice Shopkeeper.savedVariables.winLeft = Shopkeeper.acctSavedVariables.winLeft Shopkeeper.savedVariables.winTop = Shopkeeper.acctSavedVariables.winTop Shopkeeper.savedVariables.miniWinLef = Shopkeeper.acctSavedVariables.miniWinLeft Shopkeeper.savedVariables.miniWinTop = Shopkeeper.acctSavedVariables.miniWinTop Shopkeeper.savedVariables.statsWinLeft = Shopkeeper.acctSavedVariables.statsWinLeft Shopkeeper.savedVariables.statsWinTop = Shopkeeper.acctSavedVariables.statsWinTop Shopkeeper.savedVariables.windowFont = Shopkeeper.acctSavedVariables.windowFont Shopkeeper.savedVariables.historyDepth = Shopkeeper.acctSavedVariables.historyDepth Shopkeeper.savedVariables.scanFreq = Shopkeeper.acctSavedVariables.scanFreq Shopkeeper.savedVariables.showAnnounceAlerts = Shopkeeper.acctSavedVariables.showAnnounceAlerts Shopkeeper.savedVariables.alertSoundName = Shopkeeper.acctSavedVariables.alertSoundName Shopkeeper.savedVariables.showUnitPrice = Shopkeeper.acctSavedVariables.showUnitPrice Shopkeeper.savedVariables.viewSize = Shopkeeper.acctSavedVariables.viewSize end Shopkeeper.acctSavedVariables.allSettingsAccount = value end, }, } -- And make the options panel LAM:RegisterOptionControls("ShopkeeperOptions", optionsData) end end end -- Filters the ScanResults (or SelfSales) table into the SearchTable, -- using the Shopkeeper.viewMode to determine which one to use. function Shopkeeper.DoSearch(searchText) Shopkeeper.SearchTable = {} local searchTerm = string.lower(searchText) local acctName = GetDisplayName() local tableToUse = Shopkeeper.ScanResults if Shopkeeper.viewMode == "self" then tableToUse = Shopkeeper.SelfSales end -- Actually carry out the search for j = 1, #tableToUse do local result = tableToUse[j] if result then if searchText == nil or searchText == " " then table.insert(Shopkeeper.SearchTable, result) else for i = 1, 3 do local fixedTerm = result[i] if i == 3 then fixedTerm = GetItemLinkName(fixedTerm) end if string.lower(fixedTerm):find(searchTerm) then if result[i] ~= nil then table.insert(Shopkeeper.SearchTable, result) break end end end end end end Shopkeeper.DisplayRows() end -- Switch between all sales and your sales function Shopkeeper.SwitchViewMode() if Shopkeeper.viewMode == "self" then ShopkeeperSwitchViewButton:SetText(GetString(SK_VIEW_YOUR_SALES)) ShopkeeperWindowTitle:SetText("Shopkeeper - " .. GetString(SK_ALL_SALES_TITLE)) ShopkeeperMiniSwitchViewButton:SetText(GetString(SK_VIEW_YOUR_SALES)) ShopkeeperMiniWindowTitle:SetText("Shopkeeper - " .. GetString(SK_ALL_SALES_TITLE)) Shopkeeper.viewMode = "all" else ShopkeeperSwitchViewButton:SetText(GetString(SK_VIEW_ALL_SALES)) ShopkeeperWindowTitle:SetText("Shopkeeper - " .. GetString(SK_YOUR_SALES_TITLE)) ShopkeeperMiniSwitchViewButton:SetText(GetString(SK_VIEW_ALL_SALES)) ShopkeeperMiniWindowTitle:SetText("Shopkeeper - " .. GetString(SK_YOUR_SALES_TITLE)) Shopkeeper.viewMode = "self" end if (Shopkeeper.acctSavedVariables.allSettingsAccount and Shopkeeper.acctSavedVariables.viewSize == "full") or (not Shopkeeper.acctSavedVariables.allSettingsAccount and Shopkeeper.savedVariables.viewSize == "full") then Shopkeeper.DoSearch(ShopkeeperWindowSearchBox:GetText()) else Shopkeeper.DoSearch(ShopkeeperMiniWindowSearchBox:GetText()) end end -- Switch between total price mode and unit price mode function Shopkeeper.SwitchPriceMode() local settingsToUse = Shopkeeper.savedVariables if Shopkeeper.acctSavedVariables.allSettingsAccount then settingsToUse = Shopkeeper.acctSavedVariables end if settingsToUse.showUnitPrice then settingsToUse.showUnitPrice = false ShopkeeperPriceSwitchButton:SetText(GetString(SK_SHOW_UNIT)) ShopkeeperWindowPrice:SetText(GetString(SK_PRICE_COLUMN)) ShopkeeperMiniPriceSwitchButton:SetText(GetString(SK_SHOW_UNIT)) ShopkeeperMiniWindowPrice:SetText(GetString(SK_PRICE_COLUMN)) else settingsToUse.showUnitPrice = true ShopkeeperPriceSwitchButton:SetText(GetString(SK_SHOW_TOTAL)) ShopkeeperWindowPrice:SetText(GetString(SK_PRICE_EACH_COLUMN)) ShopkeeperMiniPriceSwitchButton:SetText(GetString(SK_SHOW_TOTAL)) ShopkeeperMiniWindowPrice:SetText(GetString(SK_PRICE_EACH_COLUMN)) end if Shopkeeper.curSort[1] == "price" then Shopkeeper.SortByPrice(Shopkeeper.curSort[2]) else Shopkeeper.DisplayRows() end end -- Called after store scans complete, updates the search table -- and slider range, then sorts the fresh table. -- Once this is done writes out to the saved variables scan history -- and updates the displayed table, sending a message to chat if -- the scan was initiated via the 'refresh' button. function Shopkeeper:PostScan(doAlert) local settingsToUse = Shopkeeper.savedVariables if Shopkeeper.acctSavedVariables.allSettingsAccount then settingsToUse = Shopkeeper.acctSavedVariables end if settingsToUse.viewSize == "full" then Shopkeeper.DoSearch(ShopkeeperWindowSearchBox:GetText()) else Shopkeeper.DoSearch(ShopkeeperMiniWindowSearchBox:GetText()) end -- Scale the slider's range to the number of items we have minus the number of rows local sliderMax = 0 local tableToUse = Shopkeeper.ScanResults if Shopkeeper.viewMode == "self" then tableToUse = Shopkeeper.SelfSales end if #tableToUse > 15 then sliderMax = (#tableToUse - 15) end ShopkeeperWindowSlider:SetMinMax(0, sliderMax) sliderMax = 0 if #tableToUse > 8 then sliderMax = (#tableToUse - 8) end ShopkeeperMiniWindowSlider:SetMinMax(0, sliderMax) if Shopkeeper.curSort[1] == "time" then Shopkeeper.SortByTime(Shopkeeper.curSort[2]) else Shopkeeper.SortByPrice(Shopkeeper.curSort[2]) end Shopkeeper.acctSavedVariables.scanHistory = Shopkeeper.ScanResults Shopkeeper.DisplayRows() if doAlert then CHAT_SYSTEM:AddMessage("[Shopkeeper] " .. GetString(SK_REFRESH_DONE)) end -- If there's anything in the alert queue, handle it. if #Shopkeeper.alertQueue > 0 then -- Play an alert chime once if there are any alerts in the queue if settingsToUse.showChatAlerts or settingsToUse.showAnnounceAlerts then PlaySound(settingsToUse.alertSoundName) end local numSold = 0 local totalGold = 0 local numAlerts = #Shopkeeper.alertQueue local lastEvent = {} for i = 1, numAlerts do local theEvent = table.remove(Shopkeeper.alertQueue, 1) numSold = numSold + 1 -- Adjust the price if they want the post-cut prices instead local dispPrice = theEvent.salePrice if not settingsToUse.showFullPrice then local cutPrice = price * (1 - (GetTradingHouseCutPercentage() / 100)) dispPrice = math.floor(cutPrice + 0.5) end totalGold = totalGold + dispPrice -- If they want multiple alerts, we'll alert on each loop iteration -- or if there's only one. if settingsToUse.showMultiple or numAlerts == 1 then -- Insert thousands separators for the price local stringPrice = Shopkeeper.localizedNumber(dispPrice) -- On-screen alert if settingsToUse.showAnnounceAlerts then -- We'll add a numerical suffix to avoid queueing two identical messages in a row -- because the alerts will 'miss' if we do local textTime = Shopkeeper.textTimeSince(theEvent.saleTime, true) local alertSuffix = "" if lastEvent[1] ~= nil and theEvent.itemName == lastEvent[1].itemName and textTime == lastEvent[2] then lastEvent[3] = lastEvent[3] + 1 alertSuffix = " (" .. lastEvent[3] .. ")" else lastEvent[1] = theEvent lastEvent[2] = textTime lastEvent[3] = 1 end -- German word order differs so argument order also needs to be changed -- Also due to plurality differences in German, need to differentiate -- single item sold vs. multiple of an item sold. if Shopkeeper.locale == "de" then if theEvent.quant > 1 then CENTER_SCREEN_ANNOUNCE:AddMessage("ShopkeeperAlert", CSA_EVENT_SMALL_TEXT, SOUNDS.NONE, string.format(GetString(SK_SALES_ALERT_COLOR), theEvent.quant, zo_strformat("<<t:1>>", theEvent.itemName), stringPrice, theEvent.guild, textTime) .. alertSuffix) else CENTER_SCREEN_ANNOUNCE:AddMessage("ShopkeeperAlert", CSA_EVENT_SMALL_TEXT, SOUNDS.NONE, string.format(GetString(SK_SALES_ALERT_SINGLE_COLOR),zo_strformat("<<t:1>>", theEvent.itemName), stringPrice, theEvent.guild, textTime) .. alertSuffix) end else CENTER_SCREEN_ANNOUNCE:AddMessage("ShopkeeperAlert", CSA_EVENT_SMALL_TEXT, SOUNDS.NONE, string.format(GetString(SK_SALES_ALERT_COLOR), zo_strformat("<<t:1>>", theEvent.itemName), theEvent.quant, stringPrice, theEvent.guild, textTime) .. alertSuffix) end end -- Chat alert if settingsToUse.showChatAlerts then if Shopkeeper.locale == "de" then if theEvent.quant > 1 then CHAT_SYSTEM:AddMessage(string.format("[Shopkeeper] " .. GetString(SK_SALES_ALERT), theEvent.quant, zo_strformat("<<t:1>>", theEvent.itemName), stringPrice, theEvent.guild, Shopkeeper.textTimeSince(theEvent.saleTime, true))) else CHAT_SYSTEM:AddMessage(string.format("[Shopkeeper] " .. GetString(SK_SALES_ALERT_SINGLE), zo_strformat("<<t:1>>", theEvent.itemName), stringPrice, theEvent.guild, Shopkeeper.textTimeSince(theEvent.saleTime, true))) end else CHAT_SYSTEM:AddMessage(string.format("[Shopkeeper] " .. GetString(SK_SALES_ALERT), zo_strformat("<<t:1>>", theEvent.itemName), theEvent.quant, stringPrice, theEvent.guild, Shopkeeper.textTimeSince(theEvent.saleTime, true))) end end end end -- Otherwise, we'll just alert once with a summary at the end if not settingsToUse.showMultiple and numAlerts > 1 then -- Insert thousands separators for the price local stringPrice = Shopkeeper.localizedNumber(totalGold) if settingsToUse.showAnnounceAlerts then CENTER_SCREEN_ANNOUNCE:AddMessage("ShopkeeperAlert", CSA_EVENT_SMALL_TEXT, settingsToUse.alertSoundName, string.format(GetString(SK_SALES_ALERT_GROUP_COLOR), numSold, stringPrice)) else CHAT_SYSTEM:AddMessage(string.format("[Shopkeeper] " .. GetString(SK_SALES_ALERT_GROUP), numSold, stringPrice)) end end end -- Finally, update the table Shopkeeper.DisplayRows() end -- Makes sure all the necessary data is there, and adds the passed-in event theEvent -- to the ScanResults and SelfSales table. If doAlert is true, also adds it to -- alertQueue, which means an alert may fire during PostScan. function Shopkeeper:InsertEvent(theEvent, doAlert) local thePlayer = string.lower(GetDisplayName()) local settingsToUse = Shopkeeper.savedVariables if Shopkeeper.acctSavedVariables.allSettingsAccount then settingsToUse = Shopkeeper.acctSavedVariables end if theEvent.itemName ~= nil and theEvent.seller ~= nil and theEvent.buyer ~= nil and theEvent.salePrice ~= nil then -- Grab the icon local itemIcon, _, _, _ = GetItemLinkInfo(theEvent.itemName) -- Insert the entry into the ScanResults table table.insert(Shopkeeper.ScanResults, {theEvent.buyer, theEvent.guild, theEvent.itemName, itemIcon, theEvent.quant, theEvent.saleTime, theEvent.salePrice, theEvent.seller, theEvent.kioskSale}) -- And then, if it's the player's sale, insert into that table if string.lower(theEvent.seller) == thePlayer then table.insert(Shopkeeper.SelfSales, {theEvent.buyer, theEvent.guild, theEvent.itemName, itemIcon, theEvent.quant, theEvent.saleTime, theEvent.salePrice, theEvent.seller, theEvent.kioskSale}) if doAlert and (settingsToUse.showChatAlerts or settingsToUse.showAnnounceAlerts) then table.insert(Shopkeeper.alertQueue, theEvent) end end end end -- Actually carries out of the scan of a specific guild store's sales history. -- Grabs all the members of the guild first to determine if a sale came from the -- guild's kiosk (guild trader) or not. -- Calls InsertEvent to actually insert the event into the ScanResults and SelfSales -- tables. -- Afterwards, will start scan of the next guild or call postscan if no more guilds. function Shopkeeper:DoScan(guildNum, checkOlder, doAlert) local guildID = GetGuildId(guildNum) local numEvents = GetNumGuildEvents(guildID, GUILD_HISTORY_SALES) local guildName = GetGuildName(guildID) local prevEvents = 0 if Shopkeeper.numEvents[guildName] ~= nil then prevEvents = Shopkeeper.numEvents[guildName] end if numEvents > prevEvents then local guildMemberInfo = {} -- Index the table with the account names themselves as they're -- (hopefully!) unique - search much faster for i = 1, GetNumGuildMembers(guildID) do local guildMemInfo, _, _, _, _ = GetGuildMemberInfo(guildID, i) guildMemberInfo[string.lower(guildMemInfo)] = true end for i = (prevEvents + 1), numEvents do local theEvent = {} _, theEvent.secsSince, theEvent.seller, theEvent.buyer, theEvent.quant, theEvent.itemName, theEvent.salePrice, _ = GetGuildEventInfo(guildID, GUILD_HISTORY_SALES, i) theEvent.guild = guildName theEvent.saleTime = GetTimeStamp() - theEvent.secsSince -- If we didn't add an entry to guildMemberInfo earlier setting the -- buyer's name index to true, then this was either bought at a kiosk -- or the buyer left the guild after buying but before we scanned. -- Close enough! theEvent.kioskSale = (guildMemberInfo[string.lower(theEvent.buyer)] == nil) -- If we're doing a deep scan, revert to timestamp checking -- For reasons I cannot determine, I am getting items not previously -- seen up to 10 seconds BEFORE the last call to RequestGuildHistoryCategoryNewest. -- Bizarre, but adding this fudge factor hasn't resulted in dupes...yet. if checkOlder then if Shopkeeper.acctSavedVariables.lastScan[guildName] == nil or GetDiffBetweenTimeStamps(theEvent.saleTime, Shopkeeper.acctSavedVariables.lastScan[guildName]) >= -8 then Shopkeeper:InsertEvent(theEvent, false) end -- Otherwise, all new events are assumed good -- Inspiration for event number-based handling from sirinsidiator else Shopkeeper:InsertEvent(theEvent, true) end end end -- We got through any new (to us) events, so update the timestamp and number of events Shopkeeper.acctSavedVariables.lastScan[guildName] = Shopkeeper.requestTimestamp Shopkeeper.numEvents[guildName] = numEvents -- If we have another guild to scan, see if we need to check older and scan it if guildNum < GetNumGuilds() then local nextGuild = guildNum + 1 local nextGuildID = GetGuildId(nextGuild) local nextGuildName = GetGuildName(nextGuildID) -- Transition from numerical (old-style) indexing of last scan times -- May go away eventually if Shopkeeper.acctSavedVariables.lastScan[nextGuild] ~= nil then Shopkeeper.acctSavedVariables.lastScan[nextGuildName] = Shopkeeper.acctSavedVariables.lastScan[nextGuild] Shopkeeper.acctSavedVariables.lastScan[nextGuild] = nil end -- If we don't have any event info for the next guild, do a deep scan local nextCheckOlder = (Shopkeeper.numEvents[nextGuildName] == nil or Shopkeeper.numEvents[nextGuildName] == 0) Shopkeeper.requestTimestamp = GetTimeStamp() RequestGuildHistoryCategoryNewest(nextGuildID, GUILD_HISTORY_SALES) if nextCheckOlder then zo_callLater(function() Shopkeeper:ScanOlder(nextGuild, doAlert) end, 1500) else zo_callLater(function() Shopkeeper:DoScan(nextGuild, false, doAlert) end, 1500) end -- Otherwise, start the postscan routines else Shopkeeper.isScanning = false Shopkeeper:PostScan(doAlert) end end -- Repeatedly checks for older events until there aren't anymore, -- then calls DoScan to pick up sales events function Shopkeeper:ScanOlder(guildNum, doAlert) local guildID = GetGuildId(guildNum) local numEvents = GetNumGuildEvents(guildID, GUILD_HISTORY_SALES) if numEvents > 0 then if DoesGuildHistoryCategoryHaveMoreEvents(guildID, GUILD_HISTORY_SALES) then local newRequestTime = GetTimeStamp() RequestGuildHistoryCategoryOlder(guildID, GUILD_HISTORY_SALES) zo_callLater(function() Shopkeeper:ScanOlder(guildNum, doAlert) end, 1500) else zo_callLater(function() Shopkeeper:DoScan(guildNum, true, doAlert) end, 1500) end else zo_callLater(function() Shopkeeper:DoScan(guildNum, true, doAlert) end, 1500) end end -- Scans all stores a player has access to with delays between them. function Shopkeeper:ScanStores(doAlert) -- If it's been less than 45 seconds since we last scanned the store, -- don't do it again so we don't hammer the server either accidentally -- or on purpose local timeLimit = GetTimeStamp() - 45 local guildNum = GetNumGuilds() -- Nothing to scan! if guildNum == 0 then return end -- Grab some info about the first guild (since we now know there's at least one) local firstGuildID = GetGuildId(1) local firstGuildName = GetGuildName(firstGuildID) -- Transition from numerical (old-style) indexing of last scan times -- May go away at some point if Shopkeeper.acctSavedVariables.lastScan[1] ~= nil then Shopkeeper.acctSavedVariables.lastScan[firstGuildName] = Shopkeeper.acctSavedVariables.lastScan[1] Shopkeeper.acctSavedVariables.lastScan[1] = nil end -- Right, let's actually request some events, assuming we haven't already done so recently if not Shopkeeper.isScanning and ((Shopkeeper.acctSavedVariables.lastScan[firstGuildName] == nil) or (timeLimit > Shopkeeper.acctSavedVariables.lastScan[firstGuildName])) then Shopkeeper.isScanning = true local checkOlder = false -- If we have no event info for this guild, let's do a full scan if Shopkeeper.numEvents[firstGuildName] == nil or Shopkeeper.numEvents[firstGuildName] == 0 then checkOlder = true end Shopkeeper.requestTimestamp = GetTimeStamp() RequestGuildHistoryCategoryNewest(firstGuildID, GUILD_HISTORY_SALES) if checkOlder then zo_callLater(function() Shopkeeper:ScanOlder(1, doAlert) end, 1500) else zo_callLater(function() Shopkeeper:DoScan(1, false, doAlert) end, 1500) end end end -- Handle the refresh button - do a scan if it's been more than a minute -- since the last successful one. function Shopkeeper.DoRefresh() local timeStamp = GetTimeStamp() -- If it's been less than a minute since we last scanned the store, -- don't do it again so we don't hammer the server either accidentally -- or on purpose local timeLimit = timeStamp - 59 local guildNum = GetNumGuilds() if guildNum > 0 then local firstGuildName = GetGuildName(1) if Shopkeeper.acctSavedVariables.lastScan[firstGuildName] == nil or timeLimit > Shopkeeper.acctSavedVariables.lastScan[firstGuildName] then CHAT_SYSTEM:AddMessage("[Shopkeeper] " .. GetString(SK_REFRESH_START)) Shopkeeper:ScanStores(true) else CHAT_SYSTEM:AddMessage("[Shopkeeper] " .. GetString(SK_REFRESH_WAIT)) end end end -- Handle the reset button - clear out the search and scan tables, -- and set the time of the last scan to nil. The next interval scan -- will grab the sales histories from each guild to re-populate. function Shopkeeper.DoReset() Shopkeeper.ScanResults = {} Shopkeeper.SelfSales = {} Shopkeeper.SearchTable = {} Shopkeeper.acctSavedVariables.scanHistory = {} Shopkeeper.acctSavedVariables.lastScan = {} Shopkeeper.DisplayRows() Shopkeeper.isScanning = false Shopkeeper.numEvents = {} CHAT_SYSTEM:AddMessage("[Shopkeeper] " .. GetString(SK_RESET_DONE)) CHAT_SYSTEM:AddMessage("[Shopkeeper] " .. GetString(SK_REFRESH_START)) Shopkeeper:ScanStores(true) end -- Set up the labels and tooltips from translation files and do a couple other UI -- setup routines function Shopkeeper:SetupShopkeeperWindow() local settingsToUse = Shopkeeper.savedVariables if Shopkeeper.acctSavedVariables.allSettingsAccount then settingsToUse = Shopkeeper.acctSavedVariables end -- Shopkeeper button in guild store screen local reopenShopkeeper = CreateControlFromVirtual("ShopkeeperReopenButton", ZO_TradingHouseLeftPane, "ZO_DefaultButton") reopenShopkeeper:SetAnchor(CENTER, ZO_TradingHouseLeftPane, BOTTOM, 0, 5) reopenShopkeeper:SetWidth(200) reopenShopkeeper:SetText("Shopkeeper") reopenShopkeeper:SetHandler("OnClicked", Shopkeeper.ToggleShopkeeperWindow) -- Shopkeeper button in mail screen local shopkeeperMail = CreateControlFromVirtual("ShopkeeperMailButton", ZO_MailInbox, "ZO_DefaultButton") shopkeeperMail:SetAnchor(TOPLEFT, ZO_MailInbox, TOPLEFT, 100, 4) shopkeeperMail:SetWidth(200) shopkeeperMail:SetText("Shopkeeper") shopkeeperMail:SetHandler("OnClicked", Shopkeeper.ToggleShopkeeperWindow) -- Stats dropdown choice box local shopkeeperStatsGuild = CreateControlFromVirtual("ShopkeeperStatsGuildChooser", ShopkeeperStatsWindow, "ShopkeeperStatsGuildDropdown") shopkeeperStatsGuild:SetDimensions(270,25) shopkeeperStatsGuild:SetAnchor(LEFT, ShopkeeperStatsWindowGuildChooserLabel, RIGHT, 5, 0) shopkeeperStatsGuild.m_comboBox:SetSortsItems(false) -- Set column headers and search label from translation ShopkeeperWindowBuyer:SetText(GetString(SK_BUYER_COLUMN)) ShopkeeperWindowGuild:SetText(GetString(SK_GUILD_COLUMN)) ShopkeeperWindowItemName:SetText(GetString(SK_ITEM_COLUMN)) ShopkeeperWindowSellTime:SetText(GetString(SK_TIME_COLUMN)) ShopkeeperMiniWindowGuild:SetText(GetString(SK_GUILD_COLUMN)) ShopkeeperMiniWindowItemName:SetText(GetString(SK_ITEM_COLUMN)) ShopkeeperMiniWindowSellTime:SetText(GetString(SK_TIME_COLUMN)) if settingsToUse.showUnitPrice then ShopkeeperWindowPrice:SetText(GetString(SK_PRICE_EACH_COLUMN)) ShopkeeperMiniWindowPrice:SetText(GetString(SK_PRICE_EACH_COLUMN)) else ShopkeeperWindowPrice:SetText(GetString(SK_PRICE_COLUMN)) ShopkeeperMiniWindowPrice:SetText(GetString(SK_PRICE_COLUMN)) end -- Set second half of window title from translation ShopkeeperWindowTitle:SetText("Shopkeeper - " .. GetString(SK_YOUR_SALES_TITLE)) ShopkeeperMiniWindowTitle:SetText("Shopkeeper - " .. GetString(SK_YOUR_SALES_TITLE)) -- And set the stats window title and slider label from translation ShopkeeperStatsWindowTitle:SetText("Shopkeeper " .. GetString(SK_STATS_TITLE)) ShopkeeperStatsWindowGuildChooserLabel:SetText(GetString(SK_GUILD_COLUMN) .. ": ") ShopkeeperStatsWindowSliderLabel:SetText(GetString(SK_STATS_DAYS)) -- Set up some helpful tooltips for the Buyer, Item, Time, and Price column headers ShopkeeperWindowBuyer:SetHandler("OnMouseEnter", function(self) ZO_Tooltips_ShowTextTooltip(self, TOP, GetString(SK_BUYER_TOOLTIP)) end) ShopkeeperWindowItemName:SetHandler("OnMouseEnter", function(self) ZO_Tooltips_ShowTextTooltip(self, TOP, GetString(SK_ITEM_TOOLTIP)) end) ShopkeeperMiniWindowItemName:SetHandler("OnMouseEnter", function(self) ZO_Tooltips_ShowTextTooltip(self, TOP, GetString(SK_ITEM_TOOLTIP)) end) ShopkeeperWindowSellTime:SetHandler("OnMouseEnter", function(self) ZO_Tooltips_ShowTextTooltip(self, TOP, GetString(SK_SORT_TIME_TOOLTIP)) end) ShopkeeperMiniWindowSellTime:SetHandler("OnMouseEnter", function(self) ZO_Tooltips_ShowTextTooltip(self, TOP, GetString(SK_SORT_TIME_TOOLTIP)) end) ShopkeeperWindowPrice:SetHandler("OnMouseEnter", function(self) ZO_Tooltips_ShowTextTooltip(self, TOP, GetString(SK_SORT_PRICE_TOOLTIP)) end) ShopkeeperMiniWindowPrice:SetHandler("OnMouseEnter", function(self) ZO_Tooltips_ShowTextTooltip(self, TOP, GetString(SK_SORT_PRICE_TOOLTIP)) end) -- View switch button ShopkeeperSwitchViewButton:SetText(GetString(SK_VIEW_ALL_SALES)) ShopkeeperMiniSwitchViewButton:SetText(GetString(SK_VIEW_ALL_SALES)) -- Total / unit price switch button if settingsToUse.showUnitPrice then ShopkeeperPriceSwitchButton:SetText(GetString(SK_SHOW_TOTAL)) ShopkeeperMiniPriceSwitchButton:SetText(GetString(SK_SHOW_TOTAL)) else ShopkeeperPriceSwitchButton:SetText(GetString(SK_SHOW_UNIT)) ShopkeeperMiniPriceSwitchButton:SetText(GetString(SK_SHOW_UNIT)) end -- Refresh button ShopkeeperRefreshButton:SetText(GetString(SK_REFRESH_LABEL)) ShopkeeperMiniRefreshButton:SetText(GetString(SK_REFRESH_LABEL)) -- Reset button ShopkeeperResetButton:SetText(GetString(SK_RESET_LABEL)) ShopkeeperMiniResetButton:SetText(GetString(SK_RESET_LABEL)) -- Make the 15 rows that comprise the visible table if #Shopkeeper.DataRows == 0 then local dataRowOffsetX = 25 local dataRowOffsetY = 74 for i = 1, 15 do local dRow = CreateControlFromVirtual("ShopkeeperDataRow", ShopkeeperWindow, "ShopkeeperDataRow", i) dRow:SetSimpleAnchorParent(dataRowOffsetX, dataRowOffsetY+((dRow:GetHeight()+2)*(i-1))) Shopkeeper.DataRows[i] = dRow end end -- And 8 for the mini window if #Shopkeeper.MiniDataRows == 0 then local dataRowOffsetX = 10 local dataRowOffsetY = 74 for i = 1, 8 do local dRow = CreateControlFromVirtual("ShopkeeperMiniDataRow", ShopkeeperMiniWindow, "ShopkeeperMiniDataRow", i) dRow:SetSimpleAnchorParent(dataRowOffsetX, dataRowOffsetY+((dRow:GetHeight()+2)*(i-1))) Shopkeeper.MiniDataRows[i] = dRow end end -- Stats buttons ShopkeeperWindowStatsButton:SetHandler("OnMouseEnter", function(self) ZO_Tooltips_ShowTextTooltip(self, TOP, GetString(SK_STATS_TOOLTIP)) end) ShopkeeperMiniWindowStatsButton:SetHandler("OnMouseEnter", function(self) ZO_Tooltips_ShowTextTooltip(self, TOP, GetString(SK_STATS_TOOLTIP)) end) -- View size change buttons ShopkeeperWindowViewSizeButton:SetHandler("OnMouseEnter", function(self) ZO_Tooltips_ShowTextTooltip(self, TOP, GetString(SK_SIZE_TOOLTIP)) end) ShopkeeperMiniWindowViewSizeButton:SetHandler("OnMouseEnter", function(self) ZO_Tooltips_ShowTextTooltip(self, TOP, GetString(SK_SIZE_TOOLTIP)) end) -- Slider setup ShopkeeperWindow:SetHandler("OnMouseWheel", Shopkeeper.OnSliderMouseWheel) ShopkeeperWindowSlider:SetValue(0) ShopkeeperMiniWindow:SetHandler("OnMouseWheel", Shopkeeper.OnSliderMouseWheel) ShopkeeperMiniWindowSlider:SetValue(0) ShopkeeperStatsWindowSlider:SetValue(0) -- Search handler ZO_PreHookHandler(ShopkeeperWindowSearchBox, "OnTextChanged", function(self) Shopkeeper.DoSearch(ShopkeeperWindowSearchBox:GetText()) end) ZO_PreHookHandler(ShopkeeperMiniWindowSearchBox, "OnTextChanged", function(self) Shopkeeper.DoSearch(ShopkeeperMiniWindowSearchBox:GetText()) end) -- We're all set, so make sure we're using the right font and then update the UI Shopkeeper.windowFont = settingsToUse.windowFont Shopkeeper:UpdateFonts() Shopkeeper.DisplayRows() end -- Init function function Shopkeeper:Initialize() -- SavedVar defaults local Defaults = { ["showChatAlerts"] = false, ["showMultiple"] = true, ["openWithMail"] = true, ["openWithStore"] = true, ["showFullPrice"] = true, ["winLeft"] = 30, ["winTop"] = 30, ["miniWinLeft"] = 30, ["miniWinTop"] = 30, ["statsWinLeft"] = 720, ["statsWinTop"] = 820, ["windowFont"] = "ProseAntique", ["historyDepth"] = 3000, ["scanFreq"] = 120, ["showAnnounceAlerts"] = true, ["alertSoundName"] = "Book_Acquired", ["showUnitPrice"] = false, ["viewSize"] = "full", } local acctDefaults = { ["lastScan"] = {}, ["scanHistory"] = {}, ["allSettingsAccount"] = false, ["showChatAlerts"] = false, ["showMultiple"] = true, ["openWithMail"] = true, ["openWithStore"] = true, ["showFullPrice"] = true, ["winLeft"] = 30, ["winTop"] = 30, ["miniWinLeft"] = 30, ["miniWinTop"] = 30, ["statsWinLeft"] = 720, ["statsWinTop"] = 820, ["windowFont"] = "ProseAntique", ["historyDepth"] = 3000, ["scanFreq"] = 120, ["showAnnounceAlerts"] = true, ["alertSoundName"] = "Book_Acquired", ["showUnitPrice"] = false, ["viewSize"] = "full", } -- Populate savedVariables and fill the ScanResults table from savedvars self.savedVariables = ZO_SavedVars:New("ShopkeeperSavedVars", 1, GetDisplayName(), Defaults) self.acctSavedVariables = ZO_SavedVars:NewAccountWide("ShopkeeperSavedVars", 1, GetDisplayName(), acctDefaults) self.ScanResults = Shopkeeper.acctSavedVariables.scanHistory -- Update the lastScan value, as it was previously a number in older versions -- (may get removed eventually) if type(self.acctSavedVariables.lastScan) == "number" then local guildNum = GetNumGuilds() local oldStamp = Shopkeeper.acctSavedVariables.lastScan self.acctSavedVariables.lastScan = {} for i = 1, guildNum do self.acctSavedVariables.lastScan[GetGuildName(GetGuildId(i))] = oldStamp end end -- Setup the options menu and main windows self:LibAddonInit() self:SetupShopkeeperWindow() self:RestoreWindowPosition() self.SortByTime("desc") -- We'll grab their locale now, it's really only used for a couple things as -- most localization is handled by the i18n/$(language).lua files Shopkeeper.locale = GetCVar('Language.2') if Shopkeeper.locale ~= "en" and Shopkeeper.locale ~= "de" and Shopkeeper.locale ~= "fr" then Shopkeeper.locale = "en" end -- Rather than constantly managing the length of the history, we'll just -- truncate it once at init-time since we now have it sorted. As a result -- it will fluctuate in size depending on how active guild stores are and -- how long someone plays for at a time, but that's OK as it shouldn't impact -- performance too severely unless someone plays for 24+ hours straight local historyDepth = self.savedVariables.historyDepth if self.acctSavedVariables.allSettingsAccount then historyDepth = self.acctSavedVariables.historyDepth end if #self.ScanResults > historyDepth then for i = (historyDepth + 1), #self.ScanResults do table.remove(self.ScanResults) end end -- Now that we've truncated, populate the SelfSales table local loggedInAccount = string.lower(GetDisplayName()) for i = 1, #self.ScanResults do if string.lower(self.ScanResults[i][8]) == loggedInAccount then table.insert(self.SelfSales, self.ScanResults[i]) end end -- Populate the search table self.DoSearch(nil) -- Add the shopkeeper window to the mail and trading house scenes if the -- player's settings indicate they want that behavior self.uiFragment = ZO_FadeSceneFragment:New(ShopkeeperWindow) self.miniUiFragment = ZO_FadeSceneFragment:New(ShopkeeperMiniWindow) local settingsToUse = Shopkeeper.savedVariables if Shopkeeper.acctSavedVariables.allSettingsAccount then settingsToUse = Shopkeeper.acctSavedVariables end if settingsToUse.openWithMail then if settingsToUse.viewSize == "full" then MAIL_INBOX_SCENE:AddFragment(Shopkeeper.uiFragment) MAIL_SEND_SCENE:AddFragment(Shopkeeper.uiFragment) else MAIL_INBOX_SCENE:AddFragment(Shopkeeper.miniUiFragment) MAIL_SEND_SCENE:AddFragment(Shopkeeper.miniUiFragment) end end if settingsToUse.openWithStore then if settingsToUse.viewSize == "full" then TRADING_HOUSE_SCENE:AddFragment(Shopkeeper.uiFragment) else TRADING_HOUSE_SCENE:AddFragment(Shopkeeper.miniUiFragment) end end -- Because we allow manual toggling of the Shopkeeper window in those scenes (without -- making that setting permanent), we also have to hide the window on closing them -- if they're not part of the scene. EVENT_MANAGER:RegisterForEvent(Shopkeeper.name, EVENT_MAIL_CLOSE_MAILBOX, function() if settingsToUse.openWithMail then ShopkeeperWindow:SetHidden(true) ShopkeeperMiniWindow:SetHidden(true) end end) EVENT_MANAGER:RegisterForEvent(Shopkeeper.name, EVENT_CLOSE_TRADING_HOUSE, function() if settingsToUse.openWithStore then ShopkeeperWindow:SetHidden(true) ShopkeeperMiniWindow:SetHidden(true) end end) -- We also want to make sure the Shopkeeper windows are hidden in the game menu ZO_PreHookHandler(ZO_GameMenu_InGame, "OnShow", function() ShopkeeperWindow:SetHidden(true) ShopkeeperStatsWindow:SetHidden(true) ShopkeeperMiniWindow:SetHidden(true) end) -- I could do this with action layer pop/push, but it's kind've a pain -- when it's just these I want to hook EVENT_MANAGER:RegisterForEvent(Shopkeeper.name, EVENT_CLOSE_BANK, function() ShopkeeperWindow:SetHidden(true) ShopkeeperMiniWindow:SetHidden(true) end) EVENT_MANAGER:RegisterForEvent(Shopkeeper.name, EVENT_CLOSE_GUILD_BANK, function() ShopkeeperWindow:SetHidden(true) ShopkeeperMiniWindow:SetHidden(true) end) EVENT_MANAGER:RegisterForEvent(Shopkeeper.name, EVENT_CLOSE_STORE, function() ShopkeeperWindow:SetHidden(true) ShopkeeperMiniWindow:SetHidden(true) end) EVENT_MANAGER:RegisterForEvent(Shopkeeper.name, EVENT_END_CRAFTING_STATION_INTERACT, function() ShopkeeperWindow:SetHidden(true) ShopkeeperMiniWindow:SetHidden(true) end) -- RegisterForUpdate lets us scan at a given interval (in ms), so we'll use that to -- keep the sales history updated local scanInterval = self.savedVariables.scanFreq * 1000 if self.acctSavedVariables.allSettingsAccount then scanInterval = self.acctSavedVariables.scanFreq * 1000 end EVENT_MANAGER:RegisterForUpdate(Shopkeeper.name, scanInterval, function() Shopkeeper:ScanStores(false) end) -- Right, we're all set up, so give the client a few seconds to catch up on everything -- and then do an initial (deep) scan in case it's been a while since the player -- logged on. zo_callLater(function() Shopkeeper:ScanStores(false) end, 5000) end -- Event handler for the OnAddOnLoaded event function Shopkeeper.OnAddOnLoaded(event, addonName) if addonName == Shopkeeper.name then Shopkeeper:Initialize() end end -- Register for the OnAddOnLoaded event EVENT_MANAGER:RegisterForEvent(Shopkeeper.name, EVENT_ADD_ON_LOADED, Shopkeeper.OnAddOnLoaded) -- Set up /shopkeeper as a slash command toggle for the main window SLASH_COMMANDS["/shopkeeper"] = function() Shopkeeper.ToggleShopkeeperWindow() end