You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1268 lines
43 KiB

4 years ago
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster_Accounting --
-- http://www.curse.com/addons/wow/tradeskillmaster_accounting --
-- --
-- A TradeSkillMaster Addon (http://tradeskillmaster.com) --
-- All Rights Reserved* - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
-- create a local reference to the TradeSkillMaster_Accounting table and register a new module
local TSM = select(2, ...)
local Data = TSM:NewModule("Data", "AceEvent-3.0", "AceHook-3.0")
local L = LibStub("AceLocale-3.0"):GetLocale("TradeSkillMaster_Accounting") -- loads the localization table
local LibParse = LibStub("LibParse")
local private = {}
TSMAPI:RegisterForTracing(private, "TSM_Accounting.Data_private")
local SECONDS_PER_DAY = 24 * 60 * 60
local TIME_BUCKET = 300 -- group sales/buys within 5 minutes together
local REPAIR_COST, REPAIR_MONEY, COULD_REPAIR, CAN_REPAIR = 0, 0, false, ""
local expired = AUCTION_EXPIRED_MAIL_SUBJECT:gsub("%%s", "")
local cancelled = AUCTION_REMOVED_MAIL_SUBJECT:gsub("%%s", "")
local outbid = AUCTION_OUTBID_MAIL_SUBJECT:gsub("%%s", "(.+)")
--[[
****************************** In-Memory Data Layout ***************************
TSM = {
items = {
[itemString] = {
sales = {
-- Possible keys: Auction, COD, Trade, Vendor
{key="...", stackSize=#, quantity=#, time=#, copper=#, player="...", otherPlayer="..."},
...
},
buys = {
-- Possible keys: Auction, COD, Trade, Vendor
{key="...", stackSize=#, quantity=#, time=#, copper=#, player="...", otherPlayer="..."},
...
},
auctions = {
-- Possible keys: Cancel, Expire
{key="...", stackSize=#, quantity=#, time=#, player="..."},
...
},
},
},
money = {
income = {
-- Possible keys: Transfer
{key="...", copper=#, time=#, player="...", otherPlayer="..."},
},
expense = {
-- Possible keys: Postage, Repair, Transfer
{key="...", copper=#, time=#, player="...", otherPlayer="..."},
},
},
}
]]
function private:CleanRecord(record)
record.itemName = nil
record.itemString = nil
record.time = floor(record.time)
record.type = nil
record.otherPlayer = record.buyer or record.seller or record.destination or record.source or "?"
record.copper = record.price or record.amount
record.price = nil
record.amount = nil
record.buyer = nil
record.seller = nil
record.destination = nil
record.source = nil
end
function private:LoadItemRecords(csvData, recordType, key)
local saveTimeIndex = 1
local saveTimes
if recordType == "sales" then
saveTimes = TSMAPI:SafeStrSplit(TSM.db.factionrealm.saveTimeSales, ",")
elseif recordType == "buys" then
saveTimes = TSMAPI:SafeStrSplit(TSM.db.factionrealm.saveTimeBuys, ",")
end
for _, record in ipairs(select(2, LibParse:CSVDecode(csvData)) or {}) do
local itemString = record.itemString
if itemString and type(record.time) == "number" then
local itemName = (record.itemName ~= "?") and record.itemName or nil
itemName = itemName or TSMAPI:GetSafeItemInfo(itemString) or TSM:GetItemName(itemString)
record.key = key or record.source or "Auction"
private:CleanRecord(record)
if saveTimes and record.key == "Auction" then
record.saveTime = tonumber(saveTimes[saveTimeIndex])
saveTimeIndex = saveTimeIndex + 1
end
TSM.items[itemString] = TSM.items[itemString] or {sales={}, buys={}, auctions={}}
TSM.items[itemString].name = TSM.items[itemString].name or itemName
tinsert(TSM.items[itemString][recordType], record)
TSM.cache[itemString] = {}
end
end
for itemString in ipairs(TSM.items) do
sort(TSM.items[itemString][recordType], function(a, b) return (a.time or 0) < (b.time or 0) end)
end
end
function private:LoadMoneyRecords(csvData, recordType)
TSM.money[recordType] = {}
local typeTranslation = {}
if recordType == "income" then
typeTranslation = {["Money Transfer"]="Transfer"}
elseif recordType == "expense" then
typeTranslation = {["Money Transfer"]="Transfer", ["Postage"]="Postage", ["Repair Bill"]="Repair"}
end
for _, record in ipairs(select(2, LibParse:CSVDecode(csvData)) or {}) do
record.key = typeTranslation[record.type] or "Transfer"
if record.key and type(record.time) == "number" then
private:CleanRecord(record)
tinsert(TSM.money[recordType], record)
end
end
end
function Data:Load()
-- Decode item records
TSM.items = {}
TSM.cache = {}
private:LoadItemRecords(TSM.db.factionrealm.csvSales, "sales")
private:LoadItemRecords(TSM.db.factionrealm.csvBuys, "buys")
private:LoadItemRecords(TSM.db.factionrealm.csvCancelled, "auctions", "Cancel")
private:LoadItemRecords(TSM.db.factionrealm.csvExpired, "auctions", "Expire")
-- Decode money records
TSM.money = {}
private:LoadMoneyRecords(TSM.db.factionrealm.csvIncome, "income")
private:LoadMoneyRecords(TSM.db.factionrealm.csvExpense, "expense")
-- Decode the gold log
for player, data in pairs(TSM.db.factionrealm.goldLog) do
if type(data) == "string" then
TSM.db.factionrealm.goldLog[player] = select(2, LibParse:CSVDecode(data))
end
end
Data:SetupDataTracking()
Data:PopulateDataCaches()
end
function Data:SetupDataTracking()
Data:RawHook("TakeInboxItem", function(...) Data:ScanCollectedMail("TakeInboxItem", 1, ...) end, true)
Data:RawHook("TakeInboxMoney", function(...) Data:ScanCollectedMail("TakeInboxMoney", 1, ...) end, true)
Data:RawHook("AutoLootMailItem", function(...) Data:ScanCollectedMail("AutoLootMailItem", 1, ...) end, true)
Data:RawHook("SendMail", function(...) Data:CheckSendMail("SendMail", ...) end, true)
Data:SecureHook("UseContainerItem", function(...) Data:CheckMerchantSale(...) end)
Data:SecureHook("BuyMerchantItem", function(...) Data:BuyMerchantItem(...) end)
Data:SecureHook("BuybackItem", function(...) Data:BuybackMerchantItem(...) end)
Data:RegisterEvent("AUCTION_OWNED_LIST_UPDATE", "ScanAuctionItems")
Data:RegisterEvent("MERCHANT_SHOW", "SetupRepairCost")
Data:RegisterEvent("MERCHANT_UPDATE", "ResetRepairMoney")
Data:RegisterEvent("UPDATE_INVENTORY_DURABILITY", "AddRepairCosts")
Data:RegisterEvent("MAIL_SHOW")
Data:RegisterEvent("MAIL_CLOSED")
TSMAPI:RegisterForBagChange(function(...) Data:ScanBagItems(...) end)
TSMAPI:CreateFunctionRepeat("accountingGoldTracking", Data.LogGold)
end
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
TSMAPI:CancelFrame("accountingGetSellers")
end
end
function Data:MAIL_SHOW()
TSMAPI:CreateTimeDelay("accountingGetSellers", 0.1, private.RequestSellerInfo, 0.1)
end
function Data:MAIL_CLOSED()
TSMAPI:CancelFrame("accountingGetSellers")
end
function private:CanCombineRecords(recordA, recordB)
local keys = {"key", "copper", "stackSize", "player", "otherPlayer"}
for _, key in ipairs(keys) do
if recordA[key] ~= recordB[key] then
return false
end
end
return abs(recordA.time-recordB.time) < TIME_BUCKET
end
function private:InsertItemRecord(itemString, dataType, newRecord)
newRecord.time = floor(newRecord.time or time())
newRecord.player = UnitName("player")
TSM.items[itemString] = TSM.items[itemString] or {sales={}, buys={}, auctions={}}
for _, record in ipairs(TSM.items[itemString][dataType]) do
if private:CanCombineRecords(record, newRecord) then
-- combine with existing record
record.quantity = record.quantity + newRecord.stackSize -- this is total quantity, not number of stacks
return
end
end
tinsert(TSM.items[itemString][dataType], newRecord)
TSM.cache[itemString] = {}
-- keep the records sorted by time
sort(TSM.items[itemString][dataType], function(a, b) return (a.time or 0) < (b.time or 0) end)
end
function Data:InsertItemSaleRecord(itemString, key, stackSize, copper, buyer, timeStamp)
if not (itemString and key and stackSize and copper and buyer and copper > 0) then return end
if key ~= "Auction" and key ~= "COD" and key ~= "Trade" and key ~= "Vendor" then return end
private:InsertItemRecord(itemString, "sales", {key=key, stackSize=stackSize, quantity=stackSize, copper=copper, otherPlayer=buyer, time=timeStamp})
end
function Data:InsertItemBuyRecord(itemString, key, stackSize, copper, seller, timeStamp)
if not (itemString and key and stackSize and copper and seller and copper > 0) then return end
if key ~= "Auction" and key ~= "COD" and key ~= "Trade" and key ~= "Vendor" then return end
private:InsertItemRecord(itemString, "buys", {key=key, stackSize=stackSize, quantity=stackSize, copper=copper, otherPlayer=seller, time=timeStamp})
end
function Data:InsertItemAuctionRecord(itemString, key, stackSize, timeStamp)
if not (itemString and key and stackSize) then return end
if key ~= "Cancel" and key ~= "Expire" then return end
private:InsertItemRecord(itemString, "auctions", {key=key, stackSize=stackSize, quantity=stackSize, time=timeStamp})
end
function private:InsertMoneyRecord(dataType, newRecord)
newRecord.time = floor(time())
newRecord.player = UnitName("player")
for _, record in ipairs(TSM.money[dataType]) do
if private:CanCombineRecords(record, newRecord) then
-- combine with existing record
record.copper = record.copper + newRecord.copper
return
end
end
tinsert(TSM.money[dataType], newRecord)
end
function Data:InsertMoneyIncomeRecord(key, copper, destination, timeStamp)
if not (key and copper and destination and copper > 0) then return end
if key ~= "Transfer" then return end
private:InsertMoneyRecord("income", {key=key, copper=copper, otherPlayer=destination, time=timeStamp})
end
function Data:InsertMoneyExpenseRecord(key, copper, destination, timeStamp)
if not (key and copper and destination and copper > 0) then return end
if key ~= "Postage" and key ~= "Repair" and key ~= "Transfer" then return end
private:InsertMoneyRecord("expense", {key=key, copper=copper, otherPlayer=destination, time=timeStamp})
end
function Data:CheckMerchantSale(bag, slot, onSelf)
if MerchantFrame:IsShown() and not onSelf then
local itemString = TSMAPI:GetItemString(GetContainerItemLink(bag, slot))
local quantity = select(2, GetContainerItemInfo(bag, slot))
local copper = select(11, TSMAPI:GetSafeItemInfo(itemString))
Data:InsertItemSaleRecord(itemString, "Vendor", quantity, copper, "Merchant")
end
end
function Data:BuyMerchantItem(index, quantity)
local itemName, _, price, batchQuantity = GetMerchantItemInfo(index)
if not price or price <= 0 then return end
if not quantity then
quantity = batchQuantity
end
local link = GetMerchantItemLink(index);
local itemString = TSM.db.global.itemStrings[itemName] or TSMAPI:GetItemString(link)
local copper = floor(price / batchQuantity + 0.5)
Data:InsertItemBuyRecord(itemString, "Vendor", quantity, copper, "Merchant")
end
function Data:BuybackMerchantItem(index)
local itemName, _, price, quantity = GetBuybackItemInfo(index)
local link = GetMerchantItemLink(index)
local itemString = TSM.db.global.itemStrings[itemName] or TSMAPI:GetItemString(link)
local copper = floor(price / quantity + 0.5)
Data:InsertItemBuyRecord(itemString, "Vendor", quantity, copper, "Merchant")
end
function Data:AddRepairCosts()
if (COULD_REPAIR and REPAIR_COST > 0) then
local cash = GetMoney()
if (REPAIR_MONEY > cash) then
-- this is probably a repair bill
local cost = REPAIR_MONEY - cash
Data:InsertMoneyExpenseRecord("Repair", cost, "Merchant")
-- reset money as this might have been a single item repair
REPAIR_MONEY = cash
-- reset the repair cost for the next repair
REPAIR_COST, CAN_REPAIR = GetRepairAllCost()
end
end
end
-- scans the mail that the player just attempted to send (Pre-Hook) to see if COD
function Data:CheckSendMail(oFunc, destination, currentSubject, bodyText)
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
Data:InsertMoneyExpenseRecord("Transfer", moneyAmount, destination)
Data:InsertMoneyExpenseRecord("Postage", mailCost - moneyAmount, destination)
else
-- add a record for the mail cost
Data:InsertMoneyExpenseRecord("Postage", mailCost, destination)
end
if not ignore then
Data.hooks[oFunc](destination, subject .. " (" .. total .. ") TSM", bodyText)
else
Data.hooks[oFunc](destination, currentSubject, bodyText)
end
end
function private:CanLootMailIndex(index)
local hasItem = select(8, GetInboxHeaderInfo(index))
if not hasItem or hasItem == 0 then return true end
for j = 1, ATTACHMENTS_MAX_RECEIVE do
local itemString = TSMAPI:GetItemString(GetInboxItemLink(index, j))
local quantity = select(3, GetInboxItem(index, j))
local space = 0
if itemString then
for bag = 0, NUM_BAG_SLOTS do
if TSMAPI:ItemWillGoInBag(itemString, bag) then
for slot = 1, GetContainerNumSlots(bag) do
local iString = TSMAPI:GetItemString(GetContainerItemLink(bag, slot))
if iString == itemString then
local stackSize = select(2, GetContainerItemInfo(bag, slot))
local maxStackSize = select(8, TSMAPI:GetSafeItemInfo(itemString))
if (maxStackSize - stackSize) >= quantity then
return true
end
elseif not iString then
return true
end
end
end
end
end
end
end
-- scans the mail that the player just attempted to collected (Pre-Hook)
function Data:ScanCollectedMail(oFunc, attempt, index, subIndex)
local invoiceType, itemName, buyer, bid, buyout, deposit, ahcut = GetInboxInvoiceInfo(index)
--local invoiceType, itemName, buyer, bid, _, _, ahcut, _, _, _, quantity = GetInboxInvoiceInfo(index)
local sender, subject, money, codAmount, _, itemCount = select(3, GetInboxHeaderInfo(index))
if not subject then return end
local success = true
if attempt > 2 then
if buyer == "" then
buyer = "?"
elseif sender == "" then
sender = "?"
end
end
local quantity = 0
for j = 1, ATTACHMENTS_MAX_RECEIVE do
quantity = select(3, GetInboxItem(index, j))
end
if quantity == 0 then
quantity = 1
end
if invoiceType == "seller" and buyer and buyer ~= "" then -- AH Sales
local daysLeft = select(7, GetInboxHeaderInfo(index))
local saleTime = (time() + (daysLeft - 30) * SECONDS_PER_DAY)
local link = select(2, TSMAPI:GetSafeItemInfo(itemName))
local itemString = TSM.db.global.itemStrings[itemName] or TSMAPI:GetItemString(link)
if itemString and private:CanLootMailIndex(index) then
local copper = floor((bid - ahcut) / quantity + 0.5)
Data:InsertItemSaleRecord(itemString, "Auction", quantity, copper, buyer, saleTime)
end
elseif invoiceType == "buyer" and buyer and buyer ~= "" then -- AH Buys
local link = GetInboxItemLink(index, subIndex or 1)
local itemString = TSMAPI:GetItemString(link)
if itemString and private:CanLootMailIndex(index) then
--might as well grab the name for future lookups
local name = TSMAPI:GetSafeItemInfo(link)
TSM.db.global.itemStrings[name] = itemString
local copper = floor(bid / quantity + 0.5)
local daysLeft = select(7, GetInboxHeaderInfo(index))
local buyTime = (time() + (daysLeft - 30) * SECONDS_PER_DAY)
Data:InsertItemBuyRecord(itemString, "Auction", quantity, copper, buyer, buyTime)
end
elseif codAmount > 0 then -- COD Buys (only if all attachments are same item)
local link = GetInboxItemLink(index, subIndex or 1)
local itemString = TSMAPI:GetItemString(link)
if itemString then
--might as well grab the name for future lookups
local name = TSMAPI:GetSafeItemInfo(link)
TSM.db.global.itemStrings[name] = itemString
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) then
local copper = floor(codAmount / total + 0.5)
local daysLeft = select(7, GetInboxHeaderInfo(index))
local buyTime = (time() + (daysLeft - 3) * SECONDS_PER_DAY)
local maxStack = select(8, TSMAPI:GetSafeItemInfo(link))
for i = 1, stacks do
local stackSize = (total >= maxStack) and maxStack or total
Data:InsertItemBuyRecord(itemString, "COD", stackSize, copper, sender, buyTime)
total = total - stackSize
if total <= 0 then break end
end
end
end
elseif money > 0 and invoiceType ~= "seller" and not strfind(subject, outbid) then
local str
if GetLocale() == "deDE" then
str = gsub(subject, gsub(COD_PAYMENT, TSMAPI:StrEscape("%1$s"), ""), "")
else
str = gsub(subject, gsub(COD_PAYMENT, TSMAPI:StrEscape("%s"), ""), "")
end
local daysLeft = select(7, GetInboxHeaderInfo(index))
local saleTime = (time() + (daysLeft - 31) * SECONDS_PER_DAY)
if private:CanLootMailIndex(index) then
if str and strfind(str, "TSM$") then -- payment for a COD the player sent
local codName = strmatch(str, "([^%(]+)"):trim()
local qty = strmatch(str, "%(([0-9]+)%)")
qty = tonumber(qty)
local itemString = TSM.db.global.itemStrings[codName]
if itemString then
local copper = floor(money / qty + 0.5)
local maxStack = select(8, TSMAPI:GetSafeItemInfo(itemString)) or 1
local stacks = ceil(qty / maxStack)
for i = 1, stacks do
local stackSize = (qty >= maxStack) and maxStack or qty
Data:InsertItemSaleRecord(itemString, "COD", stackSize, copper, sender, saleTime)
qty = qty - stackSize
if qty <= 0 then break end
end
end
else -- record a money transfer
Data:InsertMoneyIncomeRecord("Transfer", money, sender, saleTime)
end
end
elseif strfind(subject, expired) then -- expired auction
local daysLeft = select(7, GetInboxHeaderInfo(index))
local expiredTime = (time() + (daysLeft - 30) * SECONDS_PER_DAY)
local link = GetInboxItemLink(index, subIndex or 1)
local qty = select(3, GetInboxItem(index, subIndex or 1))
local itemString = TSMAPI:GetItemString(link)
if private:CanLootMailIndex(index) then
Data:InsertItemAuctionRecord(itemString, "Expire", qty, expiredTime)
end
elseif strfind(subject, cancelled) then -- cancelled auction
local daysLeft = select(7, GetInboxHeaderInfo(index))
local cancelledTime = (time() + (daysLeft - 30) * SECONDS_PER_DAY)
local link = GetInboxItemLink(index, subIndex or 1)
local qty = select(3, GetInboxItem(index, subIndex or 1))
local itemString = TSMAPI:GetItemString(link)
if private:CanLootMailIndex(index) then
Data:InsertItemAuctionRecord(itemString, "Cancel", qty, cancelledTime)
end
else
success = false
end
if success then
Data.hooks[oFunc](index, subIndex)
elseif (not select(2, GetInboxHeaderInfo(index)) or (invoiceType and (not buyer or buyer == ""))) and attempt <= 5 then
TSMAPI:CreateTimeDelay("accountingHookDelay", 0.2, function() Data:ScanCollectedMail(oFunc, attempt + 1, index, subIndex) end)
elseif attempt > 5 then
Data.hooks[oFunc](index, subIndex)
else
Data.hooks[oFunc](index, subIndex)
end
end
-- scan the auctions the user has on the AH for name -> itemString lookup table
function Data:ScanAuctionItems()
for i = 1, GetNumAuctionItems("owner") do
local name = GetAuctionItemInfo("owner", i)
if name then
local itemString = TSMAPI:GetItemString(GetAuctionItemLink("owner", i))
TSM.db.global.itemStrings[name] = itemString
end
end
end
-- scans the bags to help build the name -> itemString lookup table
function Data:ScanBagItems(state)
for itemString in pairs(state) do
local name = TSMAPI:GetSafeItemInfo(itemString)
if name then
TSM.db.global.itemStrings[name] = itemString
end
end
end
-- ************************************************ --
-- GUI Helper Functions --
-- ************************************************ --
-- returns a formatted time in the format that the user has selected
function private:GetFormattedTime(rTime)
if TSM.db.factionrealm.timeFormat == "ago" then
return format(L["%s ago"], SecondsToTime(time() - rTime) or "?")
elseif TSM.db.factionrealm.timeFormat == "usdate" then
return date("%m/%d/%y %H:%M", rTime)
elseif TSM.db.factionrealm.timeFormat == "eudate" then
return date("%d/%m/%y %H:%M", rTime)
elseif TSM.db.factionrealm.timeFormat == "aidate" then
return date("%y/%m/%d %H:%M", rTime)
end
end
function private:IsItemFiltered(itemString, filters)
local name, _, rarity = TSMAPI:GetSafeItemInfo(itemString)
name = name or TSM.items[itemString].name
rarity = rarity or 0
if not name then return true end
if filters.name and not strfind(strlower(name), strlower(filters.name)) then
return true
end
if filters.rarity and rarity ~= filters.rarity then
return true
end
if not TSM.db.factionrealm.displayGreys and rarity == 0 then
return true
end
if filters.group then
local groupPath = TSMAPI:GetGroupPath(itemString)
if not groupPath or not strfind(groupPath, "^"..TSMAPI:StrEscape(filters.group)) then
return true
end
end
end
function private:IsRecordFiltered(record, filters)
if filters.player and record.player ~= filters.player then
return true
end
if filters.otherPlayer and record.otherPlayer ~= filters.otherPlayer then
return true
end
if not TSM.db.factionrealm.displayTransfers and record.key == "Transfer" then
return true
end
if filters.time and floor(record.time/SECONDS_PER_DAY) < (floor(time()/SECONDS_PER_DAY) - filters.time) then
return true
end
if not record.key or (filters.key and record.key ~= filters.key) then
return true
end
end
function private:GetItemSTData(dataType, filters)
local stData = {}
for itemString, data in pairs(TSM.items) do
if #data[dataType] > 0 and not private:IsItemFiltered(itemString, filters) then
for _, record in ipairs(data[dataType]) do
if not private:IsRecordFiltered(record, filters) then
local row = {
cols = {
{
value = select(2, TSMAPI:GetSafeItemInfo(itemString)) or TSM.items[itemString].name,
sortArg = TSM.items[itemString].name or itemString,
},
{
value = record.player,
sortArg = record.player,
},
{
value = record.key,
sortArg = record.key,
},
{
value = record.stackSize,
sortArg = record.stackSize,
},
{
value = floor(record.quantity / record.stackSize),
sortArg = floor(record.quantity / record.stackSize),
},
{
value = TSMAPI:FormatTextMoney(record.copper),
sortArg = record.copper,
},
{
value = TSMAPI:FormatTextMoney(record.copper*record.quantity),
sortArg = record.copper*record.quantity,
},
{
value = private:GetFormattedTime(record.time),
sortArg = record.time,
},
},
itemString = itemString,
record = record,
}
tinsert(stData, row)
end
end
end
end
return stData
end
function Data.GetSaleSTData(filters)
return private:GetItemSTData("sales", filters)
end
function Data.GetBuySTData(filters)
return private:GetItemSTData("buys", filters)
end
function private:GetItemSummaryData(filters, includeProfit)
local itemData = {}
for itemString, data in pairs(TSM.items) do
if not private:IsItemFiltered(itemString, filters) then
local sellTotal, sellNum = 0, 0
for _, record in ipairs(data.sales) do
if not private:IsRecordFiltered(record, filters) then
sellTotal = sellTotal + record.copper * record.quantity
sellNum = sellNum + record.quantity
end
end
local avgSell = TSM:Round(sellTotal / sellNum) or 0
local buyTotal, buyNum = 0, 0
for _, record in ipairs(data.buys) do
if not private:IsRecordFiltered(record, filters) then
buyTotal = buyTotal + record.copper * record.quantity
buyNum = buyNum + record.quantity
end
end
local avgBuy = TSM:Round(buyTotal / buyNum) or 0
local isValidItem
local profit, profitText
if includeProfit and buyNum > 0 and sellNum > 0 then
profit = avgSell - avgBuy
local profitPercent = TSM:Round(100 * profit / avgBuy)
local color = profit > 0 and "|cff00ff00" or "|cffff0000"
profitText = TSMAPI:FormatTextMoney(profit, color) .. " (" .. color .. profitPercent .. "%|r)"
isValidItem = true
elseif not includeProfit then
isValidItem = (buyNum > 0 or sellNum > 0)
end
if isValidItem then
itemData[itemString] = {buyNum=buyNum, sellNum=sellNum, profit=profit, profitText=profitText}
if TSM.db.factionrealm.priceFormat == "total" then
itemData[itemString].avgSell = sellNum > 0 and sellTotal or 0
itemData[itemString].avgBuy = buyNum > 0 and buyTotal or 0
else
itemData[itemString].avgSell = sellNum > 0 and avgSell or 0
itemData[itemString].avgBuy = buyNum > 0 and avgBuy or 0
end
end
end
end
return itemData
end
function Data.GetResaleSTData(filters)
local resaleItemData = private:GetItemSummaryData(filters, true)
local stData = {}
for itemString, data in pairs(resaleItemData) do
local name = TSM.items[itemString].name
local row = {
cols = {
{
value = select(2, TSMAPI:GetSafeItemInfo(itemString)) or TSM.items[itemString].name,
sortArg = TSM.items[itemString].name or itemString,
},
{
value = data.sellNum,
sortArg = data.sellNum,
},
{
value = TSMAPI:FormatTextMoney(data.avgSell),
sortArg = data.avgSell,
},
{
value = data.buyNum,
sortArg = data.buyNum,
},
{
value = TSMAPI:FormatTextMoney(data.avgBuy),
sortArg = data.avgBuy,
},
{
value = data.profitText,
sortArg = data.profit,
},
},
itemString = itemString,
totalNum = data.sellNum + data.buyNum,
}
tinsert(stData, row)
end
return stData
end
function Data.GetItemSummarySTData(filters)
local itemData = private:GetItemSummaryData(filters, false)
local stData = {}
for itemString, data in pairs(itemData) do
local name = TSM.items[itemString].name
local marketValue = TSMAPI:GetItemValue(itemString, TSM.db.factionrealm.mvSource)
local row = {
cols = {
{
value = select(2, TSMAPI:GetSafeItemInfo(itemString)) or name,
sortArg = name or itemString,
},
{
value = TSMAPI:FormatTextMoney(marketValue) or "|cff999999---|r",
sortArg = marketValue or 0,
},
{
value = data.sellNum,
sortArg = data.sellNum,
},
{
value = data.avgSell > 0 and TSMAPI:FormatTextMoney(data.avgSell) or "|cff999999---|r",
sortArg = data.avgSell,
},
{
value = data.buyNum,
sortArg = data.buyNum,
},
{
value = data.avgSell > 0 and TSMAPI:FormatTextMoney(data.avgBuy) or "|cff999999---|r",
sortArg = data.avgBuy,
},
},
itemString = itemString,
totalNum = data.sellNum + data.buyNum,
}
tinsert(stData, row)
end
return stData
end
function private:GetAuctionSTData(dataType, filters)
local stData = {}
for itemString, data in pairs(TSM.items) do
if #data.auctions > 0 and not private:IsItemFiltered(itemString, filters) then
for _, record in ipairs(data.auctions) do
if record.key == dataType and not private:IsRecordFiltered(record, filters) then
local row = {
cols = {
{
value = select(2, TSMAPI:GetSafeItemInfo(itemString)) or data.name,
sortArg = data.name or itemString,
},
{
value = record.player,
sortArg = record.player,
},
{
value = record.stackSize,
sortArg = record.stackSize,
},
{
value = record.quantity / record.stackSize,
sortArg = record.quantity / record.stackSize,
},
{
value = private:GetFormattedTime(record.time),
sortArg = record.time,
},
},
itemString = itemString,
}
tinsert(stData, row)
end
end
end
end
return stData
end
function Data.GetExpireSTData(filters)
return private:GetAuctionSTData("Expire", filters)
end
function Data.GetCancelSTData(filters)
return private:GetAuctionSTData("Cancel", filters)
end
function private:GetMoneySTData(dataType, filters)
local stData = {}
for _, record in ipairs(TSM.money[dataType]) do
if not private:IsRecordFiltered(record, filters) then
local row = {
cols = {
{
value = record.key,
sortArg = record.key,
},
{
value = record.player,
sortArg = record.player,
},
{
value = record.otherPlayer,
sortArg = record.otherPlayer,
},
{
value = TSMAPI:FormatTextMoney(record.copper),
sortArg = record.copper,
},
{
value = private:GetFormattedTime(record.time),
sortArg = record.time,
},
},
}
tinsert(stData, row)
end
end
return stData
end
function Data.GetIncomeSTData(filters)
return private:GetMoneySTData("income", filters)
end
function Data.GetExpenseSTData(filters)
return private:GetMoneySTData("expense", filters)
end
function Data.GetSummaryData(filters)
local goldData = {
sales = {total=0, month=0, week=0, topGold={}, topQuantity={}},
income = {total=0, month=0, week=0, topGold={}, topQuantity={}},
buys = {total=0, month=0, week=0, topGold={}, topQuantity={}},
expense = {total=0, month=0, week=0, topGold={}, topQuantity={}},
profit = {total=0, month=0, week=0},
totalTime = 0,
monthTime = 0,
weekTime = 0,
}
local function ProcessSummaryItemData(itemData, resultTbl, itemString)
local itemTotal, itemNum = 0, 0
for _, record in ipairs(itemData) do
if not private:IsRecordFiltered(record, filters) then
local timeDiff = time() - record.time
-- update local variables
itemNum = itemNum + record.quantity
itemTotal = itemTotal + record.copper * record.quantity
-- update total data
resultTbl.total = resultTbl.total + record.copper * record.quantity
goldData.totalTime = max(goldData.totalTime, timeDiff)
if timeDiff <= (SECONDS_PER_DAY * 30) then
-- update month data
resultTbl.month = resultTbl.month + record.copper * record.quantity
goldData.monthTime = max(goldData.monthTime, timeDiff)
end
if timeDiff <= (SECONDS_PER_DAY * 7) then
-- update week data
resultTbl.week = resultTbl.week + record.copper * record.quantity
goldData.weekTime = max(goldData.weekTime, timeDiff)
end
end
end
-- check if this is a top item by gold and/or quantity
if itemTotal > (resultTbl.topGold.copper or 0) then
resultTbl.topGold = {itemString=itemString, copper=itemTotal, itemID=TSMAPI:GetItemID(itemString)}
end
if itemNum > (resultTbl.topQuantity.num or 0) then
resultTbl.topQuantity = {itemString=itemString, num=itemNum, itemID=TSMAPI:GetItemID(itemString)}
end
end
for itemString, data in pairs(TSM.items) do
if not private:IsItemFiltered(itemString, filters) then
ProcessSummaryItemData(data.sales, goldData.sales, itemString)
ProcessSummaryItemData(data.buys, goldData.buys, itemString)
end
end
local function ProcessSummaryMoneyData(moneyData, resultTbl)
local moneyKeyNum, moneyKeyGold = {}, {}
for _, record in ipairs(moneyData) do
if not private:IsRecordFiltered(record, filters) then
local timeDiff = time() - record.time
-- update local variables
moneyKeyNum[record.key] = (moneyKeyNum[record.key] or 0) + 1
moneyKeyGold[record.key] = (moneyKeyGold[record.key] or 0) + record.copper
-- update total data
resultTbl.total = resultTbl.total + record.copper
goldData.totalTime = max(goldData.totalTime, timeDiff)
if timeDiff < (SECONDS_PER_DAY * 30) then
-- update month data
resultTbl.month = resultTbl.month + record.copper
goldData.monthTime = max(goldData.monthTime, timeDiff)
end
if timeDiff < (SECONDS_PER_DAY * 7) then
-- update week data
resultTbl.week = resultTbl.week + record.copper
goldData.weekTime = max(goldData.weekTime, timeDiff)
end
end
end
for key, total in pairs(moneyKeyNum) do
if total > (resultTbl.topQuantity.num or 0) then
resultTbl.topQuantity = {key=key, num=total}
end
end
for key, total in pairs(moneyKeyGold) do
if total > (resultTbl.topGold.copper or 0) then
resultTbl.topGold = {key=key, copper=total}
end
end
end
ProcessSummaryMoneyData(TSM.money.income, goldData.income)
ProcessSummaryMoneyData(TSM.money.expense, goldData.expense)
goldData.sales.topGold.link = goldData.sales.topGold.itemString and (select(2, TSMAPI:GetSafeItemInfo(goldData.sales.topGold.itemString)) or TSM.items[goldData.sales.topGold.itemString].name) or L["none"]
goldData.sales.topQuantity.link = goldData.sales.topQuantity.itemString and (select(2, TSMAPI:GetSafeItemInfo(goldData.sales.topQuantity.itemString)) or TSM.items[goldData.sales.topQuantity.itemString].name) or L["none"]
goldData.buys.topGold.link = goldData.buys.topGold.itemString and (select(2, TSMAPI:GetSafeItemInfo(goldData.buys.topGold.itemString)) or TSM.items[goldData.buys.topGold.itemString].name) or L["none"]
goldData.buys.topQuantity.link = goldData.buys.topQuantity.itemString and (select(2, TSMAPI:GetSafeItemInfo(goldData.buys.topQuantity.itemString)) or TSM.items[goldData.buys.topQuantity.itemString].name) or L["none"]
goldData.profit.total = ((goldData.sales.total + goldData.income.total) - (goldData.buys.total + goldData.expense.total))
goldData.profit.month = ((goldData.sales.month + goldData.income.month) - (goldData.buys.month + goldData.expense.month))
goldData.profit.week = ((goldData.sales.week + goldData.income.week) - (goldData.buys.week + goldData.expense.week))
if goldData.totalTime > (SECONDS_PER_DAY * 30) then
goldData.monthTime = SECONDS_PER_DAY * 30
end
if goldData.totalTime > (SECONDS_PER_DAY * 7) then
goldData.weekTime = SECONDS_PER_DAY * 7
end
goldData.totalTime = ceil(goldData.totalTime / SECONDS_PER_DAY)
goldData.monthTime = ceil(goldData.monthTime / SECONDS_PER_DAY)
goldData.weekTime = ceil(goldData.weekTime / SECONDS_PER_DAY)
return goldData
end
function Data.GetItemDetailData(itemString)
if not TSM.items[itemString] then return end
local data = {activity={}, buys={players={}, price={}, num={}, avg={}}, sales={players={}, price={}, num={}, avg={}}, stData={}}
local function ProcessItemActivity(itemData, resultTbl, activityType)
local totalPrice, totalNum = 0, 0
local monthPrice, monthNum = 0, 0
local weekPrice, weekNum = 0, 0
for i, record in ipairs(itemData) do
resultTbl.players[record.otherPlayer] = (resultTbl.players[record.otherPlayer] or 0) + record.quantity
tinsert(data.activity, {activityType=activityType, record=record})
totalPrice = totalPrice + record.copper * record.quantity
totalNum = totalNum + record.quantity
local timeDiff = time() - record.time
if timeDiff < (SECONDS_PER_DAY * 30) then
monthPrice = monthPrice + record.copper * record.quantity
monthNum = monthNum + record.quantity
end
if timeDiff < (SECONDS_PER_DAY * 7) then
weekPrice = weekPrice + record.copper * record.quantity
weekNum = weekNum + record.quantity
end
end
resultTbl.price.total = totalPrice
resultTbl.price.month = monthPrice
resultTbl.price.week = weekPrice
resultTbl.num.total = totalNum
resultTbl.num.month = monthNum
resultTbl.num.week = weekNum
resultTbl.avg.total = totalNum > 0 and TSM:Round(totalPrice / totalNum) or 0
resultTbl.avg.month = monthNum > 0 and TSM:Round(monthPrice / monthNum) or 0
resultTbl.avg.week = weekNum > 0 and TSM:Round(weekPrice / weekNum) or 0
if totalNum > 0 then
resultTbl.hasData = true
end
end
ProcessItemActivity(TSM.items[itemString].buys, data.buys, "Purchase")
ProcessItemActivity(TSM.items[itemString].sales, data.sales, "Sale")
for _, stRecord in ipairs(data.activity) do
local activityType = stRecord.activityType
local record = stRecord.record
local row = {
cols = {
{
value = activityType,
sortArg = activityType,
},
{
value = record.key,
sortArg = record.key or "",
},
{
value = record.otherPlayer,
sortArg = record.otherPlayer,
},
{
value = record.quantity,
sortArg = record.quantity,
},
{
value = TSMAPI:FormatTextMoney(record.copper),
sortArg = record.copper,
},
{
value = TSMAPI:FormatTextMoney(record.copper * record.quantity),
sortArg = record.copper * record.quantity,
},
{
value = private:GetFormattedTime(record.time),
sortArg = record.time,
},
},
record = record,
recordType = activityType,
}
tinsert(data.stData, row)
end
return data
end
function Data:PopulateDataCaches()
Data.playerListCache = {}
for itemString, data in pairs(TSM.items) do
for _, record in ipairs(data.buys) do
Data.playerListCache[record.player] = record.player
end
for _, record in ipairs(data.sales) do
Data.playerListCache[record.player] = record.player
end
for _, record in ipairs(data.auctions) do
Data.playerListCache[record.player] = record.player
end
end
for _, record in pairs(TSM.money.income) do
Data.playerListCache[record.player] = record.player
end
for _, record in pairs(TSM.money.expense) do
Data.playerListCache[record.player] = record.player
end
end
function Data:RemoveOldData(daysOld)
local cutOffTime = time() - daysOld * SECONDS_PER_DAY
local numRecords, numItems = 0, 0
for itemString, data in pairs(TSM.items) do
local numLeft = 0
for _, key in ipairs({"sales", "buys", "auctions"}) do
for i=#data[key], 1, -1 do
if data[key][i].time < cutOffTime then
numRecords = numRecords + 1
tremove(data[key], i)
end
end
numLeft = numLeft + #data[key]
end
if numLeft == 0 then
TSM.items[itemString] = nil
numItems = numItems + 1
end
end
for dataType, records in pairs(TSM.money) do
for i=#records, 1, -1 do
if records[i].time < cutOffTime then
numRecords = numRecords + 1
tremove(records, i)
end
end
end
TSM:Printf(L["Removed a total of %s old records and %s items with no remaining records."], numRecords, numItems)
end
function Data:SetupRepairCost()
REPAIR_MONEY = GetMoney();
COULD_REPAIR = CanMerchantRepair();
-- if merchant can repair set up variables so we can track repairs
if (COULD_REPAIR) then
REPAIR_COST, CAN_REPAIR = GetRepairAllCost();
end
end
function Data:ResetRepairMoney()
-- Could have bought something before or after repair
REPAIR_MONEY = GetMoney()
end
do
local tradeInfo
local function onAcceptUpdate(_, player, target)
if (player == 1 or target == 1) and not (GetTradePlayerItemLink(7) or GetTradeTargetItemLink(7)) then
-- update tradeInfo
tradeInfo = { player = {}, target = {} }
tradeInfo.player.money = tonumber(GetPlayerTradeMoney())
tradeInfo.target.money = tonumber(GetTargetTradeMoney())
tradeInfo.target.name = UnitName("NPC")
for i = 1, 6 do
local link = GetTradeTargetItemLink(i)
local count = select(3, GetTradeTargetItemInfo(i))
if link then
tinsert(tradeInfo.target, { itemString = TSMAPI:GetItemString(link), count = count })
end
local link = GetTradePlayerItemLink(i)
local count = select(3, GetTradePlayerItemInfo(i))
if link then
tinsert(tradeInfo.player, { itemString = TSMAPI:GetItemString(link), count = count })
end
end
else
tradeInfo = nil
end
end
local function onChatMsg(_, msg)
if not TSM.db.factionrealm.trackTrades then return
end
if msg == ERR_TRADE_COMPLETE and tradeInfo then
-- trade went through
local info
if tradeInfo.player.money > 0 and #tradeInfo.player == 0 and tradeInfo.target.money == 0 and #tradeInfo.target > 0 then
-- player bought items
local itemString, count
for i = 1, #tradeInfo.target do
local data = 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
if not itemString or not count then return
end
info = { type = "buys", itemString = itemString, count = count, price = tradeInfo.player.money / count }
info.gotText = select(2, TSMAPI:GetSafeItemInfo(itemString)) .. "x" .. count
info.gaveText = TSMAPI:FormatTextMoney(tradeInfo.player.money)
elseif tradeInfo.player.money == 0 and #tradeInfo.player > 0 and tradeInfo.target.money > 0 and #tradeInfo.target == 0 then
-- player sold items
local itemString, count
for i = 1, #tradeInfo.player do
local data = 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
if not itemString or not count then return
end
info = { type = "sales", itemString = itemString, count = count, price = tradeInfo.target.money / count }
info.gaveText = select(2, TSMAPI:GetSafeItemInfo(itemString)) .. "x" .. count
info.gotText = TSMAPI:FormatTextMoney(tradeInfo.target.money)
else
return
end
local function InsertTradeRecord()
if info.type == "sales" then
Data:InsertItemSaleRecord(info.itemString, "Trade", info.count, info.price, tradeInfo.target.name)
elseif info.type == "buys" then
Data:InsertItemBuyRecord(info.itemString, "Trade", info.count, info.price, tradeInfo.target.name)
end
end
if TSM.db.factionrealm.autoTrackTrades then
InsertTradeRecord()
else
StaticPopupDialogs["TSMAccountingOnTrade"] = {
text = format(L["TSM_Accounting detected that you just traded %s %s in return for %s. Would you like Accounting to store a record of this trade?"], tradeInfo.target.name, info.gaveText, info.gotText),
button1 = YES,
button2 = NO,
timeout = 0,
whileDead = true,
hideOnEscape = true,
OnAccept = InsertTradeRecord,
OnCancel = function() end,
}
TSMAPI:ShowStaticPopupDialog("TSMAccountingOnTrade")
end
end
end
Data:RegisterEvent("TRADE_ACCEPT_UPDATE", onAcceptUpdate)
Data:RegisterEvent("UI_INFO_MESSAGE", onChatMsg)
end
local lastTrackMinute = 0
function Data:LogGold()
local currentTime = time()
local currentMinute = floor(currentTime / 60)
if currentMinute <= lastTrackMinute then return end
local player = UnitName("player")
if not player then return end
lastTrackMinute = currentMinute
TSM.db.factionrealm.goldLog[player] = TSM.db.factionrealm.goldLog[player] or {}
local goldLog = TSM.db.factionrealm.goldLog[player]
local currentGold = TSM:Round(GetMoney(), COPPER_PER_GOLD * 1000)
if #goldLog > 0 and currentGold == goldLog[#goldLog].copper then
goldLog[#goldLog].endMinute = currentMinute
else
if #goldLog > 0 then
goldLog[#goldLog].endMinute = currentMinute - 1
end
tinsert(goldLog, { startMinute = currentMinute, endMinute = currentMinute, copper = currentGold })
end
end