|
|
|
|
-- ------------------------------------------------------------------------------ --
|
|
|
|
|
-- 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")
|
|
|
|
|
|
|
|
|
|
local CHANNEL_NAME = "TSM_AuctionDB"
|
|
|
|
|
local COMM_PREFIX = "TSMADB1"
|
|
|
|
|
local CHUNK_SIZE = 220
|
|
|
|
|
local BUNDLE_TIMEOUT = 45
|
|
|
|
|
local SEND_INTERVAL = 0.08
|
|
|
|
|
local MAX_TOTAL_CHUNKS = 800
|
|
|
|
|
|
|
|
|
|
local private = {
|
|
|
|
|
channelId = nil,
|
|
|
|
|
channelName = nil,
|
|
|
|
|
lastBroadcastHash = nil,
|
|
|
|
|
incoming = {},
|
|
|
|
|
sendQueue = {},
|
|
|
|
|
isSending = false,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
local strbyte = string.byte
|
|
|
|
|
|
|
|
|
|
local libS = LibStub:GetLibrary("AceSerializer-3.0")
|
|
|
|
|
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.")
|
|
|
|
|
end
|
|
|
|
|
return false
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function HashString(str)
|
|
|
|
|
local hash = 0
|
|
|
|
|
for i = 1, #str do
|
|
|
|
|
hash = (hash * 31 + strbyte(str, i)) % 4294967296
|
|
|
|
|
end
|
|
|
|
|
return hash
|
|
|
|
|
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
|
|
|
|
|
private.channelName = private.channelId and CHANNEL_NAME or nil
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function ChatFilter(_, _, msg, _, _, _, _, _, channelName)
|
|
|
|
|
if channelName ~= CHANNEL_NAME then return end
|
|
|
|
|
if strsub(msg, 1, #COMM_PREFIX) == COMM_PREFIX then
|
|
|
|
|
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")
|
|
|
|
|
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)
|
|
|
|
|
if not CanEncode() then return end
|
|
|
|
|
local serialized = libS:Serialize(payload)
|
|
|
|
|
local compressed = libD:CompressDeflate(serialized, { level = 9 })
|
|
|
|
|
if not compressed then return end
|
|
|
|
|
return libD:EncodeForPrint(compressed)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function DecodePayload(encoded)
|
|
|
|
|
if not CanEncode() then return end
|
|
|
|
|
local decoded = libD:DecodeForPrint(encoded)
|
|
|
|
|
if not decoded then return end
|
|
|
|
|
local decompressed = libD:DecompressDeflate(decoded)
|
|
|
|
|
if not decompressed then return end
|
|
|
|
|
local ok, payload = libS:Deserialize(decompressed)
|
|
|
|
|
if not ok then return end
|
|
|
|
|
return payload
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
function ChannelSync:BroadcastScanData(scanType)
|
|
|
|
|
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()
|
|
|
|
|
ChannelSync:BroadcastScanData(scanType)
|
|
|
|
|
end)
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
EnsureChannel()
|
|
|
|
|
if not private.channelName or not private.channelId then return end
|
|
|
|
|
|
|
|
|
|
TSM:Serialize()
|
|
|
|
|
local payload = {
|
|
|
|
|
scanTime = TSM.db.realm.lastCompleteScan,
|
|
|
|
|
scanData = TSM.db.realm.scanData,
|
|
|
|
|
}
|
|
|
|
|
local encoded = EncodePayload(payload)
|
|
|
|
|
if not encoded then return end
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
TSM:Print("AuctionDB channel sync payload too large; skipping broadcast.")
|
|
|
|
|
return
|
|
|
|
|
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
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function MergeIncomingData(payload)
|
|
|
|
|
if type(payload) ~= "table" or type(payload.scanData) ~= "string" then return end
|
|
|
|
|
|
|
|
|
|
local incoming = {}
|
|
|
|
|
TSM:Deserialize(payload.scanData, incoming, true)
|
|
|
|
|
for itemID, data in pairs(incoming) do
|
|
|
|
|
local existing = TSM.data[itemID]
|
|
|
|
|
if existing then
|
|
|
|
|
TSM:DecodeItemData(itemID)
|
|
|
|
|
if data.lastScan and (not existing.lastScan or data.lastScan > existing.lastScan) then
|
|
|
|
|
TSM.data[itemID] = data
|
|
|
|
|
end
|
|
|
|
|
else
|
|
|
|
|
TSM.data[itemID] = data
|
|
|
|
|
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()
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
function ChannelSync:OnChannelMessage(_, msg, source, _, _, _, _, _, channelName)
|
|
|
|
|
if channelName ~= CHANNEL_NAME then return end
|
|
|
|
|
if strsub(msg or "", 1, #COMM_PREFIX) ~= COMM_PREFIX then return end
|
|
|
|
|
|
|
|
|
|
source = ("-"):split(source or "")
|
|
|
|
|
if strlower(source or "") == strlower(UnitName("player") or "") then return end
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
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
|