initial commit
This commit is contained in:
235
Core/Service/Accounting/Auctions.lua
Normal file
235
Core/Service/Accounting/Auctions.lua
Normal file
@@ -0,0 +1,235 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- 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
|
||||
54
Core/Service/Accounting/Core.lua
Normal file
54
Core/Service/Accounting/Core.lua
Normal file
@@ -0,0 +1,54 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Accounting = TSM:NewPackage("Accounting")
|
||||
local Math = TSM.Include("Util.Math")
|
||||
local private = {
|
||||
characterGuildTemp = {},
|
||||
}
|
||||
local SECONDS_PER_DAY = 24 * 60 * 60
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Accounting.GetSummaryQuery(timeFilterStart, timeFilterEnd, ignoredCharacters)
|
||||
local query = TSM.Accounting.Transactions.CreateQuery()
|
||||
:Select("type", "itemString", "price", "quantity", "time")
|
||||
if timeFilterStart then
|
||||
query:GreaterThan("time", timeFilterStart)
|
||||
end
|
||||
if timeFilterEnd then
|
||||
query:LessThan("time", timeFilterEnd)
|
||||
end
|
||||
if ignoredCharacters then
|
||||
wipe(private.characterGuildTemp)
|
||||
for characterGuild in pairs(ignoredCharacters) do
|
||||
local character, realm = strmatch(characterGuild, "^(.+) %- .+ %- (.+)$")
|
||||
if character and realm == GetRealmName() then
|
||||
private.characterGuildTemp[character] = true
|
||||
end
|
||||
end
|
||||
query:NotInTable("player", private.characterGuildTemp)
|
||||
end
|
||||
return query
|
||||
end
|
||||
|
||||
function Accounting.GetSaleRate(itemString)
|
||||
-- since auction data only goes back 180 days, limit the sales to that same time range
|
||||
local _, totalSaleNum = TSM.Accounting.Transactions.GetSaleStats(itemString, 180 * SECONDS_PER_DAY)
|
||||
if not totalSaleNum then
|
||||
return nil
|
||||
end
|
||||
local _, _, totalFailed = TSM.Accounting.Auctions.GetStats(itemString)
|
||||
if not totalFailed then
|
||||
return nil
|
||||
end
|
||||
return Math.Round(totalSaleNum / (totalSaleNum + totalFailed), 0.01)
|
||||
end
|
||||
56
Core/Service/Accounting/Garrison.lua
Normal file
56
Core/Service/Accounting/Garrison.lua
Normal file
@@ -0,0 +1,56 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Garrison = TSM.Accounting:NewPackage("Garrison")
|
||||
local Event = TSM.Include("Util.Event")
|
||||
local private = {}
|
||||
local GOLD_TRAIT_ID = 256 -- traitId for the treasure hunter trait which increases gold from missions
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Garrison.OnInitialize()
|
||||
if not TSM.IsWowClassic() then
|
||||
Event.Register("GARRISON_MISSION_COMPLETE_RESPONSE", private.MissionComplete)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Misson Reward Tracking
|
||||
-- ============================================================================
|
||||
|
||||
function private.MissionComplete(_, missionId)
|
||||
local moneyAward = 0
|
||||
local info = C_Garrison.GetBasicMissionInfo(missionId)
|
||||
if not info then
|
||||
return
|
||||
end
|
||||
local rewards = info.rewards or info.overMaxRewards
|
||||
for _, reward in pairs(rewards) do
|
||||
if reward.title == GARRISON_REWARD_MONEY and reward.currencyID == 0 then
|
||||
moneyAward = moneyAward + reward.quantity
|
||||
end
|
||||
end
|
||||
if moneyAward > 0 then
|
||||
-- check for followers which give bonus gold
|
||||
local multiplier = 1
|
||||
for _, followerId in ipairs(info.followers) do
|
||||
for _, trait in ipairs(C_Garrison.GetFollowerAbilities(followerId)) do
|
||||
if trait.id == GOLD_TRAIT_ID then
|
||||
multiplier = multiplier + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
moneyAward = moneyAward * multiplier
|
||||
TSM.Accounting.Money.InsertGarrisonIncome(moneyAward)
|
||||
end
|
||||
end
|
||||
291
Core/Service/Accounting/GoldTracker.lua
Normal file
291
Core/Service/Accounting/GoldTracker.lua
Normal file
@@ -0,0 +1,291 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local GoldTracker = TSM.Accounting:NewPackage("GoldTracker")
|
||||
local Event = TSM.Include("Util.Event")
|
||||
local Delay = TSM.Include("Util.Delay")
|
||||
local CSV = TSM.Include("Util.CSV")
|
||||
local Math = TSM.Include("Util.Math")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local Table = TSM.Include("Util.Table")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Settings = TSM.Include("Service.Settings")
|
||||
local PlayerInfo = TSM.Include("Service.PlayerInfo")
|
||||
local private = {
|
||||
truncateGoldLog = {},
|
||||
characterGoldLog = {},
|
||||
guildGoldLog = {},
|
||||
currentCharacterKey = nil,
|
||||
playerLogCount = 0,
|
||||
searchValueTemp = {},
|
||||
}
|
||||
local CSV_KEYS = { "minute", "copper" }
|
||||
local CHARACTER_KEY_SEP = " - "
|
||||
local SECONDS_PER_MIN = 60
|
||||
local SECONDS_PER_DAY = SECONDS_PER_MIN * 60 * 24
|
||||
local MAX_COPPER_VALUE = 10 * 1000 * 1000 * COPPER_PER_GOLD - 1
|
||||
local ERRONEOUS_ZERO_THRESHOLD = 5 * 1000 * COPPER_PER_GOLD
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function GoldTracker.OnInitialize()
|
||||
if not TSM.IsWowClassic() then
|
||||
Event.Register("GUILDBANKFRAME_OPENED", private.GuildLogGold)
|
||||
Event.Register("GUILDBANK_UPDATE_MONEY", private.GuildLogGold)
|
||||
end
|
||||
Event.Register("PLAYER_MONEY", private.PlayerLogGold)
|
||||
|
||||
-- get a list of known characters / guilds
|
||||
local validCharacterGuilds = TempTable.Acquire()
|
||||
for _, character in Settings.CharacterByFactionrealmIterator() do
|
||||
validCharacterGuilds[character..CHARACTER_KEY_SEP..UnitFactionGroup("player")..CHARACTER_KEY_SEP..GetRealmName()] = true
|
||||
local guild = TSM.db.factionrealm.internalData.characterGuilds[character]
|
||||
if guild then
|
||||
validCharacterGuilds[guild] = true
|
||||
end
|
||||
end
|
||||
|
||||
-- load the gold log data
|
||||
for realm in TSM.db:GetConnectedRealmIterator("realm") do
|
||||
for factionrealm in TSM.db:FactionrealmByRealmIterator(realm) do
|
||||
for _, character in TSM.db:FactionrealmCharacterIterator(factionrealm) do
|
||||
local data = TSM.db:Get("sync", TSM.db:GetSyncScopeKeyByCharacter(character, factionrealm), "internalData", "goldLog")
|
||||
if data then
|
||||
local lastUpdate = TSM.db:Get("sync", TSM.db:GetSyncScopeKeyByCharacter(character, factionrealm), "internalData", "goldLogLastUpdate") or 0
|
||||
local characterKey = character..CHARACTER_KEY_SEP..factionrealm
|
||||
private.LoadCharacterGoldLog(characterKey, data, validCharacterGuilds, lastUpdate)
|
||||
end
|
||||
end
|
||||
local guildData = TSM.db:Get("factionrealm", factionrealm, "internalData", "guildGoldLog")
|
||||
if guildData then
|
||||
for guild, data in pairs(guildData) do
|
||||
local entries = {}
|
||||
local decodeContext = CSV.DecodeStart(data, CSV_KEYS)
|
||||
if decodeContext then
|
||||
for minute, copper in CSV.DecodeIterator(decodeContext) do
|
||||
tinsert(entries, { minute = tonumber(minute), copper = tonumber(copper) })
|
||||
end
|
||||
CSV.DecodeEnd(decodeContext)
|
||||
end
|
||||
private.guildGoldLog[guild] = entries
|
||||
local lastEntryTime = #entries > 0 and entries[#entries].minute * SECONDS_PER_MIN or math.huge
|
||||
local lastUpdate = TSM.db:Get("factionrealm", factionrealm, "internalData", "guildGoldLogLastUpdate")
|
||||
if not validCharacterGuilds[guild] and max(lastEntryTime, lastUpdate and lastUpdate[guild] or 0) < time() - 30 * SECONDS_PER_DAY then
|
||||
-- this guild may not be valid and the last entry is over 30 days old, so truncate the data
|
||||
private.truncateGoldLog[guild] = lastEntryTime
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
TempTable.Release(validCharacterGuilds)
|
||||
private.currentCharacterKey = UnitName("player")..CHARACTER_KEY_SEP..UnitFactionGroup("player")..CHARACTER_KEY_SEP..GetRealmName()
|
||||
assert(private.characterGoldLog[private.currentCharacterKey])
|
||||
end
|
||||
|
||||
function GoldTracker.OnEnable()
|
||||
-- Log the current player gold (need to wait for OnEnable, otherwise GetMoney() returns 0 when first logging in)
|
||||
private.PlayerLogGold()
|
||||
end
|
||||
|
||||
function GoldTracker.OnDisable()
|
||||
private.PlayerLogGold()
|
||||
TSM.db.sync.internalData.goldLog = CSV.Encode(CSV_KEYS, private.characterGoldLog[private.currentCharacterKey])
|
||||
TSM.db.sync.internalData.goldLogLastUpdate = private.characterGoldLog[private.currentCharacterKey].lastUpdate
|
||||
local guild = PlayerInfo.GetPlayerGuild(UnitName("player"))
|
||||
if guild and private.guildGoldLog[guild] then
|
||||
TSM.db.factionrealm.internalData.guildGoldLog[guild] = CSV.Encode(CSV_KEYS, private.guildGoldLog[guild])
|
||||
TSM.db.factionrealm.internalData.guildGoldLogLastUpdate[guild] = private.guildGoldLog[guild].lastUpdate
|
||||
end
|
||||
end
|
||||
|
||||
function GoldTracker.CharacterGuildIterator()
|
||||
return private.CharacterGuildIteratorHelper
|
||||
end
|
||||
|
||||
function GoldTracker.GetGoldAtTime(timestamp, ignoredCharactersGuilds)
|
||||
local value = 0
|
||||
for character, logEntries in pairs(private.characterGoldLog) do
|
||||
if #logEntries > 0 and not ignoredCharactersGuilds[character] and (private.truncateGoldLog[character] or math.huge) > timestamp then
|
||||
value = value + private.GetValueAtTime(logEntries, timestamp)
|
||||
end
|
||||
end
|
||||
for guild, logEntries in pairs(private.guildGoldLog) do
|
||||
if #logEntries > 0 and not ignoredCharactersGuilds[guild] and (private.truncateGoldLog[guild] or math.huge) > timestamp then
|
||||
value = value + private.GetValueAtTime(logEntries, timestamp)
|
||||
end
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
function GoldTracker.GetGraphTimeRange(ignoredCharactersGuilds)
|
||||
local minTime = Math.Floor(time(), SECONDS_PER_MIN)
|
||||
for character, logEntries in pairs(private.characterGoldLog) do
|
||||
if #logEntries > 0 and not ignoredCharactersGuilds[character] then
|
||||
minTime = min(minTime, logEntries[1].minute * SECONDS_PER_MIN)
|
||||
end
|
||||
end
|
||||
for guild, logEntries in pairs(private.guildGoldLog) do
|
||||
if #logEntries > 0 and not ignoredCharactersGuilds[guild] then
|
||||
minTime = min(minTime, logEntries[1].minute * SECONDS_PER_MIN)
|
||||
end
|
||||
end
|
||||
return minTime, Math.Floor(time(), SECONDS_PER_MIN), SECONDS_PER_MIN
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.LoadCharacterGoldLog(characterKey, data, validCharacterGuilds, lastUpdate)
|
||||
assert(not private.characterGoldLog[characterKey])
|
||||
local decodeContext = CSV.DecodeStart(data, CSV_KEYS)
|
||||
if not decodeContext then
|
||||
Log.Err("Failed to decode (%s, %d)", characterKey, #data)
|
||||
private.characterGoldLog[characterKey] = {}
|
||||
return
|
||||
end
|
||||
|
||||
local entries = {}
|
||||
for minute, copper in CSV.DecodeIterator(decodeContext) do
|
||||
tinsert(entries, { minute = tonumber(minute), copper = tonumber(copper) })
|
||||
end
|
||||
CSV.DecodeEnd(decodeContext)
|
||||
|
||||
-- clean up any erroneous 0 entries, entries which are too high, and duplicate entries
|
||||
local didChange = true
|
||||
while didChange do
|
||||
didChange = false
|
||||
for i = #entries - 1, 2, -1 do
|
||||
local prevValue = entries[i-1].copper
|
||||
local value = entries[i].copper
|
||||
local nextValue = entries[i+1].copper
|
||||
if prevValue > ERRONEOUS_ZERO_THRESHOLD and value == 0 and nextValue > ERRONEOUS_ZERO_THRESHOLD then
|
||||
-- this is likely an erroneous 0 value
|
||||
didChange = true
|
||||
tremove(entries, i)
|
||||
end
|
||||
end
|
||||
for i = #entries, 2, -1 do
|
||||
local prevValue = entries[i-1].copper
|
||||
local value = entries[i].copper
|
||||
if prevValue == value or value > MAX_COPPER_VALUE then
|
||||
-- this is either a duplicate or invalid value
|
||||
didChange = true
|
||||
tremove(entries, i)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private.characterGoldLog[characterKey] = entries
|
||||
local lastEntryTime = #entries > 0 and entries[#entries].minute * SECONDS_PER_MIN or math.huge
|
||||
if not validCharacterGuilds[characterKey] and max(lastEntryTime, lastUpdate) < time() - 30 * SECONDS_PER_DAY then
|
||||
-- this character may not be valid and the last entry is over 30 days old, so truncate the data
|
||||
private.truncateGoldLog[characterKey] = lastEntryTime
|
||||
end
|
||||
end
|
||||
|
||||
function private.UpdateGoldLog(goldLog, copper)
|
||||
copper = Math.Round(copper, COPPER_PER_GOLD * (TSM.IsWowClassic() and 1 or 1000))
|
||||
local currentMinute = floor(time() / SECONDS_PER_MIN)
|
||||
local prevRecord = goldLog[#goldLog]
|
||||
|
||||
-- store the last update time
|
||||
goldLog.lastUpdate = time()
|
||||
|
||||
if prevRecord and copper == prevRecord.copper then
|
||||
-- amount of gold hasn't changed, so nothing to do
|
||||
return
|
||||
elseif prevRecord and prevRecord.minute == currentMinute then
|
||||
-- gold has changed and the previous record is for the current minute so just modify it
|
||||
prevRecord.copper = copper
|
||||
else
|
||||
-- amount of gold changed and we're in a new minute, so insert a new record
|
||||
while prevRecord and prevRecord.minute > currentMinute - 1 do
|
||||
-- their clock may have changed - just delete everything that's too recent
|
||||
tremove(goldLog)
|
||||
prevRecord = goldLog[#goldLog]
|
||||
end
|
||||
tinsert(goldLog, {
|
||||
minute = currentMinute,
|
||||
copper = copper
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
function private.GuildLogGold()
|
||||
local guildName = GetGuildInfo("player")
|
||||
local isGuildLeader = IsGuildLeader()
|
||||
if guildName and not isGuildLeader then
|
||||
-- check if our alt is the guild leader
|
||||
for i = 1, GetNumGuildMembers() do
|
||||
local name, _, rankIndex = GetGuildRosterInfo(i)
|
||||
if name and rankIndex == 0 and PlayerInfo.IsPlayer(gsub(name, "%-", " - "), true) then
|
||||
isGuildLeader = true
|
||||
end
|
||||
end
|
||||
end
|
||||
if guildName and isGuildLeader then
|
||||
if not private.guildGoldLog[guildName] then
|
||||
private.guildGoldLog[guildName] = {}
|
||||
end
|
||||
private.UpdateGoldLog(private.guildGoldLog[guildName], GetGuildBankMoney())
|
||||
end
|
||||
end
|
||||
|
||||
function private.PlayerLogGold()
|
||||
-- GetMoney sometimes returns 0 for a while after login, so keep trying for 30 seconds before recording a 0
|
||||
local money = GetMoney()
|
||||
if money == 0 and private.playerLogCount < 30 then
|
||||
private.playerLogCount = private.playerLogCount + 1
|
||||
Delay.AfterTime(1, private.PlayerLogGold)
|
||||
return
|
||||
end
|
||||
private.playerLogCount = 0
|
||||
private.UpdateGoldLog(private.characterGoldLog[private.currentCharacterKey], money)
|
||||
TSM.db.sync.internalData.money = money
|
||||
end
|
||||
|
||||
function private.GetValueAtTime(logEntries, timestamp)
|
||||
local minute = floor(timestamp / SECONDS_PER_MIN)
|
||||
if logEntries[1].minute > minute then
|
||||
-- timestamp is before we had any data
|
||||
return 0
|
||||
end
|
||||
private.searchValueTemp.minute = minute
|
||||
local index, insertIndex = Table.BinarySearch(logEntries, private.searchValueTemp, private.GetEntryMinute)
|
||||
-- if we didn't find an exact match, the index is the previous one (compared to the insert index)
|
||||
-- as that point's gold value is true up until the next point
|
||||
index = index or (insertIndex - 1)
|
||||
return logEntries[index].copper
|
||||
end
|
||||
|
||||
function private.GetEntryMinute(entry)
|
||||
return entry.minute
|
||||
end
|
||||
|
||||
function private.CharacterGuildIteratorHelper(_, lastKey)
|
||||
local result, isGuild = nil, nil
|
||||
if not lastKey or private.characterGoldLog[lastKey] then
|
||||
result = next(private.characterGoldLog, lastKey)
|
||||
isGuild = false
|
||||
if not result then
|
||||
lastKey = nil
|
||||
end
|
||||
end
|
||||
if not result then
|
||||
result = next(private.guildGoldLog, lastKey)
|
||||
isGuild = result and true or false
|
||||
end
|
||||
return result, isGuild
|
||||
end
|
||||
387
Core/Service/Accounting/Mail.lua
Normal file
387
Core/Service/Accounting/Mail.lua
Normal file
@@ -0,0 +1,387 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Mail = TSM.Accounting:NewPackage("Mail")
|
||||
local Event = TSM.Include("Util.Event")
|
||||
local Delay = TSM.Include("Util.Delay")
|
||||
local String = TSM.Include("Util.String")
|
||||
local ItemString = TSM.Include("Util.ItemString")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
local InventoryInfo = TSM.Include("Service.InventoryInfo")
|
||||
local AuctionTracking = TSM.Include("Service.AuctionTracking")
|
||||
local Inventory = TSM.Include("Service.Inventory")
|
||||
local private = {
|
||||
hooks = {},
|
||||
}
|
||||
local SECONDS_PER_DAY = 24 * 60 * 60
|
||||
local EXPIRED_MATCH_TEXT = AUCTION_EXPIRED_MAIL_SUBJECT:gsub("%%s", "")
|
||||
local CANCELLED_MATCH_TEXT = AUCTION_REMOVED_MAIL_SUBJECT:gsub("%%s", "")
|
||||
local OUTBID_MATCH_TEXT = AUCTION_OUTBID_MAIL_SUBJECT:gsub("%%s", "(.+)")
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Mail.OnInitialize()
|
||||
Event.Register("MAIL_SHOW", function() Delay.AfterTime("ACCOUNTING_GET_SELLERS", 0.1, private.RequestSellerInfo, 0.1) end)
|
||||
Event.Register("MAIL_CLOSED", function() Delay.Cancel("ACCOUNTING_GET_SELLERS") end)
|
||||
-- hook certain mail functions
|
||||
private.hooks.TakeInboxItem = TakeInboxItem
|
||||
TakeInboxItem = function(...)
|
||||
Mail:ScanCollectedMail("TakeInboxItem", 1, ...)
|
||||
end
|
||||
private.hooks.TakeInboxMoney = TakeInboxMoney
|
||||
TakeInboxMoney = function(...)
|
||||
Mail:ScanCollectedMail("TakeInboxMoney", 1, ...)
|
||||
end
|
||||
private.hooks.AutoLootMailItem = AutoLootMailItem
|
||||
AutoLootMailItem = function(...)
|
||||
Mail:ScanCollectedMail("AutoLootMailItem", 1, ...)
|
||||
end
|
||||
private.hooks.SendMail = SendMail
|
||||
SendMail = private.CheckSendMail
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Inbox Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.RequestSellerInfo()
|
||||
local isDone = true
|
||||
for i = 1, GetInboxNumItems() do
|
||||
local invoiceType, _, seller = GetInboxInvoiceInfo(i)
|
||||
if invoiceType and seller == "" then
|
||||
isDone = false
|
||||
end
|
||||
end
|
||||
if isDone and GetInboxNumItems() > 0 then
|
||||
Delay.Cancel("ACCOUNTING_GET_SELLERS")
|
||||
end
|
||||
end
|
||||
|
||||
function private.CanLootMailIndex(index, copper)
|
||||
local currentMoney = GetMoney()
|
||||
assert(currentMoney <= MAXIMUM_BID_PRICE)
|
||||
-- check if this would put them over the gold cap
|
||||
if currentMoney + copper > MAXIMUM_BID_PRICE then return end
|
||||
local _, _, _, _, _, _, _, itemCount = GetInboxHeaderInfo(index)
|
||||
if not itemCount or itemCount == 0 then return true end
|
||||
for j = 1, ATTACHMENTS_MAX_RECEIVE do
|
||||
-- TODO: prevent items that you can't loot because of internal mail error
|
||||
if CalculateTotalNumberOfFreeBagSlots() <= 0 then
|
||||
return
|
||||
end
|
||||
local link = GetInboxItemLink(index, j)
|
||||
local itemString = ItemString.Get(link)
|
||||
local _, _, _, count = GetInboxItem(index, j)
|
||||
local quantity = count or 0
|
||||
local maxUnique = private.GetInboxMaxUnique(index, j)
|
||||
-- dont record unique items that we can't loot
|
||||
local playerQty = Inventory.GetBagQuantity(itemString) + Inventory.GetBankQuantity(itemString) + Inventory.GetReagentBankQuantity(itemString)
|
||||
if maxUnique > 0 and maxUnique < playerQty + quantity then
|
||||
return
|
||||
end
|
||||
if itemString then
|
||||
for bag = 0, NUM_BAG_SLOTS do
|
||||
if InventoryInfo.ItemWillGoInBag(link, bag) then
|
||||
for slot = 1, GetContainerNumSlots(bag) do
|
||||
local iString = ItemString.Get(GetContainerItemLink(bag, slot))
|
||||
if iString == itemString then
|
||||
local _, stackSize = GetContainerItemInfo(bag, slot)
|
||||
local maxStackSize = ItemInfo.GetMaxStack(itemString) or 1
|
||||
if (maxStackSize - stackSize) >= quantity then
|
||||
return true
|
||||
end
|
||||
elseif not iString then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function private.GetInboxMaxUnique(index, num)
|
||||
if not num then
|
||||
num = 1
|
||||
end
|
||||
|
||||
if not TSMScanTooltip then
|
||||
CreateFrame("GameTooltip", "TSMScanTooltip", UIParent, "GameTooltipTemplate")
|
||||
end
|
||||
|
||||
TSMScanTooltip:SetOwner(UIParent, "ANCHOR_NONE")
|
||||
TSMScanTooltip:ClearLines()
|
||||
|
||||
local _, speciesId = TSMScanTooltip:SetInboxItem(index, num)
|
||||
if (speciesId or 0) > 0 then
|
||||
return 0
|
||||
else
|
||||
for id = 2, TSMScanTooltip:NumLines() do
|
||||
local text = private.GetTooltipText(_G["TSMScanTooltipTextLeft"..id])
|
||||
if text then
|
||||
if text == ITEM_UNIQUE then
|
||||
return 1
|
||||
else
|
||||
local match = text and strmatch(text, "^"..ITEM_UNIQUE.." %((%d+)%)$")
|
||||
if match then
|
||||
return tonumber(match)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return 0
|
||||
end
|
||||
|
||||
function private.GetTooltipText(text)
|
||||
local textStr = strtrim(text and text:GetText() or "")
|
||||
if textStr == "" then return end
|
||||
|
||||
return textStr
|
||||
end
|
||||
|
||||
-- scans the mail that the player just attempted to collected (Pre-Hook)
|
||||
function Mail:ScanCollectedMail(oFunc, attempt, index, subIndex)
|
||||
local invoiceType, itemName, buyer, bid, _, _, ahcut, _, _, _, quantity = GetInboxInvoiceInfo(index)
|
||||
buyer = buyer or (invoiceType == "buyer" and AUCTION_HOUSE_MAIL_MULTIPLE_SELLERS or AUCTION_HOUSE_MAIL_MULTIPLE_BUYERS)
|
||||
local _, stationeryIcon, sender, subject, money, codAmount, daysLeft = GetInboxHeaderInfo(index)
|
||||
if not subject then return end
|
||||
if attempt > 2 then
|
||||
if buyer == "" then
|
||||
buyer = "?"
|
||||
elseif sender == "" then
|
||||
sender = "?"
|
||||
end
|
||||
end
|
||||
|
||||
local success = false
|
||||
if invoiceType == "seller" and buyer and buyer ~= "" then -- AH Sales
|
||||
local saleTime = (time() + (daysLeft - 30) * SECONDS_PER_DAY)
|
||||
local itemString = ItemInfo.ItemNameToItemString(itemName)
|
||||
if not itemString or itemString == ItemString.GetUnknown() then
|
||||
itemString = AuctionTracking.GetSaleHintItemString(itemName, quantity, bid)
|
||||
end
|
||||
if private.CanLootMailIndex(index, (bid - ahcut)) then
|
||||
if itemString then
|
||||
local copper = floor((bid - ahcut) / quantity + 0.5)
|
||||
TSM.Accounting.Transactions.InsertAuctionSale(itemString, quantity, copper, buyer, saleTime)
|
||||
end
|
||||
success = true
|
||||
end
|
||||
elseif invoiceType == "buyer" and buyer and buyer ~= "" then -- AH Buys
|
||||
local copper = floor(bid / quantity + 0.5)
|
||||
if not TSM.IsWowClassic() then
|
||||
if subIndex then
|
||||
quantity = select(4, GetInboxItem(index, subIndex))
|
||||
else
|
||||
quantity = 0
|
||||
for i = 1, ATTACHMENTS_MAX do
|
||||
quantity = quantity + (select(4, GetInboxItem(index, i)) or 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
local link = (subIndex or 1) == 1 and private.GetFirstInboxItemLink(index) or GetInboxItemLink(index, subIndex or 1)
|
||||
local itemString = ItemString.Get(link)
|
||||
if itemString and private.CanLootMailIndex(index, 0) then
|
||||
local buyTime = (time() + (daysLeft - 30) * SECONDS_PER_DAY)
|
||||
TSM.Accounting.Transactions.InsertAuctionBuy(itemString, quantity, copper, buyer, buyTime)
|
||||
success = true
|
||||
end
|
||||
elseif codAmount > 0 then -- COD Buys (only if all attachments are same item)
|
||||
local link = (subIndex or 1) == 1 and private.GetFirstInboxItemLink(index) or GetInboxItemLink(index, subIndex or 1)
|
||||
local itemString = ItemString.Get(link)
|
||||
if itemString and sender then
|
||||
local name = ItemInfo.GetName(link)
|
||||
local total = 0
|
||||
local stacks = 0
|
||||
local ignore = false
|
||||
for i = 1, ATTACHMENTS_MAX_RECEIVE do
|
||||
local nameCheck, _, _, count = GetInboxItem(index, i)
|
||||
if nameCheck and count then
|
||||
if nameCheck == name then
|
||||
total = total + count
|
||||
stacks = stacks + 1
|
||||
else
|
||||
ignore = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if total ~= 0 and not ignore and private.CanLootMailIndex(index, codAmount) then
|
||||
local copper = floor(codAmount / total + 0.5)
|
||||
local buyTime = (time() + (daysLeft - 3) * SECONDS_PER_DAY)
|
||||
local maxStack = ItemInfo.GetMaxStack(link)
|
||||
for _ = 1, stacks do
|
||||
local stackSize = (total >= maxStack) and maxStack or total
|
||||
TSM.Accounting.Transactions.InsertCODBuy(itemString, stackSize, copper, sender, buyTime)
|
||||
total = total - stackSize
|
||||
if total <= 0 then
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
success = true
|
||||
end
|
||||
elseif money > 0 and invoiceType ~= "seller" and not strfind(subject, OUTBID_MATCH_TEXT) then
|
||||
local str = nil
|
||||
if GetLocale() == "deDE" then
|
||||
str = gsub(subject, gsub(COD_PAYMENT, String.Escape("%1$s"), ""), "")
|
||||
else
|
||||
str = gsub(subject, gsub(COD_PAYMENT, String.Escape("%s"), ""), "")
|
||||
end
|
||||
local saleTime = (time() + (daysLeft - 31) * SECONDS_PER_DAY)
|
||||
if sender and private.CanLootMailIndex(index, money) then
|
||||
if str and strfind(str, "TSM$") then -- payment for a COD the player sent
|
||||
local codName = strtrim(strmatch(str, "([^%(]+)"))
|
||||
local qty = strmatch(str, "%(([0-9]+)%)")
|
||||
qty = tonumber(qty)
|
||||
local itemString = ItemInfo.ItemNameToItemString(codName)
|
||||
if itemString then
|
||||
local copper = floor(money / qty + 0.5)
|
||||
local maxStack = ItemInfo.GetMaxStack(itemString) or 1
|
||||
local stacks = ceil(qty / maxStack)
|
||||
|
||||
for _ = 1, stacks do
|
||||
local stackSize = (qty >= maxStack) and maxStack or qty
|
||||
TSM.Accounting.Transactions.InsertCODSale(itemString, stackSize, copper, sender, saleTime)
|
||||
qty = qty - stackSize
|
||||
if qty <= 0 then
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
else -- record a money transfer
|
||||
TSM.Accounting.Money.InsertMoneyTransferIncome(money, sender, saleTime)
|
||||
end
|
||||
success = true
|
||||
end
|
||||
elseif strfind(subject, EXPIRED_MATCH_TEXT) then -- expired auction
|
||||
local expiredTime = (time() + (daysLeft - 30) * SECONDS_PER_DAY)
|
||||
local link = (subIndex or 1) == 1 and private.GetFirstInboxItemLink(index) or GetInboxItemLink(index, subIndex or 1)
|
||||
local _, _, _, count = GetInboxItem(index, subIndex or 1)
|
||||
if TSM.IsWowClassic() then
|
||||
quantity = count or 0
|
||||
else
|
||||
if subIndex then
|
||||
quantity = select(4, GetInboxItem(index, subIndex))
|
||||
else
|
||||
quantity = 0
|
||||
for i = 1, ATTACHMENTS_MAX do
|
||||
quantity = quantity + (select(4, GetInboxItem(index, i)) or 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
local itemString = ItemString.Get(link)
|
||||
if private.CanLootMailIndex(index, 0) and itemString and quantity then
|
||||
TSM.Accounting.Auctions.InsertExpire(itemString, quantity, expiredTime)
|
||||
success = true
|
||||
end
|
||||
elseif strfind(subject, CANCELLED_MATCH_TEXT) then -- cancelled auction
|
||||
local cancelledTime = (time() + (daysLeft - 30) * SECONDS_PER_DAY)
|
||||
local link = (subIndex or 1) == 1 and private.GetFirstInboxItemLink(index) or GetInboxItemLink(index, subIndex or 1)
|
||||
local _, _, _, count = GetInboxItem(index, subIndex or 1)
|
||||
if TSM.IsWowClassic() then
|
||||
quantity = count or 0
|
||||
else
|
||||
if subIndex then
|
||||
quantity = select(4, GetInboxItem(index, subIndex))
|
||||
else
|
||||
quantity = 0
|
||||
for i = 1, ATTACHMENTS_MAX do
|
||||
quantity = quantity + (select(4, GetInboxItem(index, i)) or 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
local itemString = ItemString.Get(link)
|
||||
if private.CanLootMailIndex(index, 0) and itemString and quantity then
|
||||
TSM.Accounting.Auctions.InsertCancel(itemString, quantity, cancelledTime)
|
||||
success = true
|
||||
end
|
||||
end
|
||||
|
||||
if success then
|
||||
private.hooks[oFunc](index, subIndex)
|
||||
elseif (not stationeryIcon or (invoiceType and (not buyer or buyer == ""))) and attempt <= 5 then
|
||||
Delay.AfterTime("accountingHookDelay", 0.2, function() Mail:ScanCollectedMail(oFunc, attempt + 1, index, subIndex) end)
|
||||
elseif attempt > 5 then
|
||||
private.hooks[oFunc](index, subIndex)
|
||||
else
|
||||
private.hooks[oFunc](index, subIndex)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Sending Functions
|
||||
-- ============================================================================
|
||||
|
||||
-- scans the mail that the player just attempted to send (Pre-Hook) to see if COD
|
||||
function private.CheckSendMail(destination, currentSubject, ...)
|
||||
local codAmount = GetSendMailCOD()
|
||||
local moneyAmount = GetSendMailMoney()
|
||||
local mailCost = GetSendMailPrice()
|
||||
local subject
|
||||
local total = 0
|
||||
local ignore = false
|
||||
|
||||
if codAmount ~= 0 then
|
||||
for i = 1, 12 do
|
||||
local itemName, _, _, count = GetSendMailItem(i)
|
||||
if itemName and count then
|
||||
if not subject then
|
||||
subject = itemName
|
||||
end
|
||||
|
||||
if subject == itemName then
|
||||
total = total + count
|
||||
else
|
||||
ignore = true
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
ignore = true
|
||||
end
|
||||
|
||||
if moneyAmount > 0 then
|
||||
-- add a record for the money transfer
|
||||
TSM.Accounting.Money.InsertMoneyTransferExpense(moneyAmount, destination)
|
||||
mailCost = mailCost - moneyAmount
|
||||
end
|
||||
TSM.Accounting.Money.InsertPostageExpense(mailCost, destination)
|
||||
|
||||
if not ignore then
|
||||
private.hooks.SendMail(destination, subject .. " (" .. total .. ") TSM", ...)
|
||||
else
|
||||
private.hooks.SendMail(destination, currentSubject, ...)
|
||||
end
|
||||
end
|
||||
|
||||
function private.GetFirstInboxItemLink(index)
|
||||
if not TSMAccountingMailTooltip then
|
||||
CreateFrame("GameTooltip", "TSMAccountingMailTooltip", UIParent, "GameTooltipTemplate")
|
||||
end
|
||||
TSMAccountingMailTooltip:SetOwner(UIParent, "ANCHOR_NONE")
|
||||
TSMAccountingMailTooltip:ClearLines()
|
||||
local _, speciesId, level, breedQuality, maxHealth, power, speed = TSMAccountingMailTooltip:SetInboxItem(index)
|
||||
local link = nil
|
||||
if (speciesId or 0) > 0 then
|
||||
link = ItemInfo.GetLink(strjoin(":", "p", speciesId, level, breedQuality, maxHealth, power, speed))
|
||||
else
|
||||
link = GetInboxItemLink(index, 1)
|
||||
end
|
||||
TSMAccountingMailTooltip:Hide()
|
||||
return link
|
||||
end
|
||||
133
Core/Service/Accounting/Merchant.lua
Normal file
133
Core/Service/Accounting/Merchant.lua
Normal file
@@ -0,0 +1,133 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Merchant = TSM.Accounting:NewPackage("Merchant")
|
||||
local Event = TSM.Include("Util.Event")
|
||||
local Math = TSM.Include("Util.Math")
|
||||
local ItemString = TSM.Include("Util.ItemString")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
local private = {
|
||||
repairMoney = 0,
|
||||
couldRepair = nil,
|
||||
repairCost = 0,
|
||||
pendingSales = {
|
||||
itemString = {},
|
||||
quantity = {},
|
||||
copper = {},
|
||||
insertTime = {},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Merchant.OnInitialize()
|
||||
Event.Register("MERCHANT_SHOW", private.SetupRepairCost)
|
||||
Event.Register("BAG_UPDATE_DELAYED", private.OnMerchantUpdate)
|
||||
Event.Register("UPDATE_INVENTORY_DURABILITY", private.AddRepairCosts)
|
||||
Event.Register("MERCHANT_CLOSED", private.OnMerchantClosed)
|
||||
hooksecurefunc("UseContainerItem", private.CheckMerchantSale)
|
||||
hooksecurefunc("BuyMerchantItem", private.OnMerchantBuy)
|
||||
hooksecurefunc("BuybackItem", private.OnMerchantBuyback)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Repair Cost Tracking
|
||||
-- ============================================================================
|
||||
|
||||
function private.SetupRepairCost()
|
||||
private.repairMoney = GetMoney()
|
||||
private.couldRepair = CanMerchantRepair()
|
||||
-- if merchant can repair set up variables so we can track repairs
|
||||
if private.couldRepair then
|
||||
private.repairCost = GetRepairAllCost()
|
||||
end
|
||||
end
|
||||
|
||||
function private.OnMerchantUpdate()
|
||||
-- Could have bought something before or after repair
|
||||
private.repairMoney = GetMoney()
|
||||
-- log any pending sales
|
||||
for i, insertTime in ipairs(private.pendingSales.insertTime) do
|
||||
if GetTime() - insertTime < 5 then
|
||||
TSM.Accounting.Transactions.InsertVendorSale(private.pendingSales.itemString[i], private.pendingSales.quantity[i], private.pendingSales.copper[i])
|
||||
end
|
||||
end
|
||||
wipe(private.pendingSales.itemString)
|
||||
wipe(private.pendingSales.quantity)
|
||||
wipe(private.pendingSales.copper)
|
||||
wipe(private.pendingSales.insertTime)
|
||||
end
|
||||
|
||||
function private.AddRepairCosts()
|
||||
if private.couldRepair and private.repairCost > 0 then
|
||||
local cash = GetMoney()
|
||||
if private.repairMoney > cash then
|
||||
-- this is probably a repair bill
|
||||
local cost = private.repairMoney - cash
|
||||
TSM.Accounting.Money.InsertRepairBillExpense(cost)
|
||||
-- reset money as this might have been a single item repair
|
||||
private.repairMoney = cash
|
||||
-- reset the repair cost for the next repair
|
||||
private.repairCost = GetRepairAllCost()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function private.OnMerchantClosed()
|
||||
private.couldRepair = nil
|
||||
private.repairCost = 0
|
||||
end
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Merchant Purchases / Sales Tracking
|
||||
-- ============================================================================
|
||||
|
||||
function private.CheckMerchantSale(bag, slot, onSelf)
|
||||
-- check if we are trying to sell something to a vendor
|
||||
if (not MerchantFrame:IsShown() and not TSM.UI.VendoringUI.IsVisible()) or onSelf then
|
||||
return
|
||||
end
|
||||
|
||||
local itemString = ItemString.Get(GetContainerItemLink(bag, slot))
|
||||
local _, quantity = GetContainerItemInfo(bag, slot)
|
||||
local copper = ItemInfo.GetVendorSell(itemString)
|
||||
if not itemString or not quantity or not copper then
|
||||
return
|
||||
end
|
||||
tinsert(private.pendingSales.itemString, itemString)
|
||||
tinsert(private.pendingSales.quantity, quantity)
|
||||
tinsert(private.pendingSales.copper, copper)
|
||||
tinsert(private.pendingSales.insertTime, GetTime())
|
||||
end
|
||||
|
||||
function private.OnMerchantBuy(index, quantity)
|
||||
local _, _, price, batchQuantity = GetMerchantItemInfo(index)
|
||||
local itemString = ItemString.Get(GetMerchantItemLink(index))
|
||||
if not itemString or not price or price <= 0 then
|
||||
return
|
||||
end
|
||||
quantity = quantity or batchQuantity
|
||||
local copper = Math.Round(price / batchQuantity)
|
||||
TSM.Accounting.Transactions.InsertVendorBuy(itemString, quantity, copper)
|
||||
end
|
||||
|
||||
function private.OnMerchantBuyback(index)
|
||||
local _, _, price, quantity = GetBuybackItemInfo(index)
|
||||
local itemString = ItemString.Get(GetBuybackItemLink(index))
|
||||
if not itemString or not price or price <= 0 then
|
||||
return
|
||||
end
|
||||
local copper = Math.Round(price / quantity)
|
||||
TSM.Accounting.Transactions.InsertVendorBuy(itemString, quantity, copper)
|
||||
end
|
||||
171
Core/Service/Accounting/Money.lua
Normal file
171
Core/Service/Accounting/Money.lua
Normal file
@@ -0,0 +1,171 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Money = TSM.Accounting:NewPackage("Money")
|
||||
local Database = TSM.Include("Util.Database")
|
||||
local CSV = TSM.Include("Util.CSV")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local private = {
|
||||
db = nil,
|
||||
dataChanged = false,
|
||||
}
|
||||
local CSV_KEYS = { "type", "amount", "otherPlayer", "player", "time" }
|
||||
local COMBINE_TIME_THRESHOLD = 300 -- group expenses within 5 minutes together
|
||||
local SECONDS_PER_DAY = 24 * 60 * 60
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Money.OnInitialize()
|
||||
private.db = Database.NewSchema("ACCOUNTING_MONEY")
|
||||
:AddStringField("recordType")
|
||||
:AddStringField("type")
|
||||
:AddNumberField("amount")
|
||||
:AddStringField("otherPlayer")
|
||||
:AddStringField("player")
|
||||
:AddNumberField("time")
|
||||
:AddIndex("recordType")
|
||||
:Commit()
|
||||
private.db:BulkInsertStart()
|
||||
private.LoadData("expense", TSM.db.realm.internalData.csvExpense)
|
||||
private.LoadData("income", TSM.db.realm.internalData.csvIncome)
|
||||
private.db:BulkInsertEnd()
|
||||
end
|
||||
|
||||
function Money.OnDisable()
|
||||
if not private.dataChanged then
|
||||
-- nothing changed, so just keep the previous saved values
|
||||
return
|
||||
end
|
||||
TSM.db.realm.internalData.csvExpense = private.SaveData("expense")
|
||||
TSM.db.realm.internalData.csvIncome = private.SaveData("income")
|
||||
end
|
||||
|
||||
function Money.InsertMoneyTransferExpense(amount, destination)
|
||||
private.InsertRecord("expense", "Money Transfer", amount, destination, time())
|
||||
end
|
||||
|
||||
function Money.InsertPostageExpense(amount, destination)
|
||||
private.InsertRecord("expense", "Postage", amount, destination, time())
|
||||
end
|
||||
|
||||
function Money.InsertRepairBillExpense(amount)
|
||||
private.InsertRecord("expense", "Repair Bill", amount, "Merchant", time())
|
||||
end
|
||||
|
||||
function Money.InsertMoneyTransferIncome(amount, source, timestamp)
|
||||
private.InsertRecord("income", "Money Transfer", amount, source, timestamp)
|
||||
end
|
||||
|
||||
function Money.InsertGarrisonIncome(amount)
|
||||
private.InsertRecord("income", "Garrison", amount, "Mission", time())
|
||||
end
|
||||
|
||||
function Money.CreateQuery()
|
||||
return private.db:NewQuery()
|
||||
end
|
||||
|
||||
function Money.CharacterIterator(recordType)
|
||||
return private.db:NewQuery()
|
||||
:Equal("recordType", recordType)
|
||||
:Distinct("player")
|
||||
:Select("player")
|
||||
:IteratorAndRelease()
|
||||
end
|
||||
|
||||
function Money.RemoveOldData(days)
|
||||
private.dataChanged = true
|
||||
local query = private.db:NewQuery()
|
||||
:LessThan("time", time() - days * SECONDS_PER_DAY)
|
||||
local numRecords = 0
|
||||
private.db:SetQueryUpdatesPaused(true)
|
||||
for _, row in query:Iterator() do
|
||||
private.db:DeleteRow(row)
|
||||
numRecords = numRecords + 1
|
||||
end
|
||||
query:Release()
|
||||
private.db:SetQueryUpdatesPaused(false)
|
||||
return numRecords
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.LoadData(recordType, csvRecords)
|
||||
local decodeContext = CSV.DecodeStart(csvRecords, CSV_KEYS)
|
||||
if not decodeContext then
|
||||
Log.Err("Failed to decode %s records", recordType)
|
||||
private.dataChanged = true
|
||||
return
|
||||
end
|
||||
|
||||
for type, amount, otherPlayer, player, timestamp in CSV.DecodeIterator(decodeContext) do
|
||||
amount = tonumber(amount)
|
||||
timestamp = tonumber(timestamp)
|
||||
if amount and timestamp then
|
||||
local newTimestamp = floor(timestamp)
|
||||
if newTimestamp ~= timestamp then
|
||||
-- make sure all timestamps are stored as integers
|
||||
timestamp = newTimestamp
|
||||
private.dataChanged = true
|
||||
end
|
||||
private.db:BulkInsertNewRowFast6(recordType, type, amount, otherPlayer, player, timestamp)
|
||||
else
|
||||
private.dataChanged = true
|
||||
end
|
||||
end
|
||||
|
||||
if not CSV.DecodeEnd(decodeContext) then
|
||||
Log.Err("Failed to decode %s records", recordType)
|
||||
private.dataChanged = true
|
||||
end
|
||||
end
|
||||
|
||||
function private.SaveData(recordType)
|
||||
local query = private.db:NewQuery()
|
||||
:Equal("recordType", recordType)
|
||||
local encodeContext = CSV.EncodeStart(CSV_KEYS)
|
||||
for _, row in query:Iterator() do
|
||||
CSV.EncodeAddRowData(encodeContext, row)
|
||||
end
|
||||
query:Release()
|
||||
return CSV.EncodeEnd(encodeContext)
|
||||
end
|
||||
|
||||
function private.InsertRecord(recordType, type, amount, otherPlayer, timestamp)
|
||||
private.dataChanged = true
|
||||
assert(type and amount and amount > 0 and otherPlayer and timestamp)
|
||||
timestamp = floor(timestamp)
|
||||
local matchingRow = private.db:NewQuery()
|
||||
:Equal("recordType", recordType)
|
||||
:Equal("type", type)
|
||||
:Equal("otherPlayer", otherPlayer)
|
||||
:Equal("player", UnitName("player"))
|
||||
:GreaterThan("time", timestamp - COMBINE_TIME_THRESHOLD)
|
||||
:LessThan("time", timestamp + COMBINE_TIME_THRESHOLD)
|
||||
:GetFirstResultAndRelease()
|
||||
if matchingRow then
|
||||
matchingRow:SetField("amount", matchingRow:GetField("amount") + amount)
|
||||
matchingRow:Update()
|
||||
matchingRow:Release()
|
||||
else
|
||||
private.db:NewRow()
|
||||
:SetField("recordType", recordType)
|
||||
:SetField("type", type)
|
||||
:SetField("amount", amount)
|
||||
:SetField("otherPlayer", otherPlayer)
|
||||
:SetField("player", UnitName("player"))
|
||||
:SetField("time", timestamp)
|
||||
:Create()
|
||||
end
|
||||
end
|
||||
287
Core/Service/Accounting/Sync.lua
Normal file
287
Core/Service/Accounting/Sync.lua
Normal file
@@ -0,0 +1,287 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local AccountingSync = TSM.Accounting:NewPackage("Sync")
|
||||
local L = TSM.Include("Locale").GetTable()
|
||||
local Delay = TSM.Include("Util.Delay")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Theme = TSM.Include("Util.Theme")
|
||||
local Sync = TSM.Include("Service.Sync")
|
||||
local private = {
|
||||
accountLookup = {},
|
||||
accountStatus = {},
|
||||
pendingChunks = {},
|
||||
dataTemp = {},
|
||||
}
|
||||
local CHANGE_NOTIFICATION_DELAY = 5
|
||||
local RETRY_DELAY = 5
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function AccountingSync.OnInitialize()
|
||||
Sync.RegisterConnectionChangedCallback(private.ConnectionChangedHandler)
|
||||
Sync.RegisterRPC("ACCOUNTING_GET_PLAYER_HASH", private.RPCGetPlayerHash)
|
||||
Sync.RegisterRPC("ACCOUNTING_GET_PLAYER_CHUNKS", private.RPCGetPlayerChunks)
|
||||
Sync.RegisterRPC("ACCOUNTING_GET_PLAYER_DATA", private.RPCGetData)
|
||||
Sync.RegisterRPC("ACCOUNTING_CHANGE_NOTIFICATION", private.RPCChangeNotification)
|
||||
end
|
||||
|
||||
function AccountingSync.GetStatus(account)
|
||||
local status = private.accountStatus[account]
|
||||
if not status then
|
||||
return Theme.GetFeedbackColor("RED"):ColorText(L["Not Connected"])
|
||||
elseif status == "GET_PLAYER_HASH" or status == "GET_PLAYER_CHUNKS" or status == "GET_PLAYER_DATA" or status == "RETRY" then
|
||||
return Theme.GetFeedbackColor("YELLOW"):ColorText(L["Updating"])
|
||||
elseif status == "SYNCED" then
|
||||
return Theme.GetFeedbackColor("GREEN"):ColorText(L["Up to date"])
|
||||
else
|
||||
error("Invalid status: "..tostring(status))
|
||||
end
|
||||
end
|
||||
|
||||
function AccountingSync.OnTransactionsChanged()
|
||||
Delay.AfterTime("ACCOUNTING_SYNC_CHANGE", CHANGE_NOTIFICATION_DELAY, private.NotifyChange)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- RPC Functions and Result Handlers
|
||||
-- ============================================================================
|
||||
|
||||
function private.GetPlayerHash(player)
|
||||
local account = private.accountLookup[player]
|
||||
private.accountStatus[account] = "GET_PLAYER_HASH"
|
||||
TSM.Accounting.Transactions.PrepareSyncHashes(player)
|
||||
Sync.CallRPC("ACCOUNTING_GET_PLAYER_HASH", player, private.RPCGetPlayerHashResultHandler)
|
||||
end
|
||||
|
||||
function private.RPCGetPlayerHash()
|
||||
local player = UnitName("player")
|
||||
return player, TSM.Accounting.Transactions.GetSyncHash(player)
|
||||
end
|
||||
|
||||
function private.RPCGetPlayerHashResultHandler(player, hash)
|
||||
local account = player and private.accountLookup[player]
|
||||
if not account then
|
||||
-- request timed out, so try again
|
||||
Log.Warn("Getting player hash timed out")
|
||||
private.QueueRetriesByStatus("GET_PLAYER_HASH")
|
||||
return
|
||||
elseif not hash then
|
||||
-- the hash isn't ready yet, so try again
|
||||
Log.Warn("Sync player hash not ready yet")
|
||||
private.QueueRetryByPlayer(player)
|
||||
return
|
||||
end
|
||||
if private.accountStatus[account] == "RETRY" then
|
||||
-- There is a race condition where if we tried to issue GET_PLAYER_HASH for two players and one times out,
|
||||
-- we would also queue a retry for the other one, so handle that here.
|
||||
private.accountStatus[account] = "GET_PLAYER_HASH"
|
||||
end
|
||||
assert(private.accountStatus[account] == "GET_PLAYER_HASH")
|
||||
|
||||
local currentHash = TSM.Accounting.Transactions.GetSyncHash(player)
|
||||
if not currentHash then
|
||||
-- don't have our hash yet, so try again
|
||||
Log.Warn("Current player hash not ready yet")
|
||||
private.QueueRetryByPlayer(player)
|
||||
return
|
||||
end
|
||||
|
||||
if hash ~= currentHash then
|
||||
Log.Info("Need updated transactions data from %s (%s, %s)", player, hash, currentHash)
|
||||
private.GetPlayerChunks(player)
|
||||
else
|
||||
Log.Info("Transactions data for %s already up to date (%s, %s)", player, hash, currentHash)
|
||||
private.accountStatus[account] = "SYNCED"
|
||||
end
|
||||
end
|
||||
|
||||
function private.GetPlayerChunks(player)
|
||||
local account = private.accountLookup[player]
|
||||
private.accountStatus[account] = "GET_PLAYER_CHUNKS"
|
||||
Sync.CallRPC("ACCOUNTING_GET_PLAYER_CHUNKS", player, private.RPCGetPlayerChunksResultHandler)
|
||||
end
|
||||
|
||||
function private.RPCGetPlayerChunks()
|
||||
local player = UnitName("player")
|
||||
return player, TSM.Accounting.Transactions.GetSyncHashByDay(player)
|
||||
end
|
||||
|
||||
function private.RPCGetPlayerChunksResultHandler(player, chunks)
|
||||
local account = player and private.accountLookup[player]
|
||||
if not account then
|
||||
-- request timed out, so try again from the start
|
||||
Log.Warn("Getting chunks timed out")
|
||||
private.QueueRetriesByStatus("GET_PLAYER_CHUNKS")
|
||||
return
|
||||
elseif not chunks then
|
||||
-- the hashes have been invalidated, so try again from the start
|
||||
Log.Warn("Sync player chunks not ready yet")
|
||||
private.QueueRetryByPlayer(player)
|
||||
return
|
||||
end
|
||||
assert(private.accountStatus[account] == "GET_PLAYER_CHUNKS")
|
||||
|
||||
local currentChunks = TSM.Accounting.Transactions.GetSyncHashByDay(player)
|
||||
if not currentChunks then
|
||||
-- our hashes have been invalidated, so try again from the start
|
||||
Log.Warn("Local hashes are invalid")
|
||||
private.QueueRetryByPlayer(player)
|
||||
return
|
||||
end
|
||||
for day in pairs(currentChunks) do
|
||||
if not chunks[day] then
|
||||
-- remove day which no longer exists
|
||||
TSM.Accounting.Transactions.RemovePlayerDay(player, day)
|
||||
end
|
||||
end
|
||||
|
||||
-- queue up all the pending chunks
|
||||
private.pendingChunks[player] = private.pendingChunks[player] or TempTable.Acquire()
|
||||
wipe(private.pendingChunks[player])
|
||||
for day, hash in pairs(chunks) do
|
||||
if currentChunks[day] ~= hash then
|
||||
tinsert(private.pendingChunks[player], day)
|
||||
end
|
||||
end
|
||||
|
||||
local requestDay = private.GetNextPendingChunk(player)
|
||||
if requestDay then
|
||||
Log.Info("Requesting transactions data (%s, %s, %s, %s)", player, requestDay, tostring(currentChunks[requestDay]), chunks[requestDay])
|
||||
private.GetPlayerData(player, requestDay)
|
||||
else
|
||||
Log.Info("All chunks are up to date (%s)", player)
|
||||
private.accountStatus[account] = "SYNCED"
|
||||
end
|
||||
end
|
||||
|
||||
function private.GetPlayerData(player, requestDay)
|
||||
local account = private.accountLookup[player]
|
||||
private.accountStatus[account] = "GET_PLAYER_DATA"
|
||||
Sync.CallRPC("ACCOUNTING_GET_PLAYER_DATA", player, private.RPCGetDataResultHandler, requestDay)
|
||||
end
|
||||
|
||||
function private.RPCGetData(day)
|
||||
local player = UnitName("player")
|
||||
wipe(private.dataTemp)
|
||||
TSM.Accounting.Transactions.GetSyncData(player, day, private.dataTemp)
|
||||
return player, day, private.dataTemp
|
||||
end
|
||||
|
||||
function private.RPCGetDataResultHandler(player, day, data)
|
||||
local account = player and private.accountLookup[player]
|
||||
if not account then
|
||||
-- request timed out, so try again from the start
|
||||
Log.Warn("Getting transactions data timed out")
|
||||
private.QueueRetriesByStatus("GET_PLAYER_DATA")
|
||||
return
|
||||
elseif #data % 9 ~= 0 then
|
||||
-- invalid data - just silently give up
|
||||
Log.Warn("Got invalid transactions data")
|
||||
return
|
||||
end
|
||||
assert(private.accountStatus[account] == "GET_PLAYER_DATA")
|
||||
|
||||
Log.Info("Received transactions data (%s, %s, %s)", player, day, #data)
|
||||
TSM.Accounting.Transactions.HandleSyncedData(player, day, data)
|
||||
|
||||
local requestDay = private.GetNextPendingChunk(player)
|
||||
if requestDay then
|
||||
-- request the next chunk
|
||||
Log.Info("Requesting transactions data (%s, %s)", player, requestDay)
|
||||
private.GetPlayerData(player, requestDay)
|
||||
else
|
||||
-- request chunks again to check for other chunks we need to sync
|
||||
private.GetPlayerChunks(player)
|
||||
end
|
||||
end
|
||||
|
||||
function private.RPCChangeNotification(player)
|
||||
if private.accountStatus[private.accountLookup[player]] == "SYNCED" then
|
||||
-- request the player hash
|
||||
Log.Info("Got change notification - requesting player hash")
|
||||
private.GetPlayerHash(player)
|
||||
else
|
||||
Log.Info("Got change notification - dropping (%s)", tostring(private.accountStatus[private.accountLookup[player]]))
|
||||
end
|
||||
end
|
||||
|
||||
function private.RPCChangeNotificationResultHandler()
|
||||
-- nop
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.ConnectionChangedHandler(account, player, connected)
|
||||
if connected then
|
||||
private.accountLookup[player] = account
|
||||
private.GetPlayerHash(player)
|
||||
else
|
||||
private.accountLookup[player] = nil
|
||||
private.accountStatus[account] = nil
|
||||
if private.pendingChunks[player] then
|
||||
TempTable.Release(private.pendingChunks[player])
|
||||
private.pendingChunks[player] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function private.GetNextPendingChunk(player)
|
||||
if not private.pendingChunks[player] then
|
||||
return nil
|
||||
end
|
||||
local result = tremove(private.pendingChunks[player])
|
||||
if not result then
|
||||
TempTable.Release(private.pendingChunks[player])
|
||||
private.pendingChunks[player] = nil
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function private.QueueRetriesByStatus(statusFilter)
|
||||
for player, account in pairs(private.accountLookup) do
|
||||
if private.accountStatus[account] == statusFilter then
|
||||
private.QueueRetryByPlayer(player)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function private.QueueRetryByPlayer(player)
|
||||
local account = private.accountLookup[player]
|
||||
Log.Info("Retrying (%s, %s, %s)", player, account, private.accountStatus[account])
|
||||
private.accountStatus[account] = "RETRY"
|
||||
Delay.AfterTime(RETRY_DELAY, private.RetryGetPlayerHashRPC)
|
||||
end
|
||||
|
||||
function private.RetryGetPlayerHashRPC()
|
||||
for player, account in pairs(private.accountLookup) do
|
||||
if private.accountStatus[account] == "RETRY" then
|
||||
private.GetPlayerHash(player)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function private.NotifyChange()
|
||||
for player, account in pairs(private.accountLookup) do
|
||||
if private.accountStatus[account] == "SYNCED" then
|
||||
-- notify the other account that our data has changed and request the other account's latest hash ourselves
|
||||
private.GetPlayerHash(player)
|
||||
Sync.CallRPC("ACCOUNTING_CHANGE_NOTIFICATION", player, private.RPCChangeNotificationResultHandler, UnitName("player"))
|
||||
end
|
||||
end
|
||||
end
|
||||
165
Core/Service/Accounting/Trade.lua
Normal file
165
Core/Service/Accounting/Trade.lua
Normal file
@@ -0,0 +1,165 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Trade = TSM.Accounting:NewPackage("Trade")
|
||||
local L = TSM.Include("Locale").GetTable()
|
||||
local Event = TSM.Include("Util.Event")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Money = TSM.Include("Util.Money")
|
||||
local ItemString = TSM.Include("Util.ItemString")
|
||||
local Wow = TSM.Include("Util.Wow")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
local private = {
|
||||
tradeInfo = nil,
|
||||
popupContext = nil,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Trade.OnInitialize()
|
||||
Event.Register("TRADE_ACCEPT_UPDATE", private.OnAcceptUpdate)
|
||||
Event.Register("UI_INFO_MESSAGE", private.OnChatMsg)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Trade Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.OnAcceptUpdate(_, player, target)
|
||||
if (player == 1 or target == 1) and not (GetTradePlayerItemLink(7) or GetTradeTargetItemLink(7)) then
|
||||
-- update tradeInfo
|
||||
private.tradeInfo = { player = {}, target = {} }
|
||||
private.tradeInfo.player.money = tonumber(GetPlayerTradeMoney())
|
||||
private.tradeInfo.target.money = tonumber(GetTargetTradeMoney())
|
||||
private.tradeInfo.target.name = UnitName("NPC")
|
||||
|
||||
for i = 1, 6 do
|
||||
local targetLink = GetTradeTargetItemLink(i)
|
||||
local _, _, targetCount = GetTradeTargetItemInfo(i)
|
||||
if targetLink then
|
||||
tinsert(private.tradeInfo.target, { itemString = ItemString.Get(targetLink), count = targetCount })
|
||||
end
|
||||
|
||||
local playerLink = GetTradePlayerItemLink(i)
|
||||
local _, _, playerCount = GetTradePlayerItemInfo(i)
|
||||
if playerLink then
|
||||
tinsert(private.tradeInfo.player, { itemString = ItemString.Get(playerLink), count = playerCount })
|
||||
end
|
||||
end
|
||||
else
|
||||
private.tradeInfo = nil
|
||||
end
|
||||
end
|
||||
|
||||
function private.OnChatMsg(_, msg)
|
||||
if not TSM.db.global.accountingOptions.trackTrades then
|
||||
return
|
||||
end
|
||||
if msg == LE_GAME_ERR_TRADE_COMPLETE and private.tradeInfo then
|
||||
-- trade went through
|
||||
local tradeType, itemString, count, money = nil, nil, nil, nil
|
||||
if private.tradeInfo.player.money > 0 and #private.tradeInfo.player == 0 and private.tradeInfo.target.money == 0 and #private.tradeInfo.target > 0 then
|
||||
-- player bought items
|
||||
for i = 1, #private.tradeInfo.target do
|
||||
local data = private.tradeInfo.target[i]
|
||||
if not itemString then
|
||||
itemString = data.itemString
|
||||
count = data.count
|
||||
elseif itemString == data.itemString then
|
||||
count = count + data.count
|
||||
else
|
||||
return
|
||||
end
|
||||
end
|
||||
tradeType = "buy"
|
||||
money = private.tradeInfo.player.money
|
||||
elseif private.tradeInfo.player.money == 0 and #private.tradeInfo.player > 0 and private.tradeInfo.target.money > 0 and #private.tradeInfo.target == 0 then
|
||||
-- player sold items
|
||||
for i = 1, #private.tradeInfo.player do
|
||||
local data = private.tradeInfo.player[i]
|
||||
if not itemString then
|
||||
itemString = data.itemString
|
||||
count = data.count
|
||||
elseif itemString == data.itemString then
|
||||
count = count + data.count
|
||||
else
|
||||
return
|
||||
end
|
||||
end
|
||||
tradeType = "sale"
|
||||
money = private.tradeInfo.target.money
|
||||
end
|
||||
|
||||
if not tradeType or not itemString or not count then
|
||||
return
|
||||
end
|
||||
local insertInfo = TempTable.Acquire()
|
||||
insertInfo.type = tradeType
|
||||
insertInfo.itemString = itemString
|
||||
insertInfo.price = money / count
|
||||
insertInfo.count = count
|
||||
insertInfo.name = private.tradeInfo.target.name
|
||||
local gotText, gaveText = nil, nil
|
||||
if tradeType == "buy" then
|
||||
gotText = format("%sx%d", ItemInfo.GetLink(itemString), count)
|
||||
gaveText = Money.ToString(money)
|
||||
elseif tradeType == "sale" then
|
||||
gaveText = format("%sx%d", ItemInfo.GetLink(itemString), count)
|
||||
gotText = Money.ToString(money)
|
||||
else
|
||||
error("Invalid tradeType: "..tostring(tradeType))
|
||||
end
|
||||
|
||||
if TSM.db.global.accountingOptions.autoTrackTrades then
|
||||
private.DoInsert(insertInfo)
|
||||
TempTable.Release(insertInfo)
|
||||
else
|
||||
if private.popupContext then
|
||||
-- popup already visible so ignore this
|
||||
TempTable.Release(insertInfo)
|
||||
return
|
||||
end
|
||||
private.popupContext = insertInfo
|
||||
if not StaticPopupDialogs["TSMAccountingOnTrade"] then
|
||||
StaticPopupDialogs["TSMAccountingOnTrade"] = {
|
||||
button1 = YES,
|
||||
button2 = NO,
|
||||
timeout = 0,
|
||||
whileDead = true,
|
||||
hideOnEscape = true,
|
||||
OnAccept = function()
|
||||
private.DoInsert(private.popupContext)
|
||||
TempTable.Release(private.popupContext)
|
||||
private.popupContext = nil
|
||||
end,
|
||||
OnCancel = function()
|
||||
TempTable.Release(private.popupContext)
|
||||
private.popupContext = nil
|
||||
end,
|
||||
}
|
||||
end
|
||||
StaticPopupDialogs["TSMAccountingOnTrade"].text = format(L["TSM detected that you just traded %s to %s in return for %s. Would you like Accounting to store a record of this trade?"], gaveText, insertInfo.name, gotText)
|
||||
Wow.ShowStaticPopupDialog("TSMAccountingOnTrade")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function private.DoInsert(info)
|
||||
if info.type == "sale" then
|
||||
TSM.Accounting.Transactions.InsertTradeSale(info.itemString, info.count, info.price, info.name)
|
||||
elseif info.type == "buy" then
|
||||
TSM.Accounting.Transactions.InsertTradeBuy(info.itemString, info.count, info.price, info.name)
|
||||
else
|
||||
error("Unknown type: "..tostring(info.type))
|
||||
end
|
||||
end
|
||||
825
Core/Service/Accounting/Transactions.lua
Normal file
825
Core/Service/Accounting/Transactions.lua
Normal file
@@ -0,0 +1,825 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- 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
|
||||
Reference in New Issue
Block a user