TradeSkillMaster/Core/Service/Accounting/Auctions.lua

236 lines
8.2 KiB
Lua

-- ------------------------------------------------------------------------------ --
-- 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