Push for 0.9a -

Khaibit [08-09-14 - 03:48]
Push for 0.9a -

Rewrite of part of the scanning routines to be more accurate
Fixes for odd behavior in the stats window
Fixes for the "Alert flood" issue if you sell multiple items between scans
Misc. other small bugfixes
Filename
Shopkeeper.lua
Shopkeeper.txt
Shopkeeper.xml
Shopkeeper_Namespace_Init.lua
readme
diff --git a/Shopkeeper.lua b/Shopkeeper.lua
index 5a059e6..d477a48 100644
--- a/Shopkeeper.lua
+++ b/Shopkeeper.lua
@@ -1,5 +1,5 @@
 -- Shopkeeper Main Addon File
--- Last Updated August 4, 2014
+-- Last Updated August 8, 2014
 -- Written July 2014 by Dan Stone (@khaibit) - dankitymao@gmail.com
 -- Released under terms in license accompanying this file.
 -- Distribution without license is prohibited!
@@ -653,7 +653,7 @@ function Shopkeeper.SalesStats(statsDays)
   local timeWindow = newestTime - oldestTime
   local dayWindow = 1
   if timeWindow > 86400 then dayWindow = math.floor(timeWindow / 86400) + 1 end
-  local goldPerDay = goldMade / dayWindow
+  local goldPerDay = math.floor(goldMade / dayWindow)

   -- If they have the option set to show prices post-cut, calculate that here
   if not Shopkeeper.savedVariables.showFullPrice then
@@ -899,7 +899,11 @@ function Shopkeeper.SwitchViewMode()
     Shopkeeper.viewMode = "self"
   end

-  Shopkeeper.DoSearch(ShopkeeperWindowSearchBox:GetText())
+  if Shopkeeper.savedVariables.viewSize == "full" then
+    Shopkeeper.DoSearch(ShopkeeperWindowSearchBox:GetText())
+  else
+    Shopkeeper.DoSearch(ShopkeeperMiniWindowSearchBox:GetText())
+  end
 end

 function Shopkeeper.SwitchPriceMode()
@@ -925,63 +929,6 @@ function Shopkeeper.SwitchPriceMode()

 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
@@ -989,7 +936,11 @@ end
 -- the scan was initiated via the 'refresh' button.
 function Shopkeeper:PostScan(doAlert)
   Shopkeeper.isScanning = false
-  Shopkeeper.DoSearch(ShopkeeperWindowSearchBox:GetText())
+  if Shopkeeper.savedVariables.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
@@ -1052,16 +1003,16 @@ function Shopkeeper:PostScan(doAlert)
           -- 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,
