-- ------------------------------------------------------------------------------ -- -- 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.realm.saveTimeSales, ",") elseif recordType == "buys" then saveTimes = TSMAPI:SafeStrSplit(TSM.db.realm.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.realm.csvSales, "sales") private:LoadItemRecords(TSM.db.realm.csvBuys, "buys") private:LoadItemRecords(TSM.db.realm.csvCancelled, "auctions", "Cancel") private:LoadItemRecords(TSM.db.realm.csvExpired, "auctions", "Expire") -- Decode money records TSM.money = {} private:LoadMoneyRecords(TSM.db.realm.csvIncome, "income") private:LoadMoneyRecords(TSM.db.realm.csvExpense, "expense") -- Decode the gold log for player, data in pairs(TSM.db.realm.goldLog) do if type(data) == "string" then TSM.db.realm.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 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) local quantity = select(11, GetInboxInvoiceInfo(index)) or 1 -- WotLK doesn't provide quantity sold by default, Ascension does 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, 1) local itemString = TSMAPI:GetItemString(link) local quantity = select(3, GetInboxItem(index, 1)) 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)) Data:InsertItemBuyRecord(itemString, "COD", total, codAmount, sender, buyTime) --[[ 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.realm.timeFormat == "ago" then return format(L["%s ago"], SecondsToTime(time() - rTime) or "?") elseif TSM.db.realm.timeFormat == "usdate" then return date("%m/%d/%y %H:%M", rTime) elseif TSM.db.realm.timeFormat == "eudate" then return date("%d/%m/%y %H:%M", rTime) elseif TSM.db.realm.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.realm.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.realm.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.realm.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.realm.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.realm.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.realm.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.realm.goldLog[player] = TSM.db.realm.goldLog[player] or {} local goldLog = TSM.db.realm.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