292 lines
11 KiB
Lua
292 lines
11 KiB
Lua
-- ------------------------------------------------------------------------------ --
|
|
-- 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
|