+              CENTER_SCREEN_ANNOUNCE:AddMessage(EVENT_SKILL_RANK_UPDATE, 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,
+              CENTER_SCREEN_ANNOUNCE:AddMessage(EVENT_SKILL_RANK_UPDATE, 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,
+            CENTER_SCREEN_ANNOUNCE:AddMessage(EVENT_SKILL_RANK_UPDATE, 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
@@ -1091,7 +1042,7 @@ function Shopkeeper:PostScan(doAlert)
       local stringPrice = Shopkeeper.localizedNumber(totalGold)

       if Shopkeeper.savedVariables.showAnnounceAlerts then
-        CENTER_SCREEN_ANNOUNCE:DisplayMessage(CSA_EVENT_SMALL_TEXT, Shopkeeper.savedVariables.alertSoundName,
+        CENTER_SCREEN_ANNOUNCE:AddMessage(EVENT_SKILL_RANK_UPDATE, CSA_EVENT_SMALL_TEXT, Shopkeeper.savedVariables.alertSoundName,
           string.format(Shopkeeper.translate('salesGroupAlertColor'), numSold, stringPrice))
       else
         CHAT_SYSTEM:AddMessage(string.format("[Shopkeeper] " .. Shopkeeper.translate('salesGroupAlert'),
@@ -1104,36 +1055,130 @@ function Shopkeeper:PostScan(doAlert)
   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.
+-- 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, doAlert)
+  local timeWindow = 10
+  if Shopkeeper.missedLastScan[guildID] ~= nil and Shopkeeper.missedLastScan[guildID] == true then
+    timeWindow = 25
+  end
+  local thePlayer = string.lower(Shopkeeper.GetAccountName())
+  local numEvents = GetNumGuildEvents(guildID, GUILD_HISTORY_SALES)
+  for i = 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 = GetGuildName(guildID)
+    -- Only worry about items sold since our last scan
+    if theEvent.secsSince ~= nil then
+      theEvent.saleTime = Shopkeeper.requestTimestamp - theEvent.secsSince
+
+      if Shopkeeper.acctSavedVariables.lastScan[guildID] == nil or GetDiffBetweenTimeStamps(theEvent.saleTime, (Shopkeeper.acctSavedVariables.lastScan[guildID] - timeWindow)) >= 0 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] = Shopkeeper.requestTimestamp
+  if guildID < GetNumGuilds() then
+    local nextGuild = guildID + 1
+    local didScan = RequestGuildHistoryCategoryNewest(nextGuild, GUILD_HISTORY_SALES)
+    if Shopkeeper.missedScan[nextGuild] ~= nil then Shopkeeper.missedLastScan[nextGuild] = Shopkeeper.missedScan[nextGuild]
+    else Shopkeeper.missedLastScan[nextGuild] = false end
+
+    if not didScan then
+      Shopkeeper.missedScan[nextGuild] = true
+    else
+      Shopkeeper.missedScan[nextGuild] = false
+    end
+    Shopkeeper.requestTimestamp = GetTimeStamp()
+    if checkOlder then
+      zo_callLater(function() Shopkeeper:ScanOlder(nextGuild, doAlert) end, 1250)
+    else
+      zo_callLater(function() Shopkeeper:DoScan(nextGuild , false, doAlert) end, 1250)
+    end
+  else
+    zo_callLater(function() Shopkeeper:PostScan(doAlert) end, 1250)
+  end
+end
+
+-- Repeatedly checks for older events until there aren't anymore or we've run past
+-- the timestamp of the last scan, then called DoScan to pick up sales events
+function Shopkeeper:ScanOlder(guildNum, doAlert)
+  local numEvents = GetNumGuildEvents(guildNum, GUILD_HISTORY_SALES)
+  local _, secsSince, _, _, _, _, _, _ = GetGuildEventInfo(guildNum, GUILD_HISTORY_SALES, numEvents)
+  local lastScan = 0
+  if Shopkeeper.acctSavedVariables.lastScan[guildNum] ~= nil then lastScan = Shopkeeper.acctSavedVariables.lastScan[guildNum] end
+  if not DoesGuildHistoryCategoryHaveMoreEvents(guildNum, GUILD_HISTORY_SALES) or
+     not RequestGuildHistoryCategoryOlder(guildNum, GUILD_HISTORY_SALES) or
+         GetDiffBetweenTimeStamps(lastScan, GetTimeStamp() - secsSince) > 0 then
+    zo_callLater(function() Shopkeeper:DoScan(guildNum, true, doAlert) end, 1250)
+  else
+    zo_callLater(function() Shopkeeper:ScanOlder(guildNum, doAlert) end, 1250)
+  end
+end
+
+-- Scans all stores a player has access to with delays between them.
 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 timeLimit = GetTimeStamp() - 59
   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
-
+  -- Nothing to scan!
+  if guildNum == 0 then return end
+  if not Shopkeeper.isScanning and (Shopkeeper.acctSavedVariables.lastScan[1] == nil or timeLimit > Shopkeeper.acctSavedVariables.lastScan[1]) then
     Shopkeeper.isScanning = true
-    if checkOlder and DoesGuildHistoryCategoryHaveMoreEvents(1, GUILD_HISTORY_SALES) then
-      RequestGuildHistoryCategoryNewest(1, GUILD_HISTORY_SALES)
-      RequestGuildHistoryCategoryOlder(1, GUILD_HISTORY_SALES)
+
+    -- We'll track if we get a true back here or not, because if it's false,
+    -- someone checked this history manually recently and times may be off
+    -- by up to 10-15 seconds because of that, and the next scan may get items
+    -- that should have been caught by this one
+    local didScan = RequestGuildHistoryCategoryNewest(1, GUILD_HISTORY_SALES)
+
+    if Shopkeeper.missedScan[1] ~= nil then
+      Shopkeeper.missedLastScan[1] = Shopkeeper.missedScan[1]
+    else
+      Shopkeeper.missedLastScan[1] = false
+    end
+
+    if not didScan then
+      Shopkeeper.missedScan[1] = true
     else
-      RequestGuildHistoryCategoryNewest(1, GUILD_HISTORY_SALES)
+      Shopkeeper.missedScan[1] = false
     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))
+
+    Shopkeeper.requestTimestamp = GetTimeStamp()
+    if checkOlder then
+      zo_callLater(function() Shopkeeper:ScanOlder(1, doAlert) end, 1250)
+    else
+      zo_callLater(function() Shopkeeper:DoScan(1, checkOlder, doAlert) end, 1250)
     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

@@ -1154,7 +1199,7 @@ function Shopkeeper.DoRefresh()
   -- or on purpose
   local timeLimit = timeStamp - 59
   local guildNum = GetNumGuilds()
-  if timeLimit > Shopkeeper.acctSavedVariables.lastScan[guildNum] then
+  if Shopkeeper.acctSavedVariables.lastScan[1] == nil or timeLimit > Shopkeeper.acctSavedVariables.lastScan[1] then
     CHAT_SYSTEM:AddMessage("[Shopkeeper] " .. Shopkeeper.translate('refreshStart'))
     Shopkeeper:ScanStores(false, true)

@@ -1625,11 +1670,4 @@ end
 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
\ No newline at end of file
+SLASH_COMMANDS["/shopkeeper"] = function() Shopkeeper.ToggleShopkeeperWindow() end
diff --git a/Shopkeeper.txt b/Shopkeeper.txt
index 2bef8ca..1bb232d 100644
--- a/Shopkeeper.txt
+++ b/Shopkeeper.txt
@@ -2,9 +2,9 @@
 ## APIVersion: 100008
 ## Description: Notifies you when you've sold something in a guild store and presents guild sales info in a convenient table.
 ## Author: Dan Stone (@khaibit) - dankitymao@gmail.com
-## Version: 0.9
+## Version: 0.9a
 ## License: See license - distribution without license is prohibited!
-## LastUpdated: August 4, 2014
+## LastUpdated: August 8, 2014
 ## SavedVariables: ShopkeeperSavedVars
 ## OptionalDependsOn: LibAddonMenu-2.0 LibMediaProvider-1.0 LibStub

diff --git a/Shopkeeper.xml b/Shopkeeper.xml
index d277117..e1d8b7f 100644
--- a/Shopkeeper.xml
+++ b/Shopkeeper.xml
@@ -1,6 +1,6 @@
 <!--
       Shopkeeper UI Layout File
-      Last Updated August 4, 2014
+      Last Updated August 8, 2014
       Written July 2014 by Dan Stone (@khaibit) - dankitymao@gmail.com
       Released under terms in license accompanying this file.
       Distribution without license is prohibited!
diff --git a/Shopkeeper_Namespace_Init.lua b/Shopkeeper_Namespace_Init.lua
index ea55f4b..a9fe120 100644
--- a/Shopkeeper_Namespace_Init.lua
+++ b/Shopkeeper_Namespace_Init.lua
@@ -1,12 +1,12 @@
 -- Shopkeeper Namespace Setup
--- Last Updated August 4, 2014
+-- Last Updated August 8, 2014
 -- Written July 2014 by Dan Stone (@khaibit) - dankitymao@gmail.com
 -- Released under terms in license accompanying this file.
 -- Distribution without license is prohibited!

 Shopkeeper = {}
 Shopkeeper.name = "Shopkeeper"
-Shopkeeper.version = "0.9"
+Shopkeeper.version = "0.9a"
 Shopkeeper.locale = "en"
 Shopkeeper.viewMode = "self"
 Shopkeeper.isScanning = false
@@ -19,6 +19,8 @@ Shopkeeper.MiniDataRows = {}
 Shopkeeper.ScanResults = {}
 Shopkeeper.SelfSales = {}
 Shopkeeper.SearchTable = {}
+Shopkeeper.missedScan = {}
+Shopkeeper.missedLastScan = {}
 Shopkeeper.alertQueue = {}
 Shopkeeper.shopSlider = {}
 Shopkeeper.curSort = {"time", "desc"}
diff --git a/readme b/readme
index 1773a47..6b715aa 100644
--- a/readme
+++ b/readme
@@ -4,12 +4,11 @@ trademarks or trademarks of ZeniMax Media Inc. in the United States and/or
 other countries. All rights reserved.


-Known Issues August 4, 2014:
-    The API calls to retrieve store sales histories currently return the most recent 100 sales from each guild. The
- function to retrieve older items behaves...inconsistently. If you are part of a busy guild and/or log on infrequently,
- Shopkeeper may miss some of your sales. If this is the case, you'll notice your sales also don't show up in the actual
- guild sales history window either - I can't report on what the servers won't tell me =\ I'm aware of it and trying to
- find a workaround!
+Changelog for 0.9a
+  Rewrite of part of the scanning routines to be more accurate
+  Fixes for odd behavior in the stats window
+  Fixes for the "Alert flood" issue if you sell multiple items between scans
+  Misc. other small bugfixes

 Changelog for 0.9 (version jump due to being nearly feature-complete):
   Added a new smaller view mode for the main window