initial commit
This commit is contained in:
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
|
||||
Reference in New Issue
Block a user