run a sync from lore asynchronously on login

Daniel Pittman [08-26-18 - 15:11]
run a sync from lore asynchronously on login

This implements, based on libAsync, a background scan of all the
lore book records, so we can add the ones that we have seen, as
are recorded there, but which are not present in the internal
saved variables.

While this doesn't cover all books, it does cover more than we
ever had before, so fills out gaps in history.  Quite large gaps,
in my case.

This runs every login, because why not.
Filename
SlippyCheezeReadItOnce.lua
SlippyCheezeReadItOnce.txt
diff --git a/SlippyCheezeReadItOnce.lua b/SlippyCheezeReadItOnce.lua
index 2a8b78c..0e500b2 100644
--- a/SlippyCheezeReadItOnce.lua
+++ b/SlippyCheezeReadItOnce.lua
@@ -1,24 +1,34 @@
 -- Copyright © 2018 Daniel Pittman <daniel@rimspace.net>
 -- See LICENSE for more details.
-if not SlippyCheezeReadItOnce then
-  SlippyCheezeReadItOnce = {
-    ADDON_NAME="SlippyCheezeReadItOnce",
+SlippyCheeze = SlippyCheeze or {}
+
+if not SlippyCheeze.ReadItOnce then
+  SlippyCheeze.ReadItOnce = {
+    IS_RELEASE_VERSION = false,
+    NAME="SlippyCheezeReadItOnce",
     DISPLAY_NAME = "|c798BD2ReadItOnce|r",
-    DOUBLE_TAP_TIME = 1000,
+    -- for double-tap bypass of the block
     previousBook = {id=nil, time=0},
-    -- seen holds our saved variables.
+    DOUBLE_TAP_TIME = 1000,
+    -- used for reporting on our background achievement scan
+    async = nil,
+    lore = {
+      added = 0,
+      scanned = 0,
+      start = 0,
+    },
+    -- seen holds our saved variables, eg, seen books.
     seen = {}
   }
 end

--- my local alias for the addon itself.
-local M = SlippyCheezeReadItOnce
+local addon = SlippyCheeze.ReadItOnce

 local unpack = unpack
 local insert = table.insert

 -- reduce consing at runtime in debug message display
-local msg_prefix = M.DISPLAY_NAME..": "
+local msg_prefix = addon.DISPLAY_NAME..": "

 local function msg(fmt, ...)
   local args = {}
@@ -31,7 +41,7 @@ end

 -- return bool, have we seen this before.  never called before saved variables
 -- are loaded and initialized.
-function M:HaveSeenBookBefore(id, title, body)
+function addon:HaveSeenBookBefore(id, title, body)
   if type(id) ~= "number" then
     msg("ReadItOnce: id is <<1>> (<<2>>)", type(id), id)
     return false
@@ -67,7 +77,7 @@ end

 -- Called when we want to skip showing a book.  Probably going to be very
 -- strange if you call it any other time!
-function M:DoNotShowThisBook(title)
+function addon:DoNotShowThisBook(title)
   PlaySound(SOUNDS.NEGATIVE_CLICK)

   local params = CENTER_SCREEN_ANNOUNCE:CreateMessageParams(CSA_CATEGORY_SMALL_TEXT, nil)
@@ -86,24 +96,24 @@ end
 -- 100023
 --
 -- The HaveSeenBook logic is my addition.
-function M:OnShowBookOverride(eventCode, title, body, medium, showTitle, bookId)
+function addon:OnShowBookOverride(eventCode, title, body, medium, showTitle, bookId)
   -- never block a book if we are not in the most basic state, which is the
   -- world interaction state.
   if not SCENE_MANAGER:IsShowingBaseScene() then
-    return self.DoNotShowThisBook(title)
+    return self:DoNotShowThisBook(title)
   end

   -- seen before, block unless is double-tap within the limit
   if HaveSeenBookBefore(bookId, title, body) then
     -- different book from the last time?  block.
     if self.previousBook.id ~= bookId then
-      return self.DoNotShowThisBook(title)
+      return self:DoNotShowThisBook(title)
     end

     -- last book was more than our double-tap time ago?  block.
     local now = GetGameTimeMilliseconds()
     if (now - self.previousBook.time) > DOUBLE_TAP_TIME then
-      return self.DoNotShowThisBook(title)
+      return self:DoNotShowThisBook(title)
     end

     -- otherwise record this state for the future.
@@ -119,36 +129,59 @@ function M:OnShowBookOverride(eventCode, title, body, medium, showTitle, bookId)
   end
 end

-function M:SyncFromArchivementBookHistory()
-  local added = 0
-
-  for category = 1, GetNumLoreCategories() do
-    local _, numCollections, _ = GetLoreCategoryInfo(category)
-    for collection = 1, numCollections do
-      local _, _, _, numBooks, _, _, _ = GetLoreCollectionInfo(category, collection)
-      for book = 1, numBooks do
-        local title, _, known, id = GetLoreBookInfo(category, collection, book)
-        if known then
-          local body = ReadLoreBook(category, collection, book)
-          if not self:HaveSeenBookBefore(id, title, body) then
-            added = added + 1
-            -- msg("book <<1>>: id=<<2>>(<<3>>) title=<<4>>(<<5>>)) body=<<6>>", added, type(id), id, type(title), title, HashString(body))
-          end
-        end
-      end
+function addon:ScanOneLoreCategory(category)
+  local _, numCollections, _ = GetLoreCategoryInfo(category)
+  self.async:For(1, numCollections):Do(function(collection) self:ScanOneLoreCollection(category, collection) end)
+end
+
+function addon:ScanOneLoreCollection(category, collection)
+  local _, _, _, numBooks, _, _, _ = GetLoreCollectionInfo(category, collection)
+  self.async:For(1, numBooks):Do(function(book) self:ScanOneLoreBook(category, collection, book) end)
+end
+
+function addon:ScanOneLoreBook(category, collection, book)
+  self.lore.scanned = self.lore.scanned + 1
+
+  local title, _, known, id = GetLoreBookInfo(category, collection, book)
+  if known then
+    local body = ReadLoreBook(category, collection, book)
+    if not self:HaveSeenBookBefore(id, title, body) then
+      self.lore.added = self.lore.added + 1
     end
   end
+end

-  if added > 0 then
+function addon:ReportAfterLoreScan()
+  if self.lore.added > 0 then
     -- ZOS quirk: the number **must** be the third argument.  the plural must
     -- be a substitution of text.
-    msg('added <<2>> <<m:1>> found in your achievements.', 'previously read book', added)
+    msg('added <<2>> <<m:1>> found in your achievements.', 'previously read book', self.lore.added)
+  end
+
+  if not self.IS_RELEASE_VERSION then
+    local duration = FormatTimeMilliseconds(
+      GetGameTimeMilliseconds() - self.lore.start,
+      TIME_FORMAT_STYLE_DESCRIPTIVE_MINIMAL_SHOW_TENTHS_SECS,
+      TIME_FORMAT_PRECISION_TENTHS_RELEVANT,
+      TIME_FORMAT_DIRECTION_NONE)
+    msg('SyncFromLoreBooks: scan ran for <<1>> total', duration)
   end
 end

-function M:OnAddonLoaded(name)
-  if name ~= M.ADDON_NAME then return end
-  EVENT_MANAGER:UnregisterForEvent(M.ADDON_NAME, EVENT_ADD_ON_LOADED)
+function addon:SyncFromLoreBooks()
+  self.async = LibStub("LibAsync"):Create(self.NAME)
+
+  self.lore.added = 0
+  self.lore.scanned = 0
+  self.lore.start = GetGameTimeMilliseconds()
+
+  self.async:For(1, GetNumLoreCategories()):Do(function(category) self:ScanOneLoreCategory(category) end)
+  self.async:Then(function() self:ReportAfterLoreScan() end)
+end
+
+function addon:OnAddonLoaded(name)
+  if name ~= addon.NAME then return end
+  EVENT_MANAGER:UnregisterForEvent(addon.NAME, EVENT_ADD_ON_LOADED)

   -- if the second argument, the version, changes then the data is wiped and
   -- replaced with the defaults.
@@ -157,8 +190,18 @@ function M:OnAddonLoaded(name)
   -- replace the original event handler with ours; sadly, we don't have
   -- access to the original implementation to do anything nicer. :/
   LORE_READER.control:UnregisterForEvent(EVENT_SHOW_BOOK)
-  LORE_READER.control:RegisterForEvent(EVENT_SHOW_BOOK, function(...) self:OnShowBookOverride(...) end)
+  LORE_READER.control:RegisterForEvent(EVENT_SHOW_BOOK,
+                                       function(...) self:OnShowBookOverride(...) end)
+
+  -- and once we actually log in, scan the collections for missing records in
+  -- our data on what we have seen, since this is the only in-game history we
+  -- can use...
+  local function SyncFromLoreBooksShim(...)
+    EVENT_MANAGER:UnregisterForEvent(addon.NAME, EVENT_PLAYER_ACTIVATED)
+    addon:SyncFromLoreBooks()
+  end
+  EVENT_MANAGER:RegisterForEvent(addon.NAME, EVENT_PLAYER_ACTIVATED, SyncFromLoreBooksShim)
 end

 -- bootstrapping
-EVENT_MANAGER:RegisterForEvent(M.ADDON_NAME, EVENT_ADD_ON_LOADED, function(_, name) M:OnAddonLoaded(name) end)
+EVENT_MANAGER:RegisterForEvent(addon.NAME, EVENT_ADD_ON_LOADED, function(_, name) addon:OnAddonLoaded(name) end)
diff --git a/SlippyCheezeReadItOnce.txt b/SlippyCheezeReadItOnce.txt
index ceb5f2f..e37133a 100644
--- a/SlippyCheezeReadItOnce.txt
+++ b/SlippyCheezeReadItOnce.txt
@@ -5,6 +5,7 @@
 ## AddOnVersion: 1000000
 ## APIVersion: 100024
 ## SavedVariables: SlippyCheezeReadItOnceData
+## DependsOn: LibStub LibAsync

 SlippyCheezeReadItOnce.lua