TradeSkillMaster/Core/Service/Accounting/Transactions.lua

826 lines
28 KiB
Lua

-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Transactions = TSM.Accounting:NewPackage("Transactions")
local L = TSM.Include("Locale").GetTable()
local Database = TSM.Include("Util.Database")
local TempTable = TSM.Include("Util.TempTable")
local CSV = TSM.Include("Util.CSV")
local Math = TSM.Include("Util.Math")
local String = TSM.Include("Util.String")
local Log = TSM.Include("Util.Log")
local Table = TSM.Include("Util.Table")
local ItemString = TSM.Include("Util.ItemString")
local Theme = TSM.Include("Util.Theme")
local CustomPrice = TSM.Include("Service.CustomPrice")
local ItemInfo = TSM.Include("Service.ItemInfo")
local Inventory = TSM.Include("Service.Inventory")
local Settings = TSM.Include("Service.Settings")
local Threading = TSM.Include("Service.Threading")
local private = {
db = nil,
dbSummary = nil,
dataChanged = false,
baseStatsQuery = nil,
statsQuery = nil,
baseStatsMinTimeQuery = nil,
statsMinTimeQuery = nil,
syncHashesThread = nil,
isSyncHashesThreadRunning = false,
syncHashDayCache = {},
syncHashDayCacheIsInvalid = {},
pendingSyncHashCharacters = {},
}
local OLD_CSV_KEYS = {
sale = { "itemString", "stackSize", "quantity", "price", "buyer", "player", "time", "source" },
buy = { "itemString", "stackSize", "quantity", "price", "seller", "player", "time", "source" },
}
local CSV_KEYS = { "itemString", "stackSize", "quantity", "price", "otherPlayer", "player", "time", "source" }
local COMBINE_TIME_THRESHOLD = 300 -- group transactions within 5 minutes together
local MAX_CSV_RECORDS = 55000 -- the max number of records we can store without WoW corrupting the SV file
local TRIMMED_CSV_RECORDS = 50000 -- how many records to trim to if we're over the limit (so we don't trim every time)
local SECONDS_PER_DAY = 24 * 60 * 60
local SYNC_FIELDS = { "type", "itemString", "stackSize", "quantity", "price", "otherPlayer", "time", "source", "saveTime" }
-- ============================================================================
-- Module Functions
-- ============================================================================
function Transactions.OnInitialize()
if TSM.db.realm.internalData.accountingTrimmed.sales then
Log.PrintfUser(L["%sIMPORTANT:|r When Accounting data was last saved for this realm, it was too big for WoW to handle, so old data was automatically trimmed in order to avoid corruption of the saved variables. The last %s of sale data has been preserved."], Theme.GetFeedbackColor("RED"):GetTextColorPrefix(), SecondsToTime(time() - TSM.db.realm.internalData.accountingTrimmed.sales))
TSM.db.realm.internalData.accountingTrimmed.sales = nil
end
if TSM.db.realm.internalData.accountingTrimmed.buys then
Log.PrintfUser(L["%sIMPORTANT:|r When Accounting data was last saved for this realm, it was too big for WoW to handle, so old data was automatically trimmed in order to avoid corruption of the saved variables. The last %s of purchase data has been preserved."], Theme.GetFeedbackColor("RED"):GetTextColorPrefix(), SecondsToTime(time() - TSM.db.realm.internalData.accountingTrimmed.buys))
TSM.db.realm.internalData.accountingTrimmed.buys = nil
end
private.db = Database.NewSchema("TRANSACTIONS_LOG")
:AddStringField("baseItemString")
:AddStringField("type")
:AddStringField("itemString")
:AddNumberField("stackSize")
:AddNumberField("quantity")
:AddNumberField("price")
:AddStringField("otherPlayer")
:AddStringField("player")
:AddNumberField("time")
:AddStringField("source")
:AddNumberField("saveTime")
:AddIndex("baseItemString")
:AddIndex("time")
:Commit()
private.db:BulkInsertStart()
private.LoadData("sale", TSM.db.realm.internalData.csvSales, TSM.db.realm.internalData.saveTimeSales)
private.LoadData("buy", TSM.db.realm.internalData.csvBuys, TSM.db.realm.internalData.saveTimeBuys)
private.db:BulkInsertEnd()
private.dbSummary = Database.NewSchema("TRANSACTIONS_SUMMARY")
:AddUniqueStringField("itemString")
:AddNumberField("sold")
:AddNumberField("avgSellPrice")
:AddNumberField("bought")
:AddNumberField("avgBuyPrice")
:AddNumberField("avgProfit")
:AddNumberField("totalProfit")
:AddNumberField("profitPct")
:Commit()
private.baseStatsQuery = private.db:NewQuery()
:Select("quantity", "price")
:Equal("type", Database.BoundQueryParam())
:Equal("baseItemString", Database.BoundQueryParam())
:NotEqual("source", "Vendor")
private.statsQuery = private.db:NewQuery()
:Select("quantity", "price")
:Equal("type", Database.BoundQueryParam())
:Equal("baseItemString", Database.BoundQueryParam())
:Equal("itemString", Database.BoundQueryParam())
:NotEqual("source", "Vendor")
private.baseStatsMinTimeQuery = private.db:NewQuery()
:Select("quantity", "price")
:Equal("type", Database.BoundQueryParam())
:Equal("baseItemString", Database.BoundQueryParam())
:GreaterThanOrEqual("time", Database.BoundQueryParam())
:NotEqual("source", "Vendor")
private.statsMinTimeQuery = private.db:NewQuery()
:Select("quantity", "price")
:Equal("type", Database.BoundQueryParam())
:Equal("baseItemString", Database.BoundQueryParam())
:Equal("itemString", Database.BoundQueryParam())
:GreaterThanOrEqual("time", Database.BoundQueryParam())
:NotEqual("source", "Vendor")
private.syncHashesThread = Threading.New("TRANSACTIONS_SYNC_HASHES", private.SyncHashesThread)
Inventory.RegisterCallback(private.InventoryCallback)
end
function Transactions.OnDisable()
if not private.dataChanged then
-- nothing changed, so just keep the previous saved values
return
end
TSM.db.realm.internalData.csvSales, TSM.db.realm.internalData.saveTimeSales, TSM.db.realm.internalData.accountingTrimmed.sales = private.SaveData("sale")
TSM.db.realm.internalData.csvBuys, TSM.db.realm.internalData.saveTimeBuys, TSM.db.realm.internalData.accountingTrimmed.buys = private.SaveData("buy")
end
function Transactions.InsertAuctionSale(itemString, stackSize, price, buyer, timestamp)
private.InsertRecord("sale", itemString, "Auction", stackSize, price, buyer, timestamp)
end
function Transactions.InsertAuctionBuy(itemString, stackSize, price, seller, timestamp)
private.InsertRecord("buy", itemString, "Auction", stackSize, price, seller, timestamp)
end
function Transactions.InsertCODSale(itemString, stackSize, price, buyer, timestamp)
private.InsertRecord("sale", itemString, "COD", stackSize, price, buyer, timestamp)
end
function Transactions.InsertCODBuy(itemString, stackSize, price, seller, timestamp)
private.InsertRecord("buy", itemString, "COD", stackSize, price, seller, timestamp)
end
function Transactions.InsertTradeSale(itemString, stackSize, price, buyer)
private.InsertRecord("sale", itemString, "Trade", stackSize, price, buyer, time())
end
function Transactions.InsertTradeBuy(itemString, stackSize, price, seller)
private.InsertRecord("buy", itemString, "Trade", stackSize, price, seller, time())
end
function Transactions.InsertVendorSale(itemString, stackSize, price)
private.InsertRecord("sale", itemString, "Vendor", stackSize, price, "Merchant", time())
end
function Transactions.InsertVendorBuy(itemString, stackSize, price)
private.InsertRecord("buy", itemString, "Vendor", stackSize, price, "Merchant", time())
end
function Transactions.CreateQuery()
return private.db:NewQuery()
end
function Transactions.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)
private.OnItemRecordsChanged("sale")
private.OnItemRecordsChanged("buy")
TSM.Accounting.Sync.OnTransactionsChanged()
return numRecords
end
function Transactions.GetSaleStats(itemString, minTime)
local baseItemString = ItemString.GetBase(itemString)
local isBaseItemString = itemString == baseItemString
local query = nil
if minTime then
if isBaseItemString then
query = private.baseStatsMinTimeQuery:BindParams("sale", baseItemString, minTime)
else
query = private.statsMinTimeQuery:BindParams("sale", baseItemString, itemString, minTime)
end
else
if isBaseItemString then
query = private.baseStatsQuery:BindParams("sale", baseItemString)
else
query = private.statsQuery:BindParams("sale", baseItemString, itemString)
end
end
query:ResetOrderBy()
local totalPrice = query:SumOfProduct("quantity", "price")
local totalNum = query:Sum("quantity")
if not totalNum or totalNum == 0 then
return
end
return totalPrice, totalNum
end
function Transactions.GetBuyStats(itemString, isSmart)
local baseItemString = ItemString.GetBaseFast(itemString)
local isBaseItemString = itemString == baseItemString
local query = nil
if isBaseItemString then
query = private.baseStatsQuery:BindParams("buy", baseItemString)
else
query = private.statsQuery:BindParams("buy", baseItemString, itemString)
end
query:ResetOrderBy()
if isSmart then
local totalQuantity = CustomPrice.GetItemPrice(itemString, "NumInventory") or 0
if totalQuantity == 0 then
return nil, nil
end
query:OrderBy("time", false)
local remainingSmartQuantity = totalQuantity
local priceSum, quantitySum = 0, 0
for _, quantity, price in query:Iterator() do
if remainingSmartQuantity > 0 then
quantity = min(remainingSmartQuantity, quantity)
remainingSmartQuantity = remainingSmartQuantity - quantity
priceSum = priceSum + price * quantity
quantitySum = quantitySum + quantity
end
end
if priceSum == 0 then
return nil, nil
end
return priceSum, quantitySum
else
local quantitySum = query:Sum("quantity")
if not quantitySum then
return nil, nil
end
local priceSum = query:SumOfProduct("quantity", "price")
if priceSum == 0 then
return nil, nil
end
return priceSum, quantitySum
end
end
function Transactions.GetMaxSalePrice(itemString)
local baseItemString = ItemString.GetBase(itemString)
local isBaseItemString = itemString == baseItemString
local query = private.db:NewQuery()
:Select("price")
:Equal("type", "sale")
:NotEqual("source", "Vendor")
:OrderBy("price", false)
if isBaseItemString then
query:Equal("baseItemString", itemString)
else
query:Equal("baseItemString", baseItemString)
:Equal("itemString", itemString)
end
return query:GetFirstResultAndRelease()
end
function Transactions.GetMaxBuyPrice(itemString)
local baseItemString = ItemString.GetBase(itemString)
local isBaseItemString = itemString == baseItemString
local query = private.db:NewQuery()
:Select("price")
:Equal("type", "buy")
:NotEqual("source", "Vendor")
:OrderBy("price", false)
if isBaseItemString then
query:Equal("baseItemString", itemString)
else
query:Equal("baseItemString", baseItemString)
:Equal("itemString", itemString)
end
return query:GetFirstResultAndRelease()
end
function Transactions.GetMinSalePrice(itemString)
local baseItemString = ItemString.GetBase(itemString)
local isBaseItemString = itemString == baseItemString
local query = private.db:NewQuery()
:Select("price")
:Equal("type", "sale")
:NotEqual("source", "Vendor")
:OrderBy("price", true)
if isBaseItemString then
query:Equal("baseItemString", itemString)
else
query:Equal("baseItemString", baseItemString)
:Equal("itemString", itemString)
end
return query:GetFirstResultAndRelease()
end
function Transactions.GetMinBuyPrice(itemString)
local baseItemString = ItemString.GetBase(itemString)
local isBaseItemString = itemString == baseItemString
local query = private.db:NewQuery()
:Select("price")
:Equal("type", "buy")
:NotEqual("source", "Vendor")
:OrderBy("price", true)
if isBaseItemString then
query:Equal("baseItemString", itemString)
else
query:Equal("baseItemString", baseItemString)
:Equal("itemString", itemString)
end
return query:GetFirstResultAndRelease()
end
function Transactions.GetAverageSalePrice(itemString)
local totalPrice, totalNum = Transactions.GetSaleStats(itemString)
if not totalPrice or totalPrice == 0 then
return
end
return Math.Round(totalPrice / totalNum), totalNum
end
function Transactions.GetAverageBuyPrice(itemString, isSmart)
local totalPrice, totalNum = Transactions.GetBuyStats(itemString, isSmart)
return totalPrice and Math.Round(totalPrice / totalNum) or nil
end
function Transactions.GetLastSaleTime(itemString)
local baseItemString = ItemString.GetBase(itemString)
local isBaseItemString = itemString == baseItemString
local query = private.db:NewQuery()
:Select("time")
:Equal("type", "sale")
:NotEqual("source", "Vendor")
:OrderBy("time", false)
if isBaseItemString then
query:Equal("baseItemString", itemString)
else
query:Equal("baseItemString", baseItemString)
:Equal("itemString", itemString)
end
return query:GetFirstResultAndRelease()
end
function Transactions.GetLastBuyTime(itemString)
local baseItemString = ItemString.GetBase(itemString)
local isBaseItemString = itemString == baseItemString
local query = private.db:NewQuery():Select("time")
:Equal("type", "buy")
:NotEqual("source", "Vendor")
:OrderBy("time", false)
if isBaseItemString then
query:Equal("baseItemString", itemString)
else
query:Equal("baseItemString", baseItemString)
:Equal("itemString", itemString)
end
return query:GetFirstResultAndRelease()
end
function Transactions.GetQuantity(itemString, timeFilter, typeFilter)
local baseItemString = ItemString.GetBase(itemString)
local isBaseItemString = itemString == baseItemString
local query = private.db:NewQuery()
:Equal("type", typeFilter)
if isBaseItemString then
query:Equal("baseItemString", itemString)
else
query:Equal("baseItemString", baseItemString)
:Equal("itemString", itemString)
end
if timeFilter then
query:GreaterThan("time", time() - timeFilter)
end
local sum = query:Sum("quantity") or 0
query:Release()
return sum
end
function Transactions.GetAveragePrice(itemString, timeFilter, typeFilter)
local baseItemString = ItemString.GetBase(itemString)
local isBaseItemString = itemString == baseItemString
local query = private.db:NewQuery()
:Select("price", "quantity")
:Equal("type", typeFilter)
if isBaseItemString then
query:Equal("baseItemString", itemString)
else
query:Equal("baseItemString", baseItemString)
:Equal("itemString", itemString)
end
if timeFilter then
query:GreaterThan("time", time() - timeFilter)
end
local avgPrice = 0
local totalQuantity = 0
for _, price, quantity in query:IteratorAndRelease() do
avgPrice = avgPrice + price * quantity
totalQuantity = totalQuantity + quantity
end
return Math.Round(avgPrice / totalQuantity)
end
function Transactions.GetTotalPrice(itemString, timeFilter, typeFilter)
local baseItemString = ItemString.GetBase(itemString)
local isBaseItemString = itemString == baseItemString
local query = private.db:NewQuery()
:Select("price", "quantity")
:Equal("type", typeFilter)
if isBaseItemString then
query:Equal("baseItemString", itemString)
else
query:Equal("baseItemString", baseItemString)
:Equal("itemString", itemString)
end
if timeFilter then
query:GreaterThan("time", time() - timeFilter)
end
local sumPrice = query:SumOfProduct("price", "quantity") or 0
query:Release()
return sumPrice
end
function Transactions.CreateSummaryQuery()
return private.dbSummary:NewQuery()
end
function Transactions.UpdateSummaryData(groupFilter, searchFilter, typeFilter, characterFilter, minTime)
local totalSold = TempTable.Acquire()
local totalSellPrice = TempTable.Acquire()
local totalBought = TempTable.Acquire()
local totalBoughtPrice = TempTable.Acquire()
local items = private.db:NewQuery()
:Select("itemString", "price", "quantity", "type")
:LeftJoin(TSM.Groups.GetItemDBForJoin(), "itemString")
:InnerJoin(ItemInfo.GetDBForJoin(), "itemString")
if groupFilter then
items:InTable("groupPath", groupFilter)
end
if searchFilter then
items:Matches("name", String.Escape(searchFilter))
end
if typeFilter then
items:InTable("source", typeFilter)
end
if characterFilter then
items:InTable("player", characterFilter)
end
if minTime then
items:GreaterThan("time", minTime)
end
for _, itemString, price, quantity, recordType in items:IteratorAndRelease() do
if not totalSold[itemString] then
totalSold[itemString] = 0
totalSellPrice[itemString] = 0
totalBought[itemString] = 0
totalBoughtPrice[itemString] = 0
end
if recordType == "sale" then
totalSold[itemString] = totalSold[itemString] + quantity
totalSellPrice[itemString] = totalSellPrice[itemString] + price * quantity
elseif recordType == "buy" then
totalBought[itemString] = totalBought[itemString] + quantity
totalBoughtPrice[itemString] = totalBoughtPrice[itemString] + price * quantity
else
error("Invalid recordType: "..tostring(recordType))
end
end
private.dbSummary:TruncateAndBulkInsertStart()
for itemString, sold in pairs(totalSold) do
if sold > 0 and totalBought[itemString] > 0 then
local totalAvgSellPrice = totalSellPrice[itemString] / totalSold[itemString]
local totalAvgBuyPrice = totalBoughtPrice[itemString] / totalBought[itemString]
local profit = totalAvgSellPrice - totalAvgBuyPrice
local totalProfit = profit * min(totalSold[itemString], totalBought[itemString])
local profitPct = Math.Round(profit * 100 / totalAvgBuyPrice)
private.dbSummary:BulkInsertNewRow(itemString, sold, totalAvgSellPrice, totalBought[itemString], totalAvgBuyPrice, profit, totalProfit, profitPct)
end
end
private.dbSummary:BulkInsertEnd()
TempTable.Release(totalSold)
TempTable.Release(totalSellPrice)
TempTable.Release(totalBought)
TempTable.Release(totalBoughtPrice)
end
function Transactions.GetCharacters(characters)
private.db:NewQuery()
:Distinct("player")
:Select("player")
:AsTable(characters)
:Release()
return characters
end
function Transactions.CanDeleteByUUID(uuid)
return Settings.IsCurrentAccountOwner(private.db:GetRowFieldByUUID(uuid, "player"))
end
function Transactions.RemoveRowByUUID(uuid)
local recordType = private.db:GetRowFieldByUUID(uuid, "type")
local itemString = private.db:GetRowFieldByUUID(uuid, "itemString")
local player = private.db:GetRowFieldByUUID(uuid, "player")
private.db:DeleteRowByUUID(uuid)
if private.syncHashDayCache[player] then
private.syncHashDayCacheIsInvalid[player] = true
end
private.dataChanged = true
private.OnItemRecordsChanged(recordType, itemString)
TSM.Accounting.Sync.OnTransactionsChanged()
end
function Transactions.PrepareSyncHashes(player)
tinsert(private.pendingSyncHashCharacters, player)
if not private.isSyncHashesThreadRunning then
private.isSyncHashesThreadRunning = true
Threading.Start(private.syncHashesThread)
end
end
function Transactions.GetSyncHash(player)
local hashesByDay = Transactions.GetSyncHashByDay(player)
if not hashesByDay then
return
end
return Math.CalculateHash(hashesByDay)
end
function Transactions.GetSyncHashByDay(player)
if not private.syncHashDayCache[player] or private.syncHashDayCacheIsInvalid[player] then
return
end
return private.syncHashDayCache[player]
end
function Transactions.GetSyncData(player, day, result)
local query = private.db:NewQuery()
:Equal("player", player)
:GreaterThanOrEqual("time", day * SECONDS_PER_DAY)
:LessThan("time", (day + 1) * SECONDS_PER_DAY)
for _, row in query:Iterator() do
Table.Append(result, row:GetFields(unpack(SYNC_FIELDS)))
end
query:Release()
end
function Transactions.RemovePlayerDay(player, day)
private.dataChanged = true
private.db:SetQueryUpdatesPaused(true)
local query = private.db:NewQuery()
:Equal("player", player)
:GreaterThanOrEqual("time", day * SECONDS_PER_DAY)
:LessThan("time", (day + 1) * SECONDS_PER_DAY)
for _, uuid in query:UUIDIterator() do
private.db:DeleteRowByUUID(uuid)
end
query:Release()
if private.syncHashDayCache[player] then
private.syncHashDayCacheIsInvalid[player] = true
end
private.db:SetQueryUpdatesPaused(false)
private.OnItemRecordsChanged("sale")
private.OnItemRecordsChanged("buy")
end
function Transactions.HandleSyncedData(player, day, data)
assert(#data % 9 == 0)
private.dataChanged = true
private.db:SetQueryUpdatesPaused(true)
-- remove any prior data for the day
local query = private.db:NewQuery()
:Equal("player", player)
:GreaterThanOrEqual("time", day * SECONDS_PER_DAY)
:LessThan("time", (day + 1) * SECONDS_PER_DAY)
for _, uuid in query:UUIDIterator() do
private.db:DeleteRowByUUID(uuid)
end
query:Release()
if private.syncHashDayCache[player] then
private.syncHashDayCacheIsInvalid[player] = true
end
-- insert the new data
private.db:BulkInsertStart()
for i = 1, #data, 9 do
private.BulkInsertNewRowHelper(player, unpack(data, i, i + 8))
end
private.db:BulkInsertEnd()
private.db:SetQueryUpdatesPaused(false)
private.OnItemRecordsChanged("sale")
private.OnItemRecordsChanged("buy")
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.LoadData(recordType, csvRecords, csvSaveTimes)
local saveTimes = String.SafeSplit(csvSaveTimes, ",")
local decodeContext = CSV.DecodeStart(csvRecords, OLD_CSV_KEYS[recordType]) or CSV.DecodeStart(csvRecords, CSV_KEYS)
if not decodeContext then
Log.Err("Failed to decode %s records", recordType)
private.dataChanged = true
return
end
local saveTimeIndex = 1
for itemString, stackSize, quantity, price, otherPlayer, player, timestamp, source in CSV.DecodeIterator(decodeContext) do
local saveTime = 0
if saveTimes and source == "Auction" then
saveTime = tonumber(saveTimes[saveTimeIndex])
saveTimeIndex = saveTimeIndex + 1
end
private.BulkInsertNewRowHelper(player, recordType, itemString, stackSize, quantity, price, otherPlayer, timestamp, source, saveTime)
end
if not CSV.DecodeEnd(decodeContext) then
Log.Err("Failed to decode %s records", recordType)
private.dataChanged = true
end
private.OnItemRecordsChanged(recordType)
end
function private.BulkInsertNewRowHelper(player, recordType, itemString, stackSize, quantity, price, otherPlayer, timestamp, source, saveTime)
itemString = ItemString.Get(itemString)
local baseItemString = ItemString.GetBaseFast(itemString)
stackSize = tonumber(stackSize)
quantity = tonumber(quantity)
price = tonumber(price)
timestamp = tonumber(timestamp)
if itemString and stackSize and quantity and price and otherPlayer and player and timestamp and source then
local newTimestamp = floor(timestamp)
if newTimestamp ~= timestamp then
-- make sure all timestamps are stored as integers
private.dataChanged = true
timestamp = newTimestamp
end
local newPrice = floor(price)
if newPrice ~= price then
-- make sure all prices are stored as integers
private.dataChanged = true
price = newPrice
end
private.db:BulkInsertNewRowFast11(baseItemString, recordType, itemString, stackSize, quantity, price, otherPlayer, player, timestamp, source, saveTime)
else
private.dataChanged = true
end
end
function private.SaveData(recordType)
local numRecords = private.db:NewQuery()
:Equal("type", recordType)
:CountAndRelease()
if numRecords > MAX_CSV_RECORDS then
local query = private.db:NewQuery()
:Equal("type", recordType)
:OrderBy("time", false)
local count = 0
local saveTimes = {}
local shouldTrim = query:Count() > MAX_CSV_RECORDS
local lastTime = nil
local encodeContext = CSV.EncodeStart(CSV_KEYS)
for _, row in query:Iterator() do
if not shouldTrim or count <= TRIMMED_CSV_RECORDS then
-- add the save time
local saveTime = row:GetField("saveTime")
saveTime = saveTime ~= 0 and saveTime or time()
if row:GetField("source") == "Auction" then
tinsert(saveTimes, saveTime)
end
-- update the time we're trimming to
if shouldTrim then
lastTime = row:GetField("time")
end
-- add to our list of CSV lines
CSV.EncodeAddRowData(encodeContext, row)
end
count = count + 1
end
query:Release()
return CSV.EncodeEnd(encodeContext), table.concat(saveTimes, ","), lastTime
else
local saveTimes = {}
local encodeContext = CSV.EncodeStart(CSV_KEYS)
for _, _, rowRecordType, itemString, stackSize, quantity, price, otherPlayer, player, timestamp, source, saveTime in private.db:RawIterator() do
if rowRecordType == recordType then
-- add the save time
if source == "Auction" then
tinsert(saveTimes, saveTime ~= 0 and saveTime or time())
end
-- add to our list of CSV lines
CSV.EncodeAddRowDataRaw(encodeContext, itemString, stackSize, quantity, price, otherPlayer, player, timestamp, source)
end
end
return CSV.EncodeEnd(encodeContext), table.concat(saveTimes, ","), nil
end
end
function private.InsertRecord(recordType, itemString, source, stackSize, price, otherPlayer, timestamp)
private.dataChanged = true
assert(itemString and source and stackSize and price and otherPlayer and timestamp)
timestamp = floor(timestamp)
local baseItemString = ItemString.GetBase(itemString)
local player = UnitName("player")
local matchingRow = private.db:NewQuery()
:Equal("type", recordType)
:Equal("itemString", itemString)
:Equal("baseItemString", baseItemString)
:Equal("stackSize", stackSize)
:Equal("source", source)
:Equal("price", price)
:Equal("player", player)
:Equal("otherPlayer", otherPlayer)
: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("type", recordType)
:SetField("itemString", itemString)
:SetField("baseItemString", baseItemString)
:SetField("stackSize", stackSize)
:SetField("quantity", stackSize)
:SetField("price", price)
:SetField("otherPlayer", otherPlayer)
:SetField("player", player)
:SetField("time", timestamp)
:SetField("source", source)
:SetField("saveTime", 0)
:Create()
end
if private.syncHashDayCache[player] then
private.syncHashDayCacheIsInvalid[player] = true
end
private.OnItemRecordsChanged(recordType, itemString)
TSM.Accounting.Sync.OnTransactionsChanged()
end
function private.OnItemRecordsChanged(recordType, itemString)
if recordType == "sale" then
CustomPrice.OnSourceChange("AvgSell", itemString)
CustomPrice.OnSourceChange("MaxSell", itemString)
CustomPrice.OnSourceChange("MinSell", itemString)
CustomPrice.OnSourceChange("NumExpires", itemString)
elseif recordType == "buy" then
CustomPrice.OnSourceChange("AvgBuy", itemString)
CustomPrice.OnSourceChange("MaxBuy", itemString)
CustomPrice.OnSourceChange("MinBuy", itemString)
else
error("Invalid recordType: "..tostring(recordType))
end
end
function private.SyncHashesThread(otherPlayer)
private.CalculateSyncHashesThreaded(UnitName("player"))
while #private.pendingSyncHashCharacters > 0 do
local player = tremove(private.pendingSyncHashCharacters, 1)
private.CalculateSyncHashesThreaded(player)
end
private.isSyncHashesThreadRunning = false
end
function private.CalculateSyncHashesThreaded(player)
if private.syncHashDayCache[player] and not private.syncHashDayCacheIsInvalid[player] then
Log.Info("Sync hashes for player (%s) are already up to date", player)
return
end
private.syncHashDayCache[player] = private.syncHashDayCache[player] or {}
local result = private.syncHashDayCache[player]
wipe(result)
private.syncHashDayCacheIsInvalid[player] = true
while true do
local aborted = false
local query = private.db:NewQuery()
:Equal("player", player)
:OrderBy("time", false)
:OrderBy("itemString", true)
Threading.GuardDatabaseQuery(query)
for _, row in query:Iterator(true) do
local rowHash = row:CalculateHash(SYNC_FIELDS)
local day = floor(row:GetField("time") / SECONDS_PER_DAY)
result[day] = Math.CalculateHash(rowHash, result[day])
Threading.Yield()
if query:IsIteratorAborted() then
Log.Warn("Iterator was aborted for player (%s), will retry", player)
aborted = true
end
end
Threading.UnguardDatabaseQuery(query)
query:Release()
if not aborted then
break
end
end
private.syncHashDayCacheIsInvalid[player] = nil
Log.Info("Updated sync hashes for player (%s)", player)
end
function private.InventoryCallback()
CustomPrice.OnSourceChange("SmartAvgBuy")
end