-- ------------------------------------------------------------------------------ -- -- TradeSkillMaster -- -- https://tradeskillmaster.com -- -- All Rights Reserved - Detailed license information included with addon. -- -- ------------------------------------------------------------------------------ -- local _, TSM = ... local Auctions = TSM.Accounting:NewPackage("Auctions") local Database = TSM.Include("Util.Database") local CSV = TSM.Include("Util.CSV") local String = TSM.Include("Util.String") local Log = TSM.Include("Util.Log") local ItemString = TSM.Include("Util.ItemString") local CustomPrice = TSM.Include("Service.CustomPrice") local private = { db = nil, numExpiresQuery = nil, dataChanged = false, statsQuery = nil, statsTemp = {}, } local COMBINE_TIME_THRESHOLD = 300 -- group expenses within 5 minutes together local REMOVE_OLD_THRESHOLD = 180 * 24 * 60 * 60 -- remove records over 6 months old local SECONDS_PER_DAY = 24 * 60 * 60 local CSV_KEYS = { "itemString", "stackSize", "quantity", "player", "time" } -- ============================================================================ -- Module Functions -- ============================================================================ function Auctions.OnInitialize() private.db = Database.NewSchema("ACCOUNTING_AUCTIONS") :AddStringField("baseItemString") :AddStringField("type") :AddStringField("itemString") :AddNumberField("stackSize") :AddNumberField("quantity") :AddStringField("player") :AddNumberField("time") :AddNumberField("saveTime") :AddIndex("baseItemString") :AddIndex("time") :Commit() private.numExpiresQuery = private.db:NewQuery() :Select("quantity") :Equal("type", "expire") :Equal("baseItemString", Database.BoundQueryParam()) :GreaterThanOrEqual("time", Database.BoundQueryParam()) private.statsQuery = private.db:NewQuery() :Select("type", "quantity") :Equal("baseItemString", Database.BoundQueryParam()) :GreaterThanOrEqual("time", Database.BoundQueryParam()) private.db:BulkInsertStart() private.LoadData("cancel", TSM.db.realm.internalData.csvCancelled, TSM.db.realm.internalData.saveTimeCancels) private.LoadData("expire", TSM.db.realm.internalData.csvExpired, TSM.db.realm.internalData.saveTimeExpires) private.db:BulkInsertEnd() CustomPrice.OnSourceChange("NumExpires") end function Auctions.OnDisable() if not private.dataChanged then -- nothing changed, so no need to save return end local cancelSaveTimes, expireSaveTimes = {}, {} local cancelEncodeContext = CSV.EncodeStart(CSV_KEYS) local expireEncodeContext = CSV.EncodeStart(CSV_KEYS) -- order by time to speed up loading local query = private.db:NewQuery() :Select("type", "itemString", "stackSize", "quantity", "player", "time", "saveTime") :OrderBy("time", true) for _, recordType, itemString, stackSize, quantity, player, timestamp, saveTime in query:Iterator() do local saveTimes, encodeContext = nil, nil if recordType == "cancel" then saveTimes = cancelSaveTimes encodeContext = cancelEncodeContext elseif recordType == "expire" then saveTimes = expireSaveTimes encodeContext = expireEncodeContext else error("Invalid recordType: "..tostring(recordType)) end -- add the save time tinsert(saveTimes, saveTime ~= 0 and saveTime or time()) -- add to our list of CSV lines CSV.EncodeAddRowDataRaw(encodeContext, itemString, stackSize, quantity, player, timestamp) end query:Release() TSM.db.realm.internalData.csvCancelled = CSV.EncodeEnd(cancelEncodeContext) TSM.db.realm.internalData.saveTimeCancels = table.concat(cancelSaveTimes, ",") TSM.db.realm.internalData.csvExpired = CSV.EncodeEnd(expireEncodeContext) TSM.db.realm.internalData.saveTimeExpires = table.concat(expireSaveTimes, ",") end function Auctions.InsertCancel(itemString, stackSize, timestamp) private.InsertRecord("cancel", itemString, stackSize, timestamp) end function Auctions.InsertExpire(itemString, stackSize, timestamp) private.InsertRecord("expire", itemString, stackSize, timestamp) end function Auctions.GetStats(itemString, minTime) private.statsQuery:BindParams(ItemString.GetBase(itemString), minTime or 0) wipe(private.statsTemp) private.statsQuery:GroupedSum("type", "quantity", private.statsTemp) local cancel = private.statsTemp.cancel or 0 local expire = private.statsTemp.expire or 0 local total = cancel + expire return cancel, expire, total end function Auctions.GetNumExpires(itemString, minTime) private.numExpiresQuery:BindParams(ItemString.GetBase(itemString), minTime or 0) local num = 0 for _, quantity in private.numExpiresQuery:Iterator() do num = num + quantity end return num end function Auctions.GetNumExpiresSinceSale(itemString) return Auctions.GetNumExpires(itemString, TSM.Accounting.Transactions.GetLastSaleTime(itemString)) end function Auctions.CreateQuery() return private.db:NewQuery() end function Auctions.RemoveOldData(days) private.dataChanged = true private.db:SetQueryUpdatesPaused(true) local numRecords = private.db:NewQuery() :LessThan("time", time() - days * SECONDS_PER_DAY) :DeleteAndRelease() private.db:SetQueryUpdatesPaused(false) CustomPrice.OnSourceChange("NumExpires") return numRecords end -- ============================================================================ -- Private Helper Functions -- ============================================================================ function private.LoadData(recordType, csvRecords, csvSaveTimes) local saveTimes = String.SafeSplit(csvSaveTimes, ",") if not saveTimes then return end local decodeContext = CSV.DecodeStart(csvRecords, CSV_KEYS) if not decodeContext then Log.Err("Failed to decode %s records", recordType) private.dataChanged = true return end local removeTime = time() - REMOVE_OLD_THRESHOLD local index = 1 local prevTimestamp = 0 for itemString, stackSize, quantity, player, timestamp in CSV.DecodeIterator(decodeContext) do itemString = ItemString.Get(itemString) local baseItemString = ItemString.GetBaseFast(itemString) local saveTime = tonumber(saveTimes[index]) stackSize = tonumber(stackSize) quantity = tonumber(quantity) timestamp = tonumber(timestamp) if itemString and baseItemString and stackSize and quantity and timestamp and saveTime and timestamp > removeTime then local newTimestamp = floor(timestamp) if newTimestamp ~= timestamp then -- make sure all timestamps are stored as integers private.dataChanged = true timestamp = newTimestamp end if timestamp < prevTimestamp then -- not ordered by timestamp private.dataChanged = true end prevTimestamp = timestamp private.db:BulkInsertNewRowFast8(baseItemString, recordType, itemString, stackSize, quantity, player, timestamp, saveTime) else private.dataChanged = true end index = index + 1 end if not CSV.DecodeEnd(decodeContext) then Log.Err("Failed to decode %s records", recordType) private.dataChanged = true end CustomPrice.OnSourceChange("NumExpires") end function private.InsertRecord(recordType, itemString, stackSize, timestamp) private.dataChanged = true assert(itemString and stackSize and stackSize > 0 and timestamp) timestamp = floor(timestamp) local baseItemString = ItemString.GetBase(itemString) local matchingRow = private.db:NewQuery() :Equal("type", recordType) :Equal("baseItemString", baseItemString) :Equal("itemString", itemString) :Equal("stackSize", stackSize) :Equal("player", UnitName("player")) :GreaterThan("time", timestamp - COMBINE_TIME_THRESHOLD) :LessThan("time", timestamp + COMBINE_TIME_THRESHOLD) :Equal("saveTime", 0) :GetFirstResultAndRelease() if matchingRow then matchingRow:SetField("quantity", matchingRow:GetField("quantity") + stackSize) matchingRow:Update() matchingRow:Release() else private.db:NewRow() :SetField("baseItemString", baseItemString) :SetField("type", recordType) :SetField("itemString", itemString) :SetField("stackSize", stackSize) :SetField("quantity", stackSize) :SetField("player", UnitName("player")) :SetField("time", timestamp) :SetField("saveTime", 0) :Create() end if recordType == "expire" then CustomPrice.OnSourceChange("NumExpires", itemString) end end