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.

339 lines
9.6 KiB

4 months ago
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster_AuctionDB --
-- http://www.curse.com/addons/wow/tradeskillmaster_auctiondb --
-- --
-- A TradeSkillMaster Addon (http://tradeskillmaster.com) --
-- All Rights Reserved* - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
-- Channel sync for sharing AuctionDB scan data between players.
local TSM = select(2, ...)
local ChannelSync = TSM:NewModule("ChannelSync", "AceEvent-3.0")
4 months ago
local CHANNEL_NAME = "TSM_AuctionDB"
4 months ago
local COMM_PREFIX = "TSMADB1"
local CHUNK_SIZE = 220
4 months ago
local BUNDLE_TIMEOUT = 45
local SEND_INTERVAL = 0.08
local MAX_TOTAL_CHUNKS = 800
local MAX_ITEMS_PER_PAYLOAD = 250
4 months ago
local private = {
channelId = nil,
4 months ago
channelName = nil,
4 months ago
lastBroadcastHash = nil,
incoming = {},
sendQueue = {},
isSending = false,
4 months ago
}
local strbyte = string.byte
local libS = LibStub:GetLibrary("AceSerializer-3.0")
4 months ago
local libD = LibStub("LibDeflate", true)
local warnedNoDeflate
local function CanEncode()
if libD then return true end
if not warnedNoDeflate then
warnedNoDeflate = true
TSM:Print("AuctionDB channel sync requires LibDeflate. Install or enable an addon that provides it.")
4 months ago
end
4 months ago
return false
4 months ago
end
4 months ago
local function HashString(str)
local hash = 0
for i = 1, #str do
hash = (hash * 31 + strbyte(str, i)) % 4294967296
end
4 months ago
return hash
4 months ago
end
local function EnsureChannel()
local channelId = GetChannelName(CHANNEL_NAME)
if channelId == 0 then
JoinChannelByName(CHANNEL_NAME)
channelId = GetChannelName(CHANNEL_NAME)
end
private.channelId = channelId > 0 and channelId or nil
4 months ago
private.channelName = private.channelId and CHANNEL_NAME or nil
4 months ago
end
local function ChatFilter(_, _, msg, _, _, _, _, _, channelName)
if channelName ~= CHANNEL_NAME then return end
4 months ago
if strsub(msg, 1, #COMM_PREFIX) == COMM_PREFIX then
4 months ago
return true
end
end
function ChannelSync:OnEnable()
self:RegisterEvent("PLAYER_ENTERING_WORLD", "OnPlayerEnteringWorld")
self:RegisterEvent("CHAT_MSG_CHANNEL_NOTICE", "OnChannelNotice")
self:RegisterEvent("CHAT_MSG_CHANNEL", "OnChannelMessage")
4 months ago
if ChatFrame_AddMessageEventFilter then
ChatFrame_AddMessageEventFilter("CHAT_MSG_CHANNEL", ChatFilter)
end
EnsureChannel()
end
function ChannelSync:OnPlayerEnteringWorld()
EnsureChannel()
end
function ChannelSync:OnChannelNotice()
EnsureChannel()
end
local function EncodePayload(payload)
4 months ago
if not CanEncode() then return end
4 months ago
local serialized = libS:Serialize(payload)
local compressed = libD:CompressDeflate(serialized, { level = 9 })
4 months ago
if not compressed then return end
return libD:EncodeForPrint(compressed)
4 months ago
end
local function DecodePayload(encoded)
4 months ago
if not CanEncode() then return end
local decoded = libD:DecodeForPrint(encoded)
4 months ago
if not decoded then return end
4 months ago
local decompressed = libD:DecompressDeflate(decoded)
4 months ago
if not decompressed then return end
local ok, payload = libS:Deserialize(decompressed)
if not ok then return end
return payload
end
4 months ago
function ChannelSync:BroadcastScanData(scanType, items)
4 months ago
if scanType ~= "Full" and scanType ~= "GetAll" and scanType ~= "Group" and scanType ~= "Search" then
return
end
if TSM.processingData then
TSMAPI:CreateTimeDelay("auctionDBChannelSyncBroadcast", 0.5, function()
4 months ago
ChannelSync:BroadcastScanData(scanType, items)
4 months ago
end)
return
end
EnsureChannel()
if not private.channelName or not private.channelId then return end
4 months ago
local itemIDs = ChannelSync:CollectItemIDs(items)
if not itemIDs or #itemIDs == 0 then return end
local function QueueEncoded(encoded)
local hash = HashString(encoded)
if hash == private.lastBroadcastHash then return end
private.lastBroadcastHash = hash
local total = ceil(#encoded / CHUNK_SIZE)
if total > MAX_TOTAL_CHUNKS then return false end
tinsert(private.sendQueue, {hash = hash, encoded = encoded, total = total})
if not private.isSending then
private.isSending = true
TSMAPI.Threading:Start(ChannelSync.SendQueueThread, 0.3)
end
return true
end
local function QueueBatch(startIndex, endIndex)
local batch = {}
for i = startIndex, endIndex do
tinsert(batch, itemIDs[i])
end
local payload = ChannelSync:BuildPayloadFromItemIDs(scanType, batch)
if not payload then return end
local encoded = EncodePayload(payload)
if not encoded then return end
local total = ceil(#encoded / CHUNK_SIZE)
if total > MAX_TOTAL_CHUNKS and #batch > 1 then
local mid = floor((startIndex + endIndex) / 2)
QueueBatch(startIndex, mid)
QueueBatch(mid + 1, endIndex)
return
end
if total > MAX_TOTAL_CHUNKS then
TSM:Print("AuctionDB channel sync payload too large; skipping batch.")
return
end
QueueEncoded(encoded)
end
for i = 1, #itemIDs, MAX_ITEMS_PER_PAYLOAD do
QueueBatch(i, min(i + MAX_ITEMS_PER_PAYLOAD - 1, #itemIDs))
4 months ago
end
end
function ChannelSync:CollectItemIDs(items)
local itemIDs, list = {}, {}
4 months ago
if items then
if #items > 0 then
for _, itemString in ipairs(items) do
local itemID = TSMAPI:GetItemID(itemString)
if itemID and not itemIDs[itemID] then
4 months ago
itemIDs[itemID] = true
tinsert(list, itemID)
4 months ago
end
end
else
for itemString in pairs(items) do
local itemID = TSMAPI:GetItemID(itemString)
if itemID and not itemIDs[itemID] then
4 months ago
itemIDs[itemID] = true
tinsert(list, itemID)
4 months ago
end
end
end
else
local scanTime = TSM.db.realm.lastCompleteScan
for itemID in pairs(TSM.data) do
TSM:DecodeItemData(itemID)
if TSM.data[itemID].lastScan == scanTime then
tinsert(list, itemID)
4 months ago
end
end
end
return list
end
4 months ago
function ChannelSync:BuildPayloadFromItemIDs(scanType, itemIDList)
4 months ago
local payloadItems = {}
for _, itemID in ipairs(itemIDList) do
4 months ago
local data = TSM.data[itemID]
if data then
TSM:EncodeItemData(itemID)
if data.encoded then
payloadItems[itemID] = data.encoded
end
end
end
if not next(payloadItems) then return end
return {
v = 1,
scanType = scanType,
scanTime = TSM.db.realm.lastCompleteScan,
items = payloadItems,
}
end
local function DecodeIncomingItem(itemID, encoded)
local tmp = {}
tmp[itemID] = {encoded = encoded}
TSM:DecodeItemData(itemID, tmp)
return tmp[itemID]
end
local function MergeIncomingData(payload, sender)
4 months ago
if type(payload) ~= "table" then return end
4 months ago
local incoming = {}
4 months ago
if payload.items then
incoming = payload.items
elseif type(payload.scanData) == "string" then
TSM:Deserialize(payload.scanData, incoming, true)
else
return
end
local updated = 0
local lastItemID
4 months ago
for itemID, data in pairs(incoming) do
4 months ago
local incomingData = data
if payload.items then
incomingData = DecodeIncomingItem(itemID, data)
end
if incomingData then
local existing = TSM.data[itemID]
if existing then
TSM:DecodeItemData(itemID)
if incomingData.lastScan and (not existing.lastScan or incomingData.lastScan > existing.lastScan) then
TSM.data[itemID] = incomingData
updated = updated + 1
lastItemID = itemID
end
else
TSM.data[itemID] = incomingData
updated = updated + 1
lastItemID = itemID
4 months ago
end
end
end
if payload.scanTime and (not TSM.db.realm.lastCompleteScan or payload.scanTime > TSM.db.realm.lastCompleteScan) then
TSM.db.realm.lastCompleteScan = payload.scanTime
end
TSM:Serialize()
if updated > 0 and sender then
if updated == 1 and lastItemID then
local link = select(2, GetItemInfo(lastItemID)) or ("item:" .. tostring(lastItemID))
TSM:Printf("AuctionDB updated %s from %s.", link, sender)
else
TSM:Printf("AuctionDB updated %d items from %s.", updated, sender)
end
end
4 months ago
end
function ChannelSync:OnChannelMessage(_, msg, source, _, _, _, _, _, channelName)
if channelName ~= CHANNEL_NAME then return end
4 months ago
if strsub(msg or "", 1, #COMM_PREFIX) ~= COMM_PREFIX then return end
4 months ago
source = ("-"):split(source or "")
if strlower(source or "") == strlower(UnitName("player") or "") then return end
4 months ago
local headerLen = #COMM_PREFIX + 16
if #msg <= headerLen then return end
local meta = strsub(msg, #COMM_PREFIX + 1, headerLen)
local chunk = strsub(msg, headerLen + 1)
local hashHex = strsub(meta, 1, 8)
local seqHex = strsub(meta, 9, 12)
local totalHex = strsub(meta, 13, 16)
local hash = tonumber(hashHex, 16)
local seq = tonumber(seqHex, 16)
local total = tonumber(totalHex, 16)
if not hash or not seq or not total then return end
4 months ago
local bundle = private.incoming[hash]
if not bundle or (time() - bundle.time) > BUNDLE_TIMEOUT then
bundle = {time = time(), total = total, chunks = {}, received = 0}
private.incoming[hash] = bundle
end
if bundle.total ~= total then
private.incoming[hash] = {time = time(), total = total, chunks = {}, received = 0}
bundle = private.incoming[hash]
end
if not bundle.chunks[seq] then
bundle.chunks[seq] = chunk
bundle.received = bundle.received + 1
end
if bundle.received < bundle.total then return end
local parts = {}
for i = 1, bundle.total do
if not bundle.chunks[i] then return end
tinsert(parts, bundle.chunks[i])
end
private.incoming[hash] = nil
local payload = DecodePayload(table.concat(parts))
if not payload then return end
MergeIncomingData(payload, source)
4 months ago
end
function ChannelSync:SendQueueThread()
while #private.sendQueue > 0 do
local job = private.sendQueue[1]
for i = 1, job.total do
local chunk = strsub(job.encoded, (i - 1) * CHUNK_SIZE + 1, i * CHUNK_SIZE)
local msg = COMM_PREFIX .. string.format("%08x%04x%04x", job.hash, i, job.total) .. chunk
SendChatMessage(msg, "CHANNEL", nil, private.channelId)
self:Sleep(SEND_INTERVAL)
end
tremove(private.sendQueue, 1)
end
private.isSending = false
end