-- Shopkeeper Main Addon File -- Last Updated August 4, 2014 -- Written July 2014 by Dan Stone (@khaibit) - dankitymao@gmail.com -- Released under terms in license accompanying this file. -- Distribution without license is prohibited! -- Workaround because GetDisplayName() is broken -- Maybe not needed anymore, will test function Shopkeeper.GetAccountName() local acctName = GetDisplayName() if acctName == nil or acctName == "" then -- Hopefully, they're in a guild and we can use GetPlayerGuildMemberIndex if GetNumGuilds() > 0 then acctName = GetGuildMemberInfo(GetGuildId(1), GetPlayerGuildMemberIndex(GetGuildId(1))) -- But if they're in no guilds, we have to use the AccountName CVar, which only works -- if they saved their account name on the login page. Best we can do with patch 1.2.3 -- breaking GetDisplayName(), but if they aren't in any guilds, why are they using this -- addon anyway? else acctName = "@" .. GetCVar("AccountName") end end return acctName end -- Translate from the i18n table function Shopkeeper.translate(stringName) local result = Shopkeeper.i18n.localized[stringName] assert(result, ("The id %q was not found in the current locale"):format(stringName)) return result end function Shopkeeper.localizedNumber(numberValue) local stringPrice = numberValue -- Insert thousands separators for the price -- local stringPrice = numberValue local subString = "%1" .. Shopkeeper.translate("thousandsSep") .."%2" while true do stringPrice, k = string.gsub(stringPrice, "^(-?%d+)(%d%d%d)", subString) if (k == 0) then break end end return stringPrice end -- Create a textual representation of a time interval -- (X and Y) or Z in LUA is the equivalent of C-style -- ternary syntax X ? Y : Z so long as Y is not false or nil function Shopkeeper.textTimeSince(theTime, useLowercase) local secsSince = GetTimeStamp() - theTime if secsSince < 60 then return ((useLowercase and zo_strformat(Shopkeeper.translate('timeSecondsAgoLC'), secsSince)) or zo_strformat(Shopkeeper.translate('timeSecondsAgo'), secsSince)) elseif secsSince < 3600 then return ((useLowercase and zo_strformat(Shopkeeper.translate('timeMinutesAgoLC'), math.floor(secsSince / 60.0))) or zo_strformat(Shopkeeper.translate('timeMinutesAgo'), math.floor(secsSince / 60.0))) elseif secsSince < 86400 then return ((useLowercase and zo_strformat(Shopkeeper.translate('timeHoursAgoLC'), math.floor(secsSince / 3600.0))) or zo_strformat(Shopkeeper.translate('timeHoursAgo'), math.floor(secsSince / 3600.0))) else return ((useLowercase and zo_strformat(Shopkeeper.translate('timeDaysAgoLC'), math.floor(secsSince / 86400.0))) or zo_strformat(Shopkeeper.translate('timeDaysAgo'), math.floor(secsSince / 86400.0))) end end -- A utility function to grab all the keys of the sound table -- to populate the options dropdown function Shopkeeper.soundKeys() local keyList = {} local keyIndex = 0 for i = 1, #Shopkeeper.alertSounds do keyIndex = keyIndex + 1 keyList[keyIndex] = Shopkeeper.alertSounds[i].name end return keyList end -- A utility function to find the key associated with a given value in -- the sounds table. Best we can do is a linear search unfortunately, -- but it's a small table. function Shopkeeper.searchSounds(sound) for i, theSound in ipairs(Shopkeeper.alertSounds) do if theSound.sound == sound then return theSound.name end end -- If we hit this point, we didn't find what we were looking for return nil end function Shopkeeper.searchSoundNames(name) for i,theSound in ipairs(Shopkeeper.alertSounds) do if theSound.name == name then return theSound.sound end end end -- Handle the OnMoveStop event for the window function Shopkeeper.OnWindowMoveStop() Shopkeeper.savedVariables.winLeft = ShopkeeperWindow:GetLeft() Shopkeeper.savedVariables.winTop = ShopkeeperWindow:GetTop() Shopkeeper.savedVariables.miniWinLeft = ShopkeeperMiniWindow:GetLeft() Shopkeeper.savedVariables.miniWinTop = ShopkeeperMiniWindow:GetTop() end function Shopkeeper.OnStatsWindowMoveStop() Shopkeeper.savedVariables.statsWinLeft = ShopkeeperStatsWindow:GetLeft() Shopkeeper.savedVariables.statsWinTop = ShopkeeperStatsWindow:GetTop() end -- Restore the window position from saved vars function Shopkeeper:RestoreWindowPosition() local left = self.savedVariables.winLeft local top = self.savedVariables.winTop local statsLeft = self.savedVariables.statsWinLeft local statsTop = self.savedVariables.statsWinTop local miniLeft = self.savedVariables.miniWinLeft local miniTop = self.savedVariables.miniWinTop ShopkeeperWindow:ClearAnchors() ShopkeeperStatsWindow:ClearAnchors() ShopkeeperMiniWindow:ClearAnchors() ShopkeeperWindow:SetAnchor(TOPLEFT, GuiRoot, TOPLEFT, left, top) ShopkeeperStatsWindow:SetAnchor(TOPLEFT, GuiRoot, TOPLEFT, statsLeft, statsTop) ShopkeeperMiniWindow:SetAnchor(TOPLEFT, GuiRoot, TOPLEFT, miniLeft, miniTop) end -- Handle the changing of main window font settings function Shopkeeper:UpdateFonts() local LMP = LibStub("LibMediaProvider-1.0") if LMP then local font = LMP:Fetch('font', Shopkeeper.savedVariables.windowFont) local look = string.format('%s|16', font) local titleLook = string.format('%s|22', font) local headerLook = string.format('%s|20', font) local miniLook = string.format('%s|12', font) local miniTitleLook = string.format('%s|18', font) local miniHeaderLook = string.format('%s|16', font) ShopkeeperWindowSearchLabel:SetFont(look) ShopkeeperWindowSearchBox:SetFont(look) ShopkeeperWindowTitle:SetFont(titleLook) ShopkeeperWindowBuyer:SetFont(headerLook) ShopkeeperWindowGuild:SetFont(headerLook) ShopkeeperWindowItemName:SetFont(headerLook) ShopkeeperWindowSellTime:SetFont(headerLook) ShopkeeperWindowPrice:SetFont(headerLook) ShopkeeperSwitchViewButton:SetFont(look) ShopkeeperPriceSwitchButton:SetFont(look) ShopkeeperResetButton:SetFont(look) ShopkeeperRefreshButton:SetFont(look) ShopkeeperMiniWindowSearchLabel:SetFont(miniLook) ShopkeeperMiniWindowSearchBox:SetFont(miniLook) ShopkeeperMiniWindowTitle:SetFont(miniTitleLook) ShopkeeperMiniWindowGuild:SetFont(miniHeaderLook) ShopkeeperMiniWindowItemName:SetFont(miniHeaderLook) ShopkeeperMiniWindowSellTime:SetFont(miniHeaderLook) ShopkeeperMiniWindowPrice:SetFont(miniHeaderLook) ShopkeeperMiniSwitchViewButton:SetFont(miniLook) ShopkeeperMiniPriceSwitchButton:SetFont(miniLook) ShopkeeperMiniResetButton:SetFont(miniLook) ShopkeeperMiniRefreshButton:SetFont(miniLook) ShopkeeperStatsWindowTitle:SetFont(titleLook) ShopkeeperStatsWindowItemsSoldLabel:SetFont(look) ShopkeeperStatsWindowTotalGoldLabel:SetFont(look) ShopkeeperStatsWindowBiggestSaleLabel:SetFont(look) ShopkeeperStatsWindowSliderSettingLabel:SetFont(look) ShopkeeperStatsWindowSliderLabel:SetFont(look) for i = 1, #Shopkeeper.DataRows do local dataRow = Shopkeeper.DataRows[i] dataRow:GetNamedChild("Buyer"):SetFont(look) dataRow:GetNamedChild("Guild"):SetFont(look) dataRow:GetNamedChild("ItemName"):SetFont(look) dataRow:GetNamedChild("Quantity"):SetFont(look) dataRow:GetNamedChild("SellTime"):SetFont(look) dataRow:GetNamedChild("Price"):SetFont(look) if i <= #Shopkeeper.MiniDataRows then local miniDataRow = Shopkeeper.MiniDataRows[i] miniDataRow:GetNamedChild("Guild"):SetFont(miniLook) miniDataRow:GetNamedChild("ItemName"):SetFont(miniLook) miniDataRow:GetNamedChild("Quantity"):SetFont(miniLook) miniDataRow:GetNamedChild("SellTime"):SetFont(miniLook) miniDataRow:GetNamedChild("Price"):SetFont(miniLook) end end end end -- Item tooltips function Shopkeeper:ShowToolTip(itemName, itemButton) InitializeTooltip(ItemTooltip, itemButton) ItemTooltip:SetLink(itemName) end -- Clear a given row's data function Shopkeeper.ClearDataRow(index) if index < 1 or index > 15 then return end local dataRow = Shopkeeper.DataRows[index] dataRow:GetNamedChild("Buyer"):SetText("") dataRow:GetNamedChild("Buyer"):SetHandler("OnMouseDoubleClick", nil) dataRow:GetNamedChild("Guild"):SetText("") dataRow:GetNamedChild("ItemIcon"):SetTexture(nil) dataRow:GetNamedChild("ItemIcon"):SetHidden(true) local itemCell = dataRow:GetNamedChild("ItemName") itemCell:SetText("") itemCell:SetHandler("OnMouseDoubleClick", nil) itemCell:SetHandler("OnMouseEnter", nil) dataRow:GetNamedChild("Quantity"):SetText("") dataRow:GetNamedChild("SellTime"):SetText("") dataRow:GetNamedChild("Price"):SetText("") end function Shopkeeper.ClearMiniDataRow(index) if index < 1 or index > 8 then return end local dataRow = Shopkeeper.MiniDataRows[index] dataRow:GetNamedChild("Guild"):SetText("") dataRow:GetNamedChild("ItemIcon"):SetTexture(nil) dataRow:GetNamedChild("ItemIcon"):SetHidden(true) local itemCell = dataRow:GetNamedChild("ItemName") itemCell:SetText("") itemCell:SetHandler("OnMouseDoubleClick", nil) itemCell:SetHandler("OnMouseEnter", nil) dataRow:GetNamedChild("Quantity"):SetText("") dataRow:GetNamedChild("SellTime"):SetText("") dataRow:GetNamedChild("Price"):SetText("") end -- Fill out a row with the given data function Shopkeeper.SetDataRow(index, buyer, guild, itemName, icon, quantity, sellTime, price, seller) if index < 1 or index > 15 then return end local dataRow = Shopkeeper.DataRows[index] -- Some extra stuff for the Buyer cell to handle double-click and color changes local buyerCell = dataRow:GetNamedChild("Buyer") buyerCell:SetText(buyer) -- If the seller is the player, color the buyer green. Otherwise, blue. local acctName = Shopkeeper.GetAccountName() if seller == acctName then buyerCell:SetNormalFontColor(0.18, 0.77, 0.05, 1) buyerCell:SetPressedFontColor(0.18, 0.77, 0.05, 1) buyerCell:SetMouseOverFontColor(0.32, 0.90, 0.18, 1) else buyerCell:SetNormalFontColor(0.21, 0.54, 0.94, 1) buyerCell:SetPressedFontColor(0.21, 0.54, 0.94, 1) buyerCell:SetMouseOverFontColor(0.34, 0.67, 1, 1) end buyerCell:SetHandler("OnMouseDoubleClick", function() if SCENE_MANAGER.currentScene.name == "mailSend" then ZO_MailSendToField:SetText("") ZO_MailSendToField:SetText(ZO_MailSendToField:GetText() .. buyer) else ZO_ChatWindowTextEntryEditBox:SetText("/w " .. buyer .. " " .. ZO_ChatWindowTextEntryEditBox:GetText()) end end) local buyerCellLabel = buyerCell:GetLabelControl() buyerCellLabel:SetWrapMode(TEXT_WRAP_MODE_ELLIPSIS) -- Guild cell dataRow:GetNamedChild("Guild"):SetText(guild) local guildCellLabel = dataRow:GetNamedChild("Guild"):GetLabelControl() guildCellLabel:SetWrapMode(TEXT_WRAP_MODE_ELLIPSIS) -- Item Icon dataRow:GetNamedChild("ItemIcon"):SetHidden(false) dataRow:GetNamedChild("ItemIcon"):SetTexture(icon) -- Item name cell local itemCell = dataRow:GetNamedChild("ItemName") itemCell:SetText(zo_strformat("<<t:1>>", itemName)) -- Insert the item link into the chat box, with a quick substitution so brackets show up itemCell:SetHandler("OnMouseDoubleClick", function() ZO_ChatWindowTextEntryEditBox:SetText(ZO_ChatWindowTextEntryEditBox:GetText() .. string.gsub(itemName, "|H0", "|H1")) end) itemCell:SetHandler("OnMouseEnter", function() Shopkeeper:ShowToolTip(itemName, itemCell) end) itemCell:SetHandler("OnMouseExit", function() ClearTooltip(ItemTooltip) end) local itemCellLabel = itemCell:GetLabelControl() itemCellLabel:SetWrapMode(TEXT_WRAP_MODE_ELLIPSIS) -- Quantity cell dataRow:GetNamedChild("Quantity"):SetText("x" .. quantity) -- Sale time cell dataRow:GetNamedChild("SellTime"):SetText(sellTime) -- Handle the setting of whether or not to show pre-cut sale prices -- math.floor(number + 0.5) is a quick shorthand way to round for -- positive values. local dispPrice = price if Shopkeeper.savedVariables.showFullPrice then if Shopkeeper.savedVariables.showUnitPrice and quantity > 0 then dispPrice = math.floor((dispPrice / quantity) + 0.5) end else local cutPrice = price * (1 - (GetTradingHouseCutPercentage() / 100)) if Shopkeeper.savedVariables.showUnitPrice and quantity > 0 then cutPrice = cutPrice / quantity end dispPrice = math.floor(cutPrice + 0.5) end -- Insert thousands separators for the price local stringPrice = Shopkeeper.localizedNumber(dispPrice) -- Finally, set the price dataRow:GetNamedChild("Price"):SetText(stringPrice .. " " .. string.format("|t16:16:%s|t","EsoUI/Art/currency/currency_gold.dds")) end -- Fill out a row with the given data function Shopkeeper.SetMiniDataRow(index, guild, itemName, icon, quantity, sellTime, price, seller) if index < 1 or index > 8 then return end local dataRow = Shopkeeper.MiniDataRows[index] -- Guild cell dataRow:GetNamedChild("Guild"):SetText(guild) local guildCellLabel = dataRow:GetNamedChild("Guild"):GetLabelControl() guildCellLabel:SetWrapMode(TEXT_WRAP_MODE_ELLIPSIS) -- Item Icon dataRow:GetNamedChild("ItemIcon"):SetHidden(false) dataRow:GetNamedChild("ItemIcon"):SetTexture(icon) -- Item name cell local itemCell = dataRow:GetNamedChild("ItemName") itemCell:SetText(zo_strformat("<<t:1>>", itemName)) -- Insert the item link into the chat box, with a quick substitution so brackets show up itemCell:SetHandler("OnMouseDoubleClick", function() ZO_ChatWindowTextEntryEditBox:SetText(ZO_ChatWindowTextEntryEditBox:GetText() .. string.gsub(itemName, "|H0", "|H1")) end) itemCell:SetHandler("OnMouseEnter", function() Shopkeeper:ShowToolTip(itemName, itemCell) end) itemCell:SetHandler("OnMouseExit", function() ClearTooltip(ItemTooltip) end) local itemCellLabel = itemCell:GetLabelControl() itemCellLabel:SetWrapMode(TEXT_WRAP_MODE_ELLIPSIS) -- Quantity cell dataRow:GetNamedChild("Quantity"):SetText("x" .. quantity) -- Sale time cell dataRow:GetNamedChild("SellTime"):SetText(sellTime) -- Handle the setting of whether or not to show pre-cut sale prices -- math.floor(number + 0.5) is a quick shorthand way to round for -- positive values. local dispPrice = price if Shopkeeper.savedVariables.showFullPrice then if Shopkeeper.savedVariables.showUnitPrice and quantity > 0 then dispPrice = math.floor((dispPrice / quantity) + 0.5) end else local cutPrice = price * (1 - (GetTradingHouseCutPercentage() / 100)) if Shopkeeper.savedVariables.showUnitPrice and quantity > 0 then cutPrice = cutPrice / quantity end dispPrice = math.floor(cutPrice + 0.5) end -- Insert thousands separators for the price local stringPrice = Shopkeeper.localizedNumber(dispPrice) -- Finally, set the price dataRow:GetNamedChild("Price"):SetText(stringPrice .. " " .. string.format("|t16:16:%s|t","EsoUI/Art/currency/currency_gold.dds")) end -- Build the data rows based on the position of the slider function Shopkeeper.DisplayRows() if Shopkeeper.savedVariables.viewSize == "full" then local startIndex = Shopkeeper.shopSlider:GetValue() if startIndex + #Shopkeeper.DataRows > #Shopkeeper.SearchTable then startIndex = #Shopkeeper.SearchTable - #Shopkeeper.DataRows end if startIndex < 1 then startIndex = 0 end -- Hide the slider if there's less than a full page of results Shopkeeper.shopSlider:SetHidden(#Shopkeeper.SearchTable < 16) for i = 1, #Shopkeeper.DataRows do local rowIndex = i + startIndex if rowIndex > #Shopkeeper.SearchTable then Shopkeeper.ClearDataRow(i) Shopkeeper.shopSlider:SetHidden(true) else local scanResult = Shopkeeper.SearchTable[rowIndex] Shopkeeper.SetDataRow(i, scanResult[1], scanResult[2], scanResult[3], scanResult[4], scanResult[5], Shopkeeper.textTimeSince(scanResult[6], false), scanResult[7], scanResult[8]) end 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 Shopkeeper.shopSlider:SetMinMax(0, sliderMax) else local startIndex = Shopkeeper.miniShopSlider:GetValue() if startIndex + #Shopkeeper.MiniDataRows > #Shopkeeper.SearchTable then startIndex = #Shopkeeper.SearchTable - #Shopkeeper.MiniDataRows end if startIndex < 1 then startIndex = 0 end -- Hide the slider if there's less than a full page of results Shopkeeper.miniShopSlider:SetHidden(#Shopkeeper.SearchTable < 9) for i = 1, #Shopkeeper.MiniDataRows do local rowIndex = i + startIndex if rowIndex > #Shopkeeper.SearchTable then Shopkeeper.ClearMiniDataRow(i) Shopkeeper.miniShopSlider:SetHidden(true) else local scanResult = Shopkeeper.SearchTable[rowIndex] Shopkeeper.SetMiniDataRow(i, scanResult[2], scanResult[3], scanResult[4], scanResult[5], Shopkeeper.textTimeSince(scanResult[6], false), scanResult[7], scanResult[8]) end 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 > 8 then sliderMax = (#tableToUse - 8) end Shopkeeper.miniShopSlider:SetMinMax(0, sliderMax) end end function Shopkeeper.ToggleViewMode() if Shopkeeper.savedVariables.viewSize == "full" then Shopkeeper.savedVariables.viewSize = "half" ShopkeeperWindow:SetHidden(true) Shopkeeper.DisplayRows() ShopkeeperMiniWindow:SetHidden(false) if Shopkeeper.savedVariables.openWithMail then MAIL_INBOX_SCENE:RemoveFragment(Shopkeeper.uiFragment) MAIL_SEND_SCENE:RemoveFragment(Shopkeeper.uiFragment) MAIL_INBOX_SCENE:AddFragment(Shopkeeper.miniUiFragment) MAIL_SEND_SCENE:AddFragment(Shopkeeper.miniUiFragment) end if Shopkeeper.savedVariables.openWithStore then TRADING_HOUSE_SCENE:RemoveFragment(Shopkeeper.uiFragment) TRADING_HOUSE_SCENE:AddFragment(Shopkeeper.miniUiFragment) end else Shopkeeper.savedVariables.viewSize = "full" ShopkeeperMiniWindow:SetHidden(true) Shopkeeper.DisplayRows() ShopkeeperWindow:SetHidden(false) if Shopkeeper.savedVariables.openWithMail then MAIL_INBOX_SCENE:RemoveFragment(Shopkeeper.miniUiFragment) MAIL_SEND_SCENE:RemoveFragment(Shopkeeper.miniUiFragment) MAIL_INBOX_SCENE:AddFragment(Shopkeeper.uiFragment) MAIL_SEND_SCENE:AddFragment(Shopkeeper.uiFragment) end if Shopkeeper.savedVariables.openWithStore then TRADING_HOUSE_SCENE:RemoveFragment(Shopkeeper.miniUiFragment) TRADING_HOUSE_SCENE:AddFragment(Shopkeeper.uiFragment) end end end -- Set the visibility status of the main window to the opposite of its current status function Shopkeeper.ToggleShopkeeperWindow() if Shopkeeper.savedVariables.viewSize == "full" then ShopkeeperMiniWindow:SetHidden(true) if ShopkeeperWindow:IsHidden() then Shopkeeper.DisplayRows() SetGameCameraUIMode(true) end ShopkeeperWindow:SetHidden(not ShopkeeperWindow:IsHidden()) else ShopkeeperWindow:SetHidden(true) if ShopkeeperMiniWindow:IsHidden() then Shopkeeper.DisplayRows() SetGameCameraUIMode(true) end ShopkeeperMiniWindow:SetHidden(not ShopkeeperMiniWindow:IsHidden()) end end -- Set the visibility status of the stats window to the opposite of its current status function Shopkeeper.ToggleShopkeeperStatsWindow() if ShopkeeperStatsWindow:IsHidden() then Shopkeeper.UpdateStatsWindow() end ShopkeeperStatsWindow:SetHidden(not ShopkeeperStatsWindow:IsHidden()) 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.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.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.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 function Shopkeeper.SalesStats(statsDays) local itemsSold = 0 local goldMade = 0 local largestSingle = {0, nil} local oldestTime = 0 local newestTime = 0 -- 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] if statsDays == 0 or theItem[6] > statsDaysEpoch then itemsSold = itemsSold + 1 goldMade = goldMade + theItem[7] 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[1] then largestSingle = {theItem[7], theItem[3]} end 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 goldPerDay = goldMade / dayWindow -- If they have the option set to show prices post-cut, calculate that here if not Shopkeeper.savedVariables.showFullPrice then local cutMult = 1 - (GetTradingHouseCutPercentage() / 100) goldMade = math.floor(goldMade * cutMult + 0.5) goldPerDay = math.floor(goldPerDay * cutMult + 0.5) largestSingle[1] = math.floor(largestSingle[1] * cutMult + 0.5) end -- Return the statistical data in a convenient table return { numSold = itemsSold, numDays = dayWindow, totalGold = goldMade, avgGold = goldPerDay, biggestSale = largestSingle } end function Shopkeeper.UpdateStatsWindow() local sliderLevel = Shopkeeper.statsSlider:GetValue() if sliderLevel == 0 then ShopkeeperStatsWindowSliderSettingLabel:SetText(Shopkeeper.translate('statsTimeAll')) else ShopkeeperStatsWindowSliderSettingLabel:SetText(string.format(Shopkeeper.translate('statsTimeSome'), sliderLevel)) end local newStats = Shopkeeper.SalesStats(sliderLevel) ShopkeeperStatsWindowItemsSoldLabel:SetText(string.format(Shopkeeper.translate('statsItemsSold'), Shopkeeper.localizedNumber(newStats['numSold']))) ShopkeeperStatsWindowTotalGoldLabel:SetText(string.format(Shopkeeper.translate('statsTotalGold'), Shopkeeper.localizedNumber(newStats['totalGold']), Shopkeeper.localizedNumber(newStats['avgGold']))) ShopkeeperStatsWindowBiggestSaleLabel:SetText(string.format(Shopkeeper.translate('statsBiggest'), zo_strformat("<<t:1>>", newStats['biggestSale'][2]), Shopkeeper.localizedNumber(newStats['biggestSale'][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 optionsData = { [1] = { type = "submenu", name = Shopkeeper.translate('alertOptionsName'), tooltip = Shopkeeper.translate('alertOptionsTip'), controls = { [1] = { type = "checkbox", name = Shopkeeper.translate('saleAlertAnnounceName'), tooltip = Shopkeeper.translate('saleAlertAnnounceTip'), getFunc = function() return Shopkeeper.savedVariables.showAnnounceAlerts end, setFunc = function(value) Shopkeeper.savedVariables.showAnnounceAlerts = value end, }, [2] = { type = "checkbox", name = Shopkeeper.translate('saleAlertChatName'), tooltip = Shopkeeper.translate('saleAlertChatTip'), getFunc = function() return Shopkeeper.savedVariables.showChatAlerts end, setFunc = function(value) Shopkeeper.savedVariables.showChatAlerts = value end, }, [3] = { type = "dropdown", name = Shopkeeper.translate('alertTypeName'), tooltip = Shopkeeper.translate('alertTypeTip'), choices = Shopkeeper.soundKeys(), getFunc = function() return Shopkeeper.searchSounds(Shopkeeper.savedVariables.alertSoundName) end, setFunc = function(value) Shopkeeper.savedVariables.alertSoundName = Shopkeeper.searchSoundNames(value) PlaySound(Shopkeeper.savedVariables.alertSoundName) end, }, [4] = { type = "checkbox", name = Shopkeeper.translate('multAlertName'), tooltip = Shopkeeper.translate('multAlertTip'), getFunc = function() return Shopkeeper.savedVariables.showMultiple end, setFunc = function(value) Shopkeeper.savedVariables.showMultiple = value end, }, }, }, [2] = { type = "checkbox", name = Shopkeeper.translate('openMailName'), tooltip = Shopkeeper.translate('openMailTip'), getFunc = function() return Shopkeeper.savedVariables.openWithMail end, setFunc = function(value) Shopkeeper.savedVariables.openWithMail = value 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, }, [3] = { type = "checkbox", name = Shopkeeper.translate('openStoreName'), tooltip = Shopkeeper.translate('openStoreTip'), getFunc = function() return Shopkeeper.savedVariables.openWithStore end, setFunc = function(value) Shopkeeper.savedVariables.openWithStore = value 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, }, [4] = { type = "checkbox", name = Shopkeeper.translate('fullSaleName'), tooltip = Shopkeeper.translate('fullSaleTip'), getFunc = function() return Shopkeeper.savedVariables.showFullPrice end, setFunc = function(value) Shopkeeper.savedVariables.showFullPrice = value Shopkeeper.DisplayRows() end, }, [5] = { type = "slider", name = Shopkeeper.translate('scanFreqName'), tooltip = Shopkeeper.translate('scanFreqTip'), min = 60, max = 600, getFunc = function() return Shopkeeper.savedVariables.scanFreq end, setFunc = function(value) Shopkeeper.savedVariables.scanFreq = value EVENT_MANAGER:UnregisterForUpdate(Shopkeeper.name) local scanInterval = value * 1000 EVENT_MANAGER:RegisterForUpdate(Shopkeeper.name, scanInterval, function() Shopkeeper:ScanStores(false, false) end) end, }, [6] = { type = "slider", name = Shopkeeper.translate('historyDepthName'), tooltip = Shopkeeper.translate('historyDepthTip'), min = 500, max = 7500, getFunc = function() return Shopkeeper.savedVariables.historyDepth end, setFunc = function(value) Shopkeeper.savedVariables.historyDepth = value end, }, [7] = { type = "dropdown", name = Shopkeeper.translate('windowFontName'), tooltip = Shopkeeper.translate('windowFontTip'), choices = LMP:List(LMP.MediaType.FONT), getFunc = function() return Shopkeeper.savedVariables.windowFont end, setFunc = function(value) Shopkeeper.savedVariables.windowFont = value Shopkeeper.UpdateFonts() end, }, } LAM:RegisterOptionControls("ShopkeeperOptions", optionsData) end end end -- Handle scrolling the main window function Shopkeeper.OnSliderMouseWheel(self, delta) if Shopkeeper.savedVariables.viewSize == "full" then local oldSliderLevel = Shopkeeper.shopSlider:GetValue() local newSliderLevel = oldSliderLevel - delta Shopkeeper.shopSlider:SetValue(newSliderLevel) else local oldSliderLevel = Shopkeeper.miniShopSlider:GetValue() local newSliderLevel = oldSliderLevel - delta Shopkeeper.miniShopSlider:SetValue(newSliderLevel) end end -- Update the table if the slider moved function Shopkeeper.OnSliderMoved(self, sliderLevel, eventReason) Shopkeeper.DisplayRows() end function Shopkeeper.OnStatsSliderMoved(self, sliderLevel, eventReason) Shopkeeper.UpdateStatsWindow() 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 = Shopkeeper.GetAccountName() 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(Shopkeeper.translate('viewModeYourName')) ShopkeeperWindowTitle:SetText("Shopkeeper - " .. Shopkeeper.translate('allSalesTitle')) ShopkeeperMiniSwitchViewButton:SetText(Shopkeeper.translate('viewModeYourName')) ShopkeeperMiniWindowTitle:SetText("Shopkeeper - " .. Shopkeeper.translate('allSalesTitle')) Shopkeeper.viewMode = "all" else ShopkeeperSwitchViewButton:SetText(Shopkeeper.translate('viewModeAllName')) ShopkeeperWindowTitle:SetText("Shopkeeper - " .. Shopkeeper.translate('yourSalesTitle')) ShopkeeperMiniSwitchViewButton:SetText(Shopkeeper.translate('viewModeAllName')) ShopkeeperMiniWindowTitle:SetText("Shopkeeper - " .. Shopkeeper.translate('yourSalesTitle')) Shopkeeper.viewMode = "self" end Shopkeeper.DoSearch(ShopkeeperWindowSearchBox:GetText()) end function Shopkeeper.SwitchPriceMode() if Shopkeeper.savedVariables.showUnitPrice then Shopkeeper.savedVariables.showUnitPrice = false ShopkeeperPriceSwitchButton:SetText(Shopkeeper.translate('showUnitPrice')) ShopkeeperWindowPrice:SetText(Shopkeeper.translate('priceColumnName')) ShopkeeperMiniPriceSwitchButton:SetText(Shopkeeper.translate('showUnitPrice')) ShopkeeperMiniWindowPrice:SetText(Shopkeeper.translate('priceColumnName')) else Shopkeeper.savedVariables.showUnitPrice = true ShopkeeperPriceSwitchButton:SetText(Shopkeeper.translate('showTotalPrice')) ShopkeeperWindowPrice:SetText(Shopkeeper.translate('priceEachColumnName')) ShopkeeperMiniPriceSwitchButton:SetText(Shopkeeper.translate('showTotalPrice')) ShopkeeperMiniWindowPrice:SetText(Shopkeeper.translate('priceEachColumnName')) end if Shopkeeper.curSort[1] == "price" then Shopkeeper.SortByPrice(Shopkeeper.curSort[2]) else Shopkeeper.DisplayRows() end end -- Actually carries out of the scan of a specific guild store's sales history. -- If checkOlder is true, will request older events first if there are any. -- Inserts all events that occurred after guildID guild's last scan into the ScanResults table. -- Inserts all events that occurred after guildID guild's last scan and were sold by the player -- into the SelfSales table. function Shopkeeper:DoScan(guildID, checkOlder) local numEvents = GetNumGuildEvents(guildID, GUILD_HISTORY_SALES) local thePlayer = string.lower(Shopkeeper.GetAccountName()) local timeStamp = GetTimeStamp() for i = 0, numEvents do local theEvent = {} _, theEvent.secsSince, theEvent.seller, theEvent.buyer, theEvent.quant, theEvent.itemName, theEvent.salePrice = GetGuildEventInfo(guildID, GUILD_HISTORY_SALES, i) theEvent.guild = GetGuildName(guildID) -- Only worry about items sold since our last scan if theEvent.secsSince ~= nil then theEvent.saleTime = timeStamp - theEvent.secsSince if Shopkeeper.acctSavedVariables.lastScan[guildID] == nil or theEvent.saleTime > Shopkeeper.acctSavedVariables.lastScan[guildID] then 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) -- If the seller is the player and this isn't a deep scan (and thus the first upon login or reset), -- queue up an alert if not checkOlder and (Shopkeeper.savedVariables.showChatAlerts or Shopkeeper.savedVariables.showAnnounceAlerts) and string.lower(theEvent.seller) == thePlayer then table.insert(Shopkeeper.alertQueue, theEvent) end -- 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}) -- 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}) end end end end end Shopkeeper.acctSavedVariables.lastScan[guildID] = GetTimeStamp() if guildID < GetNumGuilds() then if checkOlder and DoesGuildHistoryCategoryHaveMoreEvents((guildID + 1), GUILD_HISTORY_SALES) then RequestGuildHistoryCategoryNewest((guildID + 1), GUILD_HISTORY_SALES) RequestGuildHistoryCategoryOlder((guildID + 1), GUILD_HISTORY_SALES) else RequestGuildHistoryCategoryNewest((guildID + 1), GUILD_HISTORY_SALES) end 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) Shopkeeper.isScanning = false Shopkeeper.DoSearch(ShopkeeperWindowSearchBox:GetText()) -- 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 Shopkeeper.shopSlider:SetMinMax(0, sliderMax) sliderMax = 0 if #tableToUse > 8 then sliderMax = (#tableToUse - 8) end Shopkeeper.miniShopSlider: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] " .. Shopkeeper.translate('refreshDone')) 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 -- On-screen alerts play their own sounds, so only do this if they only get chat alerts if Shopkeeper.savedVariables.showChatAlerts and not Shopkeeper.savedVariables.showAnnounceAlerts then PlaySound(Shopkeeper.savedVariables.alertSoundName) end local numSold = 0 local totalGold = 0 local numAlerts = #Shopkeeper.alertQueue 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 Shopkeeper.savedVariables.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 Shopkeeper.savedVariables.showMultiple or numAlerts == 1 then -- Insert thousands separators for the price local stringPrice = Shopkeeper.localizedNumber(dispPrice) -- Only make a sound on the first one if sounds are turned on -- To avoid sound spam on multiple sales -- (Because of how and/or work in LUA, "X and Y or Z" is equivalent -- to the C-style ternary operator "X ? Y : Z" so long as Y is not -- false or nil.) local alertSound = (i > 1) and SOUNDS.NONE or Shopkeeper.savedVariables.alertSoundName -- On-screen alert if Shopkeeper.savedVariables.showAnnounceAlerts then -- 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:DisplayMessage(CSA_EVENT_SMALL_TEXT, alertSound, string.format(Shopkeeper.translate('salesAlertColor'), theEvent.quant, zo_strformat("<<t:1>>", theEvent.itemName), stringPrice, theEvent.guild, Shopkeeper.textTimeSince(theEvent.saleTime, true))) else CENTER_SCREEN_ANNOUNCE:DisplayMessage(CSA_EVENT_SMALL_TEXT, alertSound, string.format(Shopkeeper.translate('salesAlertColorSingle'), zo_strformat("<<t:1>>", theEvent.itemName), stringPrice, theEvent.guild, Shopkeeper.textTimeSince(theEvent.saleTime, true))) end else CENTER_SCREEN_ANNOUNCE:DisplayMessage(CSA_EVENT_SMALL_TEXT, alertSound, string.format(Shopkeeper.translate('salesAlertColor'), zo_strformat("<<t:1>>", theEvent.itemName), theEvent.quant, stringPrice, theEvent.guild, Shopkeeper.textTimeSince(theEvent.saleTime, true))) end end -- Chat alert if Shopkeeper.savedVariables.showChatAlerts then if Shopkeeper.locale == "de" then if theEvent.quant > 1 then CHAT_SYSTEM:AddMessage(string.format("[Shopkeeper] " .. Shopkeeper.translate('salesAlert'), theEvent.quant, zo_strformat("<<t:1>>", theEvent.itemName), stringPrice, theEvent.guild, Shopkeeper.textTimeSince(theEvent.saleTime, true))) else CHAT_SYSTEM:AddMessage(string.format("[Shopkeeper] " .. Shopkeeper.translate('salesAlertSingle'), zo_strformat("<<t:1>>", theEvent.itemName), stringPrice, theEvent.guild, Shopkeeper.textTimeSince(theEvent.saleTime, true))) end else CHAT_SYSTEM:AddMessage(string.format("[Shopkeeper] " .. Shopkeeper.translate('salesAlert'), 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 Shopkeeper.savedVariables.showMultiple and numAlerts > 1 then -- Insert thousands separators for the price local stringPrice = Shopkeeper.localizedNumber(totalGold) if Shopkeeper.savedVariables.showAnnounceAlerts then CENTER_SCREEN_ANNOUNCE:DisplayMessage(CSA_EVENT_SMALL_TEXT, Shopkeeper.savedVariables.alertSoundName, string.format(Shopkeeper.translate('salesGroupAlertColor'), numSold, stringPrice)) else CHAT_SYSTEM:AddMessage(string.format("[Shopkeeper] " .. Shopkeeper.translate('salesGroupAlert'), numSold, stringPrice)) end end end -- Finally, update the table just in case Shopkeeper.DisplayRows() end -- Scans all stores a player has access to with 2-second delays between them. -- Idea for spaced callbacks taken from awesomebilly's Luminary Trade/Sales -- History addon. function Shopkeeper:ScanStores(checkOlder, doAlert) -- 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 = GetTimeStamp() - 60 local guildNum = GetNumGuilds() if not Shopkeeper.isScanning and (Shopkeeper.acctSavedVariables.lastScan[guildNum] == nil or timeLimit > Shopkeeper.acctSavedVariables.lastScan[guildNum]) then -- Nothing to scan! if guildNum == 0 then return end Shopkeeper.isScanning = true if checkOlder and DoesGuildHistoryCategoryHaveMoreEvents(1, GUILD_HISTORY_SALES) then RequestGuildHistoryCategoryNewest(1, GUILD_HISTORY_SALES) RequestGuildHistoryCategoryOlder(1, GUILD_HISTORY_SALES) else RequestGuildHistoryCategoryNewest(1, GUILD_HISTORY_SALES) end for j = 1, guildNum do local guildID = GetGuildId(j) -- I need a better way to space out these checks than callbacks -- It works but feels ugly zo_callLater(function() Shopkeeper:DoScan(guildID, checkOlder) end, (((j - 1) * 2000) + 1000)) end -- Once scans are done, wait a few seconds and do some cleanup zo_callLater(function() Shopkeeper:PostScan(doAlert) end, ((guildNum + 1) * 2000)) end end -- It's silly, but I have to re-set the fonts each time the UI reloads because I define defaults -- in the .xml (at least as far as I can figure.) The PlayerActive event fires after each of these, -- so it's a nice place to hook in for that. function Shopkeeper.PlayerActive() Shopkeeper:UpdateFonts() 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 timeLimit > Shopkeeper.acctSavedVariables.lastScan[guildNum] then CHAT_SYSTEM:AddMessage("[Shopkeeper] " .. Shopkeeper.translate('refreshStart')) Shopkeeper:ScanStores(false, true) else CHAT_SYSTEM:AddMessage("[Shopkeeper] " .. Shopkeeper.translate('refreshWait')) end end -- Handle the reset button - clear out the search and scan tables, -- and set the time of the last scan to -1. 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:ScanStores(true, false) CHAT_SYSTEM:AddMessage("[Shopkeeper] " .. Shopkeeper.translate('resetDone')) end -- Set up the main window and the additional button added to the guild store interface function Shopkeeper:SetupShopkeeperWindow() -- 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) -- Set column headers and search label from translation ShopkeeperWindowBuyer:SetText(Shopkeeper.translate('buyerColumnName')) ShopkeeperWindowGuild:SetText(Shopkeeper.translate('guildColumnName')) ShopkeeperWindowItemName:SetText(Shopkeeper.translate('itemColumnName')) ShopkeeperWindowSellTime:SetText(Shopkeeper.translate('timeColumnName')) ShopkeeperMiniWindowGuild:SetText(Shopkeeper.translate('guildColumnName')) ShopkeeperMiniWindowItemName:SetText(Shopkeeper.translate('itemColumnName')) ShopkeeperMiniWindowSellTime:SetText(Shopkeeper.translate('timeColumnName')) if Shopkeeper.savedVariables.showUnitPrice then ShopkeeperWindowPrice:SetText(Shopkeeper.translate('priceEachColumnName')) ShopkeeperMiniWindowPrice:SetText(Shopkeeper.translate('priceEachColumnName')) else ShopkeeperWindowPrice:SetText(Shopkeeper.translate('priceColumnName')) ShopkeeperMiniWindowPrice:SetText(Shopkeeper.translate('priceColumnName')) end ShopkeeperWindowSearchLabel:SetText(Shopkeeper.translate('searchBoxName')) ShopkeeperMiniWindowSearchLabel:SetText(Shopkeeper.translate('searchBoxName')) -- Set second half of window title from translation ShopkeeperWindowTitle:SetText("Shopkeeper - " .. Shopkeeper.translate('yourSalesTitle')) ShopkeeperMiniWindowTitle:SetText("Shopkeeper - " .. Shopkeeper.translate('yourSalesTitle')) -- And set the stats window title and slider label from translation ShopkeeperStatsWindowTitle:SetText("Shopkeeper " .. Shopkeeper.translate('statsTitle')) ShopkeeperStatsWindowSliderLabel:SetText(Shopkeeper.translate('statsDays')) -- Set up some helpful tooltips for the Buyer and Item column headers ShopkeeperWindowBuyer:SetHandler("OnMouseEnter", function(self) ZO_Tooltips_ShowTextTooltip(self, TOP, Shopkeeper.translate('buyerTooltip')) end) ShopkeeperWindowBuyer:SetHandler("OnMouseExit", function(self) ZO_Tooltips_HideTextTooltip() end) ShopkeeperWindowItemName:SetHandler("OnMouseEnter", function(self) ZO_Tooltips_ShowTextTooltip(self, TOP, Shopkeeper.translate('itemTooltip')) end) ShopkeeperWindowItemName:SetHandler("OnMouseExit", function(self) ZO_Tooltips_HideTextTooltip() end) ShopkeeperMiniWindowItemName:SetHandler("OnMouseEnter", function(self) ZO_Tooltips_ShowTextTooltip(self, TOP, Shopkeeper.translate('itemTooltip')) end) ShopkeeperMiniWindowItemName:SetHandler("OnMouseExit", function(self) ZO_Tooltips_HideTextTooltip() end) ShopkeeperWindowSellTime:SetHandler("OnMouseEnter", function(self) ZO_Tooltips_ShowTextTooltip(self, TOP, Shopkeeper.translate('sortTimeTip')) end) ShopkeeperWindowSellTime:SetHandler("OnMouseExit", function(self) ZO_Tooltips_HideTextTooltip() end) ShopkeeperMiniWindowSellTime:SetHandler("OnMouseEnter", function(self) ZO_Tooltips_ShowTextTooltip(self, TOP, Shopkeeper.translate('sortTimeTip')) end) ShopkeeperMiniWindowSellTime:SetHandler("OnMouseExit", function(self) ZO_Tooltips_HideTextTooltip() end) ShopkeeperWindowPrice:SetHandler("OnMouseEnter", function(self) ZO_Tooltips_ShowTextTooltip(self, TOP, Shopkeeper.translate('sortPriceTip')) end) ShopkeeperWindowPrice:SetHandler("OnMouseExit", function(self) ZO_Tooltips_HideTextTooltip() end) ShopkeeperMiniWindowPrice:SetHandler("OnMouseEnter", function(self) ZO_Tooltips_ShowTextTooltip(self, TOP, Shopkeeper.translate('sortPriceTip')) end) ShopkeeperMiniWindowPrice:SetHandler("OnMouseExit", function(self) ZO_Tooltips_HideTextTooltip() end) -- View switch button local switchViews = CreateControlFromVirtual("ShopkeeperSwitchViewButton", ShopkeeperWindow, "ZO_DefaultButton") switchViews:SetAnchor(BOTTOMLEFT, ShopkeeperWindow, BOTTOMLEFT, 20, -5) switchViews:SetWidth(175) switchViews:SetText(Shopkeeper.translate('viewModeAllName')) switchViews:SetHandler("OnClicked", Shopkeeper.SwitchViewMode) local miniSwitchViews = CreateControlFromVirtual("ShopkeeperMiniSwitchViewButton", ShopkeeperMiniWindow, "ZO_DefaultButton") miniSwitchViews:SetAnchor(BOTTOMLEFT, ShopkeeperMiniWindow, BOTTOMLEFT, 2, -5) miniSwitchViews:SetWidth(175) miniSwitchViews:SetText(Shopkeeper.translate('viewModeAllName')) miniSwitchViews:SetHandler("OnClicked", Shopkeeper.SwitchViewMode) -- Total / unit price switch button local unitPrice = CreateControlFromVirtual("ShopkeeperPriceSwitchButton", ShopkeeperWindow, "ZO_DefaultButton") unitPrice:SetAnchor(LEFT, switchViews, RIGHT, 0, 0) unitPrice:SetWidth(175) if Shopkeeper.savedVariables.showUnitPrice then unitPrice:SetText(Shopkeeper.translate('showTotalPrice')) else unitPrice:SetText(Shopkeeper.translate('showUnitPrice')) end unitPrice:SetHandler("OnClicked", Shopkeeper.SwitchPriceMode) local miniUnitPrice = CreateControlFromVirtual("ShopkeeperMiniPriceSwitchButton", ShopkeeperMiniWindow, "ZO_DefaultButton") miniUnitPrice:SetAnchor(LEFT, miniSwitchViews, RIGHT, 0, 0) miniUnitPrice:SetWidth(175) if Shopkeeper.savedVariables.showUnitPrice then miniUnitPrice:SetText(Shopkeeper.translate('showTotalPrice')) else miniUnitPrice:SetText(Shopkeeper.translate('showUnitPrice')) end miniUnitPrice:SetHandler("OnClicked", Shopkeeper.SwitchPriceMode) -- Refresh button local refreshButton = CreateControlFromVirtual("ShopkeeperRefreshButton", ShopkeeperWindow, "ZO_DefaultButton") refreshButton:SetAnchor(BOTTOMRIGHT, ShopkeeperWindow, BOTTOMRIGHT, -20, -5) refreshButton:SetWidth(150) refreshButton:SetText(Shopkeeper.translate('refreshLabel')) refreshButton:SetHandler("OnClicked", Shopkeeper.DoRefresh) local miniRefreshButton = CreateControlFromVirtual("ShopkeeperMiniRefreshButton", ShopkeeperMiniWindow, "ZO_DefaultButton") miniRefreshButton:SetAnchor(BOTTOMRIGHT, ShopkeeperMiniWindow, BOTTOMRIGHT, -2, -5) miniRefreshButton:SetWidth(150) miniRefreshButton:SetText(Shopkeeper.translate('refreshLabel')) miniRefreshButton:SetHandler("OnClicked", Shopkeeper.DoRefresh) -- Reset button local resetButton = CreateControlFromVirtual("ShopkeeperResetButton", ShopkeeperWindow, "ZO_DefaultButton") resetButton:SetAnchor(BOTTOMRIGHT, ShopkeeperWindow, BOTTOMRIGHT, -170, -5) resetButton:SetWidth(150) resetButton:SetText(Shopkeeper.translate('resetLabel')) resetButton:SetHandler("OnClicked", Shopkeeper.DoReset) local miniResetButton = CreateControlFromVirtual("ShopkeeperMiniResetButton", ShopkeeperMiniWindow, "ZO_DefaultButton") miniResetButton:SetAnchor(BOTTOMRIGHT, ShopkeeperMiniWindow, BOTTOMRIGHT, -152, -5) miniResetButton:SetWidth(150) miniResetButton:SetText(Shopkeeper.translate('resetLabel')) miniResetButton:SetHandler("OnClicked", Shopkeeper.DoReset) -- 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 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 -- Close buttons ShopkeeperWindowCloseButton:SetNormalTexture("/esoui/art/hud/radialicon_cancel_up.dds") ShopkeeperWindowCloseButton:SetMouseOverTexture("/esoui/art/hud/radialicon_cancel_over.dds") ShopkeeperWindowCloseButton:SetHandler("OnClicked", function() ShopkeeperWindow:SetHidden(true) end) ShopkeeperMiniWindowCloseButton:SetNormalTexture("/esoui/art/hud/radialicon_cancel_up.dds") ShopkeeperMiniWindowCloseButton:SetMouseOverTexture("/esoui/art/hud/radialicon_cancel_over.dds") ShopkeeperMiniWindowCloseButton:SetHandler("OnClicked", function() ShopkeeperMiniWindow:SetHidden(true) end) ShopkeeperStatsWindowCloseButton:SetNormalTexture("/esoui/art/hud/radialicon_cancel_up.dds") ShopkeeperStatsWindowCloseButton:SetMouseOverTexture("/esoui/art/hud/radialicon_cancel_over.dds") ShopkeeperStatsWindowCloseButton:SetHandler("OnClicked", function() ShopkeeperStatsWindow:SetHidden(true) end) -- Stats buttons ShopkeeperWindowStatsButton:SetNormalTexture("/esoui/art/tradinghouse/tradinghouse_listings_tabicon_up.dds") ShopkeeperWindowStatsButton:SetMouseOverTexture("/esoui/art/tradinghouse/tradinghouse_listings_tabicon_over.dds") ShopkeeperWindowStatsButton:SetHandler("OnClicked", Shopkeeper.ToggleShopkeeperStatsWindow) ShopkeeperWindowStatsButton:SetHandler("OnMouseEnter", function(self) ZO_Tooltips_ShowTextTooltip(self, TOP, Shopkeeper.translate('statsTooltip')) end) ShopkeeperWindowStatsButton:SetHandler("OnMouseExit", function(self) ZO_Tooltips_HideTextTooltip() end) ShopkeeperMiniWindowStatsButton:SetNormalTexture("/esoui/art/tradinghouse/tradinghouse_listings_tabicon_up.dds") ShopkeeperMiniWindowStatsButton:SetMouseOverTexture("/esoui/art/tradinghouse/tradinghouse_listings_tabicon_over.dds") ShopkeeperMiniWindowStatsButton:SetHandler("OnClicked", Shopkeeper.ToggleShopkeeperStatsWindow) ShopkeeperMiniWindowStatsButton:SetHandler("OnMouseEnter", function(self) ZO_Tooltips_ShowTextTooltip(self, TOP, Shopkeeper.translate('statsTooltip')) end) ShopkeeperMiniWindowStatsButton:SetHandler("OnMouseExit", function(self) ZO_Tooltips_HideTextTooltip() end) -- View size change buttons ShopkeeperWindowViewSizeButton:SetNormalTexture("/esoui/art/inventory/inventory_tabicon_quest_up.dds") ShopkeeperWindowViewSizeButton:SetMouseOverTexture("/esoui/art/inventory/inventory_tabicon_quest_over.dds") ShopkeeperWindowViewSizeButton:SetHandler("OnClicked", Shopkeeper.ToggleViewMode) ShopkeeperWindowViewSizeButton:SetHandler("OnMouseEnter", function(self) ZO_Tooltips_ShowTextTooltip(self, TOP, Shopkeeper.translate('sizeTooltip')) end) ShopkeeperWindowViewSizeButton:SetHandler("OnMouseExit", function(self) ZO_Tooltips_HideTextTooltip() end) ShopkeeperMiniWindowViewSizeButton:SetNormalTexture("/esoui/art/inventory/inventory_tabicon_quest_up.dds") ShopkeeperMiniWindowViewSizeButton:SetMouseOverTexture("/esoui/art/inventory/inventory_tabicon_quest_over.dds") ShopkeeperMiniWindowViewSizeButton:SetHandler("OnClicked", Shopkeeper.ToggleViewMode) ShopkeeperMiniWindowViewSizeButton:SetHandler("OnMouseEnter", function(self) ZO_Tooltips_ShowTextTooltip(self, TOP, Shopkeeper.translate('sizeTooltip')) end) ShopkeeperMiniWindowViewSizeButton:SetHandler("OnMouseExit", function(self) ZO_Tooltips_HideTextTooltip() end) -- Slider setup ShopkeeperWindow:SetHandler("OnMouseWheel", Shopkeeper.OnSliderMouseWheel) Shopkeeper.shopSlider = CreateControl(ShopkeeperWindowSlider, ShopkeeperWindow, CT_SLIDER) Shopkeeper.shopSlider.texture = "/esoui/art/miscellaneous/scrollbox_elevator.dds" Shopkeeper.shopSlider.offset = 0 local tex = Shopkeeper.shopSlider.texture Shopkeeper.shopSlider:SetDimensions(20,561) Shopkeeper.shopSlider:SetMouseEnabled(true) Shopkeeper.shopSlider:SetThumbTexture(tex,tex,tex,20,50,0,0,1,1) Shopkeeper.shopSlider:SetMinMax(0, 100) Shopkeeper.shopSlider:SetValue(0) Shopkeeper.shopSlider:SetValueStep(1) Shopkeeper.shopSlider:SetAnchor(LEFT, ShopkeeperWindow, RIGHT, -20, 17) Shopkeeper.shopSlider:SetHandler("OnValueChanged", Shopkeeper.OnSliderMoved) Shopkeeper.sliderBG = CreateControl(nil, Shopkeeper.shopSlider, CT_BACKDROP) Shopkeeper.sliderBG:SetCenterColor(0, 0, 0) Shopkeeper.sliderBG:SetAnchor(TOPLEFT, Shopkeeper.shopSlider, TOPLEFT, 0, -4) Shopkeeper.sliderBG:SetAnchor(BOTTOMRIGHT, Shopkeeper.shopSlider, BOTTOMRIGHT, 0, 4) Shopkeeper.sliderBG:SetEdgeTexture("EsoUI\\Art\\Tooltips\\UI-SliderBackdrop.dds", 32, 4) ShopkeeperMiniWindow:SetHandler("OnMouseWheel", Shopkeeper.OnSliderMouseWheel) Shopkeeper.miniShopSlider = CreateControl(ShopkeeperMiniWindowSlider, ShopkeeperMiniWindow, CT_SLIDER) Shopkeeper.miniShopSlider.texture = "/esoui/art/miscellaneous/scrollbox_elevator.dds" Shopkeeper.miniShopSlider.offset = 0 Shopkeeper.miniShopSlider:SetDimensions(20,300) Shopkeeper.miniShopSlider:SetMouseEnabled(true) Shopkeeper.miniShopSlider:SetThumbTexture(tex,tex,tex,20,50,0,0,1,1) Shopkeeper.miniShopSlider:SetMinMax(0, 100) Shopkeeper.miniShopSlider:SetValue(0) Shopkeeper.miniShopSlider:SetValueStep(1) Shopkeeper.miniShopSlider:SetAnchor(LEFT, ShopkeeperMiniWindow, RIGHT, -20, 17) Shopkeeper.miniShopSlider:SetHandler("OnValueChanged", Shopkeeper.OnSliderMoved) Shopkeeper.miniSliderBG = CreateControl(nil, Shopkeeper.miniShopSlider, CT_BACKDROP) Shopkeeper.miniSliderBG:SetCenterColor(0, 0, 0) Shopkeeper.miniSliderBG:SetAnchor(TOPLEFT, Shopkeeper.miniShopSlider, TOPLEFT, 0, -4) Shopkeeper.miniSliderBG:SetAnchor(BOTTOMRIGHT, Shopkeeper.miniShopSlider, BOTTOMRIGHT, 0, 4) Shopkeeper.miniSliderBG:SetEdgeTexture("EsoUI\\Art\\Tooltips\\UI-SliderBackdrop.dds", 32, 4) Shopkeeper.statsSlider = CreateControl(ShopkeeperStatsWindowSlider, ShopkeeperStatsWindow, CT_SLIDER) Shopkeeper.statsSlider.texture = "/esoui/art/miscellaneous/scrollbox_elevator.dds" Shopkeeper.statsSlider.offset = 0 Shopkeeper.statsSlider:SetDimensions(375,14) Shopkeeper.statsSlider:SetMouseEnabled(true) Shopkeeper.statsSlider:SetThumbTexture("EsoUI\\Art\\Miscellaneous\\scrollbox_elevator.dds", "EsoUI\\Art\\Miscellaneous\\scrollbox_elevator_disabled.dds", nil, 8, 16) Shopkeeper.statsSlider:SetMinMax(0, 30) Shopkeeper.statsSlider:SetValue(0) Shopkeeper.statsSlider:SetValueStep(1) Shopkeeper.statsSlider:SetOrientation(ORIENTATION_HORIZONTAL) Shopkeeper.statsSlider:SetAnchor(BOTTOM, ShopkeeperStatsWindow, BOTTOM, 25, -15) Shopkeeper.statsSlider:SetHandler("OnValueChanged", Shopkeeper.OnStatsSliderMoved) Shopkeeper.statsSliderBG = CreateControl(nil, Shopkeeper.statsSlider, CT_BACKDROP) Shopkeeper.statsSliderBG:SetCenterColor(0, 0, 0) Shopkeeper.statsSliderBG:SetAnchor(TOPLEFT, Shopkeeper.statsSlider, TOPLEFT, 0, 4) Shopkeeper.statsSliderBG:SetAnchor(BOTTOMRIGHT, Shopkeeper.statsSlider, BOTTOMRIGHT, 0, -4) Shopkeeper.statsSliderBG:SetEdgeTexture("EsoUI\\Art\\Tooltips\\UI-SliderBackdrop.dds", 32, 4) -- Sorting handlers ShopkeeperWindowPrice:SetHandler("OnMouseUp", Shopkeeper.PriceSort) ShopkeeperWindowSellTime:SetHandler("OnMouseUp", Shopkeeper.TimeSort) ShopkeeperMiniWindowPrice:SetHandler("OnMouseUp", Shopkeeper.PriceSort) ShopkeeperMiniWindowSellTime:SetHandler("OnMouseUp", Shopkeeper.TimeSort) -- 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 = Shopkeeper.savedVariables.windowFont Shopkeeper:UpdateFonts() Shopkeeper.DisplayRows() end -- Init function function Shopkeeper:Initialize() 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"] = {}, } self.savedVariables = ZO_SavedVars:New("ShopkeeperSavedVars", 1, Shopkeeper.GetAccountName(), Defaults) self.acctSavedVariables = ZO_SavedVars:NewAccountWide("ShopkeeperSavedVars", 1, Shopkeeper.GetAccountName(), acctDefaults) self.ScanResults = Shopkeeper.acctSavedVariables.scanHistory -- Update the lastScan value, as it was previously a number if type(Shopkeeper.acctSavedVariables.lastScan) == "number" then local guildNum = GetNumGuilds() local oldStamp = Shopkeeper.acctSavedVariables.lastScan Shopkeeper.acctSavedVariables.lastScan = {} for i = 1, guildNum do Shopkeeper.acctSavedVariables.lastScan[i] = oldStamp end end 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.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(Shopkeeper.GetAccountName()) 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 Shopkeeper.uiFragment = ZO_FadeSceneFragment:New(ShopkeeperWindow) Shopkeeper.miniUiFragment = ZO_FadeSceneFragment:New(ShopkeeperMiniWindow) if self.savedVariables.openWithMail then if self.savedVariables.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 self.savedVariables.openWithStore then if self.savedVariables.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 not Shopkeeper.savedVariables.openWithMail then ShopkeeperWindow:SetHidden(true) ShopkeeperMiniWindow:SetHidden(true) end end) EVENT_MANAGER:RegisterForEvent(Shopkeeper.name, EVENT_CLOSE_TRADING_HOUSE, function() if not Shopkeeper.savedVariables.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) -- Update fonts after each UI load EVENT_MANAGER:RegisterForEvent(Shopkeeper.name, EVENT_PLAYER_ACTIVATED, Shopkeeper.PlayerActive) -- 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 EVENT_MANAGER:RegisterForUpdate(Shopkeeper.name, scanInterval, function() Shopkeeper:ScanStores(false, 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(true, 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() if ShopkeeperWindow:IsHidden() then Shopkeeper.DisplayRows() SetGameCameraUIMode(true) end ShopkeeperWindow:SetHidden(not ShopkeeperWindow:IsHidden()) end