initial commit
This commit is contained in:
134
LibTSM/Service/SyncClasses/Comm.lua
Normal file
134
LibTSM/Service/SyncClasses/Comm.lua
Normal file
@@ -0,0 +1,134 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Comm = TSM.Init("Service.SyncClasses.Comm")
|
||||
local Delay = TSM.Include("Util.Delay")
|
||||
local Table = TSM.Include("Util.Table")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local Settings = TSM.Include("Service.Settings")
|
||||
local Constants = TSM.Include("Service.SyncClasses.Constants")
|
||||
local private = {
|
||||
handler = {},
|
||||
queuedPacket = {},
|
||||
queuedSourceCharacter = {},
|
||||
}
|
||||
-- load libraries
|
||||
LibStub("AceComm-3.0"):Embed(Comm)
|
||||
local LibSerialize = LibStub("LibSerialize")
|
||||
local LibDeflate = LibStub("LibDeflate")
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
Comm:OnModuleLoad(function()
|
||||
Comm:RegisterComm("TSMSyncData", private.OnCommReceived)
|
||||
end)
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Comm.RegisterHandler(dataType, handler)
|
||||
assert(Table.KeyByValue(Constants.DATA_TYPES, dataType) ~= nil)
|
||||
assert(not private.handler[dataType])
|
||||
private.handler[dataType] = handler
|
||||
end
|
||||
|
||||
function Comm.SendData(dataType, targetCharacter, data)
|
||||
assert(type(dataType) == "string" and #dataType == 1)
|
||||
local packet = TempTable.Acquire()
|
||||
packet.dt = dataType
|
||||
packet.sa = Settings.GetCurrentSyncAccountKey()
|
||||
packet.v = Constants.VERSION
|
||||
packet.d = data
|
||||
local serialized = LibSerialize:Serialize(packet)
|
||||
TempTable.Release(packet)
|
||||
local compressed = LibDeflate:EncodeForWoWAddonChannel(LibDeflate:CompressDeflate(serialized))
|
||||
assert(LibDeflate:DecompressDeflate(LibDeflate:DecodeForWoWAddonChannel(compressed)) == serialized)
|
||||
|
||||
-- give heartbeats and rpc preambles a higher priority
|
||||
local priority = (dataType == Constants.DATA_TYPES.HEARTBEAT or dataType == Constants.DATA_TYPES.RPC_PREAMBLE) and "ALERT" or nil
|
||||
-- send the message
|
||||
Comm:SendCommMessage("TSMSyncData", compressed, "WHISPER", targetCharacter, priority)
|
||||
return #compressed
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.OnCommReceived(_, packet, _, sourceCharacter)
|
||||
-- delay the processing to make sure it happens within a debuggable context (this function is called via pcall)
|
||||
tinsert(private.queuedPacket, packet)
|
||||
tinsert(private.queuedSourceCharacter, sourceCharacter)
|
||||
Delay.AfterFrame("commReceiveQueue", 0, private.ProcessReceiveQueue)
|
||||
end
|
||||
|
||||
function private.ProcessReceiveQueue()
|
||||
assert(#private.queuedPacket == #private.queuedSourceCharacter)
|
||||
while #private.queuedPacket > 0 do
|
||||
local packet = tremove(private.queuedPacket, 1)
|
||||
local sourceCharacter = tremove(private.queuedSourceCharacter, 1)
|
||||
private.ProcessReceivedPacket(packet, sourceCharacter)
|
||||
end
|
||||
end
|
||||
|
||||
function private.ProcessReceivedPacket(msg, sourceCharacter)
|
||||
-- remove realm name from source player
|
||||
sourceCharacter = strsplit("-", sourceCharacter)
|
||||
sourceCharacter = strtrim(sourceCharacter)
|
||||
local sourceCharacterAccountKey = Settings.GetCharacterSyncAccountKey(sourceCharacter)
|
||||
if sourceCharacterAccountKey and sourceCharacterAccountKey == Settings.GetCurrentSyncAccountKey() then
|
||||
Log.Err("We own the source character")
|
||||
Settings.ShowSyncSVCopyError()
|
||||
return
|
||||
end
|
||||
|
||||
-- decode and decompress
|
||||
msg = LibDeflate:DecompressDeflate(LibDeflate:DecodeForWoWAddonChannel(msg))
|
||||
if not msg then
|
||||
Log.Err("Invalid packet")
|
||||
return
|
||||
end
|
||||
local success, packet = LibSerialize:Deserialize(msg)
|
||||
if not success then
|
||||
Log.Err("Invalid packet")
|
||||
return
|
||||
end
|
||||
|
||||
-- validate the packet
|
||||
local dataType = packet.dt
|
||||
local sourceAccount = packet.sa
|
||||
local version = packet.v
|
||||
local data = packet.d
|
||||
if type(dataType) ~= "string" or #dataType > 1 or not sourceAccount or version ~= Constants.VERSION then
|
||||
Log.Info("Invalid message received")
|
||||
return
|
||||
elseif sourceAccount == Settings.GetCurrentSyncAccountKey() then
|
||||
Log.Err("We are the source account (SV copy)")
|
||||
Settings.ShowSyncSVCopyError()
|
||||
return
|
||||
elseif sourceCharacterAccountKey and sourceCharacterAccountKey ~= sourceAccount then
|
||||
-- the source player now belongs to a different account than what we expect
|
||||
Log.Err("Unexpected source account")
|
||||
Settings.ShowSyncSVCopyError()
|
||||
return
|
||||
end
|
||||
|
||||
if private.handler[dataType] then
|
||||
private.handler[dataType](dataType, sourceAccount, sourceCharacter, data)
|
||||
else
|
||||
Log.Info("Received unhandled message of type: "..strbyte(dataType))
|
||||
end
|
||||
end
|
||||
443
LibTSM/Service/SyncClasses/Connection.lua
Normal file
443
LibTSM/Service/SyncClasses/Connection.lua
Normal file
@@ -0,0 +1,443 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Connection = TSM.Init("Service.SyncClasses.Connection")
|
||||
local L = TSM.Include("Locale").GetTable()
|
||||
local Delay = TSM.Include("Util.Delay")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Event = TSM.Include("Util.Event")
|
||||
local Settings = TSM.Include("Service.Settings")
|
||||
local Threading = TSM.Include("Service.Threading")
|
||||
local Constants = TSM.Include("Service.SyncClasses.Constants")
|
||||
local Comm = TSM.Include("Service.SyncClasses.Comm")
|
||||
local private = {
|
||||
isActive = false,
|
||||
hasFriendsInfo = false,
|
||||
newCharacter = nil,
|
||||
newAccount = nil,
|
||||
newSyncAcked = nil,
|
||||
connectionChangedCallbacks = {},
|
||||
threadId = {},
|
||||
threadRunning = {},
|
||||
connectedCharacter = {},
|
||||
lastHeartbeat = {},
|
||||
suppressThreadTime = {},
|
||||
connectionRequestReceived = {},
|
||||
addedFriends = {},
|
||||
invalidCharacters = {},
|
||||
}
|
||||
local RECEIVE_TIMEOUT = 5
|
||||
local HEARTBEAT_TIMEOUT = 10
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
Connection:OnSettingsLoad(function()
|
||||
Event.Register("CHAT_MSG_SYSTEM", private.ChatMsgSystemEventHandler)
|
||||
Event.Register("FRIENDLIST_UPDATE", private.PrepareFriendsInfo)
|
||||
for _ in Settings.SyncAccountIterator() do
|
||||
private.isActive = true
|
||||
end
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.WHOAMI_ACCOUNT, private.WhoAmIAccountHandler)
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.WHOAMI_ACK, private.WhoAmIAckHandler)
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.CONNECTION_REQUEST, private.ConnectionHandler)
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.CONNECTION_REQUEST_ACK, private.ConnectionHandler)
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.DISCONNECT, private.DisconnectHandler)
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.HEARTBEAT, private.HeartbeatHandler)
|
||||
|
||||
private.PrepareFriendsInfo()
|
||||
end)
|
||||
|
||||
Connection:OnModuleUnload(function()
|
||||
for _, player in pairs(private.connectedCharacter) do
|
||||
Comm.SendData(Constants.DATA_TYPES.DISCONNECT, player)
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Connection.RegisterConnectionChangedCallback(handler)
|
||||
tinsert(private.connectionChangedCallbacks, handler)
|
||||
end
|
||||
|
||||
function Connection.IsCharacterConnected(targetCharacter)
|
||||
for _, player in pairs(private.connectedCharacter) do
|
||||
if player == targetCharacter then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function Connection.ConnectedAccountIterator()
|
||||
return pairs(private.connectedCharacter)
|
||||
end
|
||||
|
||||
function Connection.Establish(targetCharacter)
|
||||
if not private.hasFriendsInfo then
|
||||
Log.PrintUser(L["TSM is not yet ready to establish a new sync connection. Please try again later."])
|
||||
return false
|
||||
end
|
||||
local wasFriend = C_FriendList.GetFriendInfo(targetCharacter) and true or false
|
||||
if strlower(targetCharacter) == strlower(UnitName("player")) then
|
||||
Log.PrintUser(L["Sync Setup Error: You entered the name of the current character and not the character on the other account."])
|
||||
return false
|
||||
elseif not private.IsOnline(targetCharacter) and wasFriend then
|
||||
Log.PrintUser(L["Sync Setup Error: The specified player on the other account is not currently online."])
|
||||
return false
|
||||
end
|
||||
local invalidCharacter = false
|
||||
for _, player in Settings.CharacterByFactionrealmIterator() do
|
||||
if strlower(player) == strlower(targetCharacter) then
|
||||
invalidCharacter = true
|
||||
end
|
||||
end
|
||||
if invalidCharacter then
|
||||
Log.PrintUser(L["Sync Setup Error: This character is already part of a known account."])
|
||||
return false
|
||||
end
|
||||
if not private.isActive then
|
||||
private.isActive = true
|
||||
Delay.AfterTime("SYNC_CONNECTION_MANAGEMENT", 1, private.ManagementLoop, 1)
|
||||
end
|
||||
private.newCharacter = targetCharacter
|
||||
private.newAccount = nil
|
||||
private.newSyncAcked = nil
|
||||
Delay.Cancel("syncNewAccount")
|
||||
Delay.AfterTime("syncNewAccount", 0, private.SendNewAccountWhoAmI, 1)
|
||||
return true
|
||||
end
|
||||
|
||||
function Connection.GetNewAccountStatus()
|
||||
if not private.newCharacter then
|
||||
return nil
|
||||
end
|
||||
return format(L["Connecting to %s"], private.newCharacter)
|
||||
end
|
||||
|
||||
function Connection.GetStatus(account)
|
||||
if private.connectedCharacter[account] then
|
||||
return true, private.connectedCharacter[account]
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
function Connection.Remove(account)
|
||||
if private.threadRunning[account] then
|
||||
Threading.Kill(private.threadId[account])
|
||||
private.ConnectionThreadDone(account)
|
||||
end
|
||||
Settings.RemoveSyncAccount(account)
|
||||
end
|
||||
|
||||
function Connection.GetConnectedCharacterByAccount(account)
|
||||
return private.connectedCharacter[account]
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Message Handlers
|
||||
-- ============================================================================
|
||||
|
||||
function private.WhoAmIAckHandler(dataType, sourceAccount, sourceCharacter, data)
|
||||
assert(dataType == Constants.DATA_TYPES.WHOAMI_ACK)
|
||||
if not private.newCharacter or strlower(private.newCharacter) ~= strlower(sourceCharacter) then
|
||||
-- we aren't trying to connect with a new account
|
||||
return
|
||||
end
|
||||
Log.Info("WHOAMI_ACK '%s'", tostring(private.newCharacter))
|
||||
private.newSyncAcked = true
|
||||
private.CheckNewAccountStatus()
|
||||
end
|
||||
|
||||
function private.WhoAmIAccountHandler(dataType, sourceAccount, sourceCharacter, data)
|
||||
assert(dataType == Constants.DATA_TYPES.WHOAMI_ACCOUNT)
|
||||
if not private.newCharacter then
|
||||
-- we aren't trying to connect with a new account
|
||||
return
|
||||
elseif strlower(private.newCharacter) ~= strlower(sourceCharacter) then
|
||||
Log.Info("WHOAMI_ACCOUNT from unknown player \"%s\", expected \"%s\"", private.newCharacter, sourceCharacter)
|
||||
return
|
||||
end
|
||||
private.newCharacter = sourceCharacter -- get correct capatilization
|
||||
private.newAccount = sourceAccount
|
||||
Log.Info("WHOAMI_ACCOUNT '%s' '%s'", private.newCharacter, private.newAccount)
|
||||
Comm.SendData(Constants.DATA_TYPES.WHOAMI_ACK, private.newCharacter)
|
||||
private.CheckNewAccountStatus()
|
||||
end
|
||||
|
||||
function private.ConnectionHandler(dataType, sourceAccount, sourceCharacter, data)
|
||||
if not private.threadRunning[sourceAccount] then
|
||||
return
|
||||
end
|
||||
private.connectionRequestReceived[sourceAccount] = true
|
||||
end
|
||||
|
||||
function private.DisconnectHandler(dataType, sourceAccount, sourceCharacter, data)
|
||||
assert(dataType == Constants.DATA_TYPES.DISCONNECT)
|
||||
if not private.threadRunning[sourceAccount] then
|
||||
return
|
||||
end
|
||||
|
||||
-- kill the thread and prevent it from running again for 2 seconds
|
||||
Threading.Kill(private.threadId[sourceAccount])
|
||||
private.ConnectionThreadDone(sourceAccount)
|
||||
private.suppressThreadTime[sourceAccount] = time() + 2
|
||||
end
|
||||
|
||||
function private.HeartbeatHandler(dataType, sourceAccount, sourceCharacter)
|
||||
assert(dataType == Constants.DATA_TYPES.HEARTBEAT)
|
||||
if not Connection.IsCharacterConnected(sourceCharacter) then
|
||||
-- we're not connected to this player
|
||||
return
|
||||
end
|
||||
private.lastHeartbeat[sourceAccount] = time()
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Management Loop / Sync Thread
|
||||
-- ============================================================================
|
||||
|
||||
function private.RequestFriendsInfo()
|
||||
C_FriendList.ShowFriends()
|
||||
end
|
||||
|
||||
function private.PrepareFriendsInfo()
|
||||
-- wait for friend info to populate
|
||||
local isValid
|
||||
local num = C_FriendList.GetNumFriends()
|
||||
if not num then
|
||||
isValid = false
|
||||
else
|
||||
isValid = true
|
||||
end
|
||||
for i = 1, num or 0 do
|
||||
if not C_FriendList.GetFriendInfoByIndex(i) then
|
||||
isValid = false
|
||||
break
|
||||
end
|
||||
end
|
||||
if isValid then
|
||||
if not private.hasFriendsInfo and private.isActive then
|
||||
-- start the management loop
|
||||
Delay.AfterTime("SYNC_CONNECTION_MANAGEMENT", 1, private.ManagementLoop, 1)
|
||||
end
|
||||
private.hasFriendsInfo = true
|
||||
else
|
||||
-- try again
|
||||
Log.Err("Missing friends info - will try again")
|
||||
Delay.AfterTime("SYNC_PREPARE_FRIENDS_INFO", 0.5, private.RequestFriendsInfo)
|
||||
end
|
||||
end
|
||||
|
||||
function private.ManagementLoop()
|
||||
-- continuously spawn connection threads with online players as necessary
|
||||
private.RequestFriendsInfo()
|
||||
local hasAccount = false
|
||||
for _, account in Settings.SyncAccountIterator() do
|
||||
hasAccount = true
|
||||
local targetCharacter = private.GetTargetCharacter(account)
|
||||
if targetCharacter then
|
||||
if not private.threadId[account] then
|
||||
private.threadId[account] = Threading.New("SYNC_"..strmatch(account, "(%d+)$"), private.ConnectionThread)
|
||||
end
|
||||
if not private.threadRunning[account] and (private.suppressThreadTime[account] or 0) < time() then
|
||||
private.threadRunning[account] = true
|
||||
Threading.Start(private.threadId[account], account, targetCharacter)
|
||||
end
|
||||
end
|
||||
end
|
||||
if not hasAccount then
|
||||
Log.Info("No more sync accounts.")
|
||||
private.isActive = false
|
||||
if not private.newCharacter then
|
||||
Delay.Cancel("SYNC_CONNECTION_MANAGEMENT")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function private.ConnectionThreadInner(account, targetCharacter)
|
||||
-- for the initial handshake, the lower account key is the server, other is the client - after this it doesn't matter
|
||||
-- add some randomness to the timeout so we don't get stuck in a race condition
|
||||
local timeout = GetTime() + RECEIVE_TIMEOUT + random(0, 1000) / 1000
|
||||
if account < Settings.GetCurrentSyncAccountKey() then
|
||||
-- wait for the connection request from the client
|
||||
while not private.connectionRequestReceived[account] do
|
||||
if GetTime() > timeout then
|
||||
-- timed out on the connection - don't try again for a bit
|
||||
Log.Warn("Timed out")
|
||||
return
|
||||
end
|
||||
Threading.Yield(true)
|
||||
end
|
||||
-- send an connection request ACK back to the client
|
||||
Comm.SendData(Constants.DATA_TYPES.CONNECTION_REQUEST_ACK, targetCharacter)
|
||||
else
|
||||
-- send a connection request to the server
|
||||
Comm.SendData(Constants.DATA_TYPES.CONNECTION_REQUEST, targetCharacter)
|
||||
-- wait for the connection request ACK
|
||||
while not private.connectionRequestReceived[account] do
|
||||
if GetTime() > timeout then
|
||||
-- timed out on the connection - don't try again for a bit
|
||||
Log.Warn("Timed out")
|
||||
private.suppressThreadTime[account] = time() + RECEIVE_TIMEOUT
|
||||
return
|
||||
end
|
||||
Threading.Yield(true)
|
||||
end
|
||||
end
|
||||
|
||||
-- we are now connected
|
||||
Log.Info("Connected to: %s %s", account, targetCharacter)
|
||||
private.connectedCharacter[account] = targetCharacter
|
||||
private.lastHeartbeat[account] = time()
|
||||
for _, callback in ipairs(private.connectionChangedCallbacks) do
|
||||
callback(account, targetCharacter, true)
|
||||
end
|
||||
|
||||
-- now that we are connected, data can flow in both directions freely
|
||||
local lastHeartbeatSend = time()
|
||||
while true do
|
||||
-- check if they either logged off or the heartbeats have timed-out
|
||||
if not private.IsOnline(targetCharacter, true) or time() - private.lastHeartbeat[account] > HEARTBEAT_TIMEOUT then
|
||||
return
|
||||
end
|
||||
|
||||
-- check if we should send a heartbeat
|
||||
if time() - lastHeartbeatSend > floor(HEARTBEAT_TIMEOUT / 2) then
|
||||
Comm.SendData(Constants.DATA_TYPES.HEARTBEAT, targetCharacter)
|
||||
lastHeartbeatSend = time()
|
||||
end
|
||||
|
||||
Threading.Yield(true)
|
||||
end
|
||||
end
|
||||
|
||||
function private.ConnectionThread(account, targetCharacter)
|
||||
private.ConnectionThreadInner(account, targetCharacter)
|
||||
private.ConnectionThreadDone(account)
|
||||
end
|
||||
|
||||
function private.ConnectionThreadDone(account)
|
||||
Log.Info("Connection ended to %s", account)
|
||||
local player = private.connectedCharacter[account]
|
||||
private.connectedCharacter[account] = nil
|
||||
if player then
|
||||
for _, callback in ipairs(private.connectionChangedCallbacks) do
|
||||
callback(account, player, false)
|
||||
end
|
||||
end
|
||||
private.threadRunning[account] = nil
|
||||
private.connectionRequestReceived[account] = nil
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.SendNewAccountWhoAmI()
|
||||
if not private.newCharacter then
|
||||
Delay.Cancel("syncNewAccount")
|
||||
elseif not C_FriendList.GetFriendInfo(private.newCharacter) then
|
||||
Log.Info("Waiting for friends list to update")
|
||||
elseif not private.IsOnline(private.newCharacter) then
|
||||
Delay.Cancel("syncNewAccount")
|
||||
private.newCharacter = nil
|
||||
private.newAccount = nil
|
||||
private.newSyncAcked = nil
|
||||
Log.Err("New player went offline")
|
||||
else
|
||||
Comm.SendData(Constants.DATA_TYPES.WHOAMI_ACCOUNT, private.newCharacter)
|
||||
Log.Info("Sent WHOAMI_ACCOUNT")
|
||||
end
|
||||
end
|
||||
|
||||
function private.CheckNewAccountStatus()
|
||||
if not private.newCharacter or not private.newAccount or not private.newSyncAcked then
|
||||
return
|
||||
end
|
||||
Log.Info("New sync character: '%s' '%s'", private.newCharacter, private.newAccount)
|
||||
-- the other account ACK'd so setup a connection
|
||||
Settings.NewSyncCharacter(private.newAccount, private.newCharacter)
|
||||
|
||||
-- call the callbacks for this new account
|
||||
for _, callback in ipairs(private.connectionChangedCallbacks) do
|
||||
callback(private.newAccount, private.newCharacter, nil)
|
||||
end
|
||||
|
||||
private.newCharacter = nil
|
||||
private.newAccount = nil
|
||||
private.newSyncAcked = nil
|
||||
end
|
||||
|
||||
function private.GetTargetCharacter(account)
|
||||
local tempTbl = TempTable.Acquire()
|
||||
for _, character in Settings.CharacterByAccountFactionrealmIterator(account) do
|
||||
tinsert(tempTbl, character)
|
||||
end
|
||||
|
||||
-- find the player to connect to without adding to the friends list
|
||||
for _, player in ipairs(tempTbl) do
|
||||
if private.IsOnline(player, true) then
|
||||
TempTable.Release(tempTbl)
|
||||
return player
|
||||
end
|
||||
end
|
||||
-- if we failed, try again with adding to friends list
|
||||
for _, player in ipairs(tempTbl) do
|
||||
if private.IsOnline(player) then
|
||||
TempTable.Release(tempTbl)
|
||||
return player
|
||||
end
|
||||
end
|
||||
TempTable.Release(tempTbl)
|
||||
end
|
||||
|
||||
function private.IsOnline(target, noAdd)
|
||||
local info = C_FriendList.GetFriendInfo(target)
|
||||
if not info and not noAdd and not private.invalidCharacters[strlower(target)] and C_FriendList.GetNumFriends() ~= 50 then
|
||||
-- add them as a friend
|
||||
C_FriendList.AddFriend(target)
|
||||
private.RequestFriendsInfo()
|
||||
tinsert(private.addedFriends, target)
|
||||
info = C_FriendList.GetFriendInfo(target)
|
||||
end
|
||||
return info and info.connected or false
|
||||
end
|
||||
|
||||
function private.ChatMsgSystemEventHandler(_, msg)
|
||||
if #private.addedFriends == 0 then
|
||||
return
|
||||
end
|
||||
if msg == ERR_FRIEND_NOT_FOUND then
|
||||
if #private.addedFriends > 0 then
|
||||
private.invalidCharacters[strlower(tremove(private.addedFriends, 1))] = true
|
||||
end
|
||||
else
|
||||
for i, v in ipairs(private.addedFriends) do
|
||||
if format(ERR_FRIEND_ADDED_S, v) == msg then
|
||||
tremove(private.addedFriends, i)
|
||||
private.invalidCharacters[strlower(v)] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
29
LibTSM/Service/SyncClasses/Constants.lua
Normal file
29
LibTSM/Service/SyncClasses/Constants.lua
Normal file
@@ -0,0 +1,29 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Constants = TSM.Init("Service.SyncClasses.Constants")
|
||||
Constants.VERSION = 11
|
||||
Constants.DATA_TYPES = {
|
||||
-- new connection types (40-49)
|
||||
WHOAMI_ACCOUNT = strchar(40),
|
||||
WHOAMI_ACK = strchar(41),
|
||||
-- connection status types (50-69)
|
||||
CONNECTION_REQUEST = strchar(50),
|
||||
CONNECTION_REQUEST_ACK = strchar(51),
|
||||
DISCONNECT = strchar(52),
|
||||
HEARTBEAT = strchar(53),
|
||||
-- data mirroring types (70-99)
|
||||
CHARACTER_HASHES_BROADCAST = strchar(70),
|
||||
CHARACTER_SETTING_HASHES_REQUEST = strchar(71),
|
||||
CHARACTER_SETTING_HASHES_RESPONSE = strchar(72),
|
||||
CHARACTER_SETTING_DATA_REQUEST = strchar(73),
|
||||
CHARACTER_SETTING_DATA_RESPONSE = strchar(74),
|
||||
-- RPC types (100-109)
|
||||
RPC_CALL = strchar(100),
|
||||
RPC_RETURN = strchar(101),
|
||||
RPC_PREAMBLE = strchar(102),
|
||||
}
|
||||
268
LibTSM/Service/SyncClasses/Mirror.lua
Normal file
268
LibTSM/Service/SyncClasses/Mirror.lua
Normal file
@@ -0,0 +1,268 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Mirror = TSM.Init("Service.SyncClasses.Mirror")
|
||||
local Delay = TSM.Include("Util.Delay")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Math = TSM.Include("Util.Math")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local Settings = TSM.Include("Service.Settings")
|
||||
local Constants = TSM.Include("Service.SyncClasses.Constants")
|
||||
local Comm = TSM.Include("Service.SyncClasses.Comm")
|
||||
local Connection = TSM.Include("Service.SyncClasses.Connection")
|
||||
local private = {
|
||||
numConnected = 0,
|
||||
accountStatus = {},
|
||||
callbacks = {},
|
||||
}
|
||||
local BROADCAST_INTERVAL = 3
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
Mirror:OnModuleLoad(function()
|
||||
Connection.RegisterConnectionChangedCallback(private.ConnectionChangedHandler)
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.CHARACTER_HASHES_BROADCAST, private.CharacterHashesBroadcastHandler)
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.CHARACTER_SETTING_HASHES_REQUEST, private.CharacterSettingHashesRequestHandler)
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.CHARACTER_SETTING_HASHES_RESPONSE, private.CharacterSettingHashesResponseHandler)
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.CHARACTER_SETTING_DATA_REQUEST, private.CharacterSettingDataRequestHandler)
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.CHARACTER_SETTING_DATA_RESPONSE, private.CharacterSettingDataResponseHandler)
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Mirror.GetStatus(account)
|
||||
local status = private.accountStatus[account]
|
||||
if not status then
|
||||
return false, false
|
||||
elseif status == "UPDATING" then
|
||||
return true, false
|
||||
elseif status == "SYNCED" then
|
||||
return true, true
|
||||
else
|
||||
error("Invalid status: "..tostring(status))
|
||||
end
|
||||
end
|
||||
|
||||
function Mirror.RegisterCallback(callback)
|
||||
tinsert(private.callbacks, callback)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Connection Callback Handlers
|
||||
-- ============================================================================
|
||||
|
||||
function private.ConnectionChangedHandler(account, player, connected)
|
||||
if connected == nil then
|
||||
-- new account, but not yet connected
|
||||
return
|
||||
end
|
||||
if connected then
|
||||
Log.Info("Connected to %s (%s)", account, player)
|
||||
else
|
||||
Log.Info("Disconnected from %s (%s)", account, player)
|
||||
end
|
||||
private.numConnected = private.numConnected + (connected and 1 or -1)
|
||||
assert(private.numConnected >= 0)
|
||||
if connected then
|
||||
private.accountStatus[account] = "UPDATING"
|
||||
Delay.AfterTime("mirrorCharacterHashes", 0, private.SendCharacterHashes, BROADCAST_INTERVAL)
|
||||
else
|
||||
private.accountStatus[account] = nil
|
||||
if private.numConnected == 0 then
|
||||
Delay.Cancel("mirrorCharacterHashes")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Delay-Based Last Update Send Function
|
||||
-- ============================================================================
|
||||
|
||||
function private.SendCharacterHashes()
|
||||
assert(private.numConnected > 0)
|
||||
|
||||
-- calculate the hashes of the sync settings for all characters on this account
|
||||
local hashes = TempTable.Acquire()
|
||||
for _, character in Settings.CharacterByAccountFactionrealmIterator() do
|
||||
hashes[character] = private.CalculateCharacterHash(character)
|
||||
end
|
||||
|
||||
-- send the hashes to all connected accounts
|
||||
for _, character in Connection.ConnectedAccountIterator() do
|
||||
Comm.SendData(Constants.DATA_TYPES.CHARACTER_HASHES_BROADCAST, character, hashes)
|
||||
end
|
||||
TempTable.Release(hashes)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Message Handlers
|
||||
-- ============================================================================
|
||||
|
||||
function private.CharacterHashesBroadcastHandler(dataType, sourceAccount, sourcePlayer, data)
|
||||
assert(dataType == Constants.DATA_TYPES.CHARACTER_HASHES_BROADCAST)
|
||||
if not Connection.IsCharacterConnected(sourcePlayer) then
|
||||
-- we're not connected to this player
|
||||
Log.Warn("Got CHARACTER_HASHES_BROADCAST for player which isn't connected")
|
||||
return
|
||||
end
|
||||
local didChange = false
|
||||
for _, character in Settings.CharacterByAccountFactionrealmIterator(sourceAccount) do
|
||||
if not data[character] then
|
||||
-- this character doesn't exist anymore, so remove it
|
||||
Log.Info("Removed character: '%s'", character)
|
||||
Settings.RemoveSyncCharacter(character)
|
||||
didChange = true
|
||||
end
|
||||
end
|
||||
for character, hash in pairs(data) do
|
||||
if not Settings.GetCharacterSyncAccountKey(character) then
|
||||
-- this is a new character, so add it to our DB
|
||||
Log.Info("New character: '%s' '%s'", character, sourceAccount)
|
||||
Settings.NewSyncCharacter(sourceAccount, character)
|
||||
didChange = true
|
||||
end
|
||||
if hash ~= private.CalculateCharacterHash(character) then
|
||||
-- this character's data has changed so request a hash of each of the keys
|
||||
Log.Info("Character data has changed: '%s'", character)
|
||||
Comm.SendData(Constants.DATA_TYPES.CHARACTER_SETTING_HASHES_REQUEST, sourcePlayer, character)
|
||||
didChange = true
|
||||
end
|
||||
end
|
||||
if didChange then
|
||||
private.accountStatus[sourceAccount] = "UPDATING"
|
||||
else
|
||||
private.accountStatus[sourceAccount] = "SYNCED"
|
||||
end
|
||||
end
|
||||
|
||||
function private.CharacterSettingHashesRequestHandler(dataType, sourceAccount, sourcePlayer, data)
|
||||
assert(dataType == Constants.DATA_TYPES.CHARACTER_SETTING_HASHES_REQUEST)
|
||||
if not Connection.IsCharacterConnected(sourcePlayer) then
|
||||
-- we're not connected to this player
|
||||
Log.Warn("Got CHARACTER_HASHES_BROADCAST for player which isn't connected")
|
||||
return
|
||||
elseif Settings.GetCharacterSyncAccountKey(data) ~= Settings.GetCurrentSyncAccountKey() then
|
||||
-- we don't own this character
|
||||
Log.Err("Request for character we don't own ('%s', '%s')", tostring(data), tostring(Settings.GetCharacterSyncAccountKey(data)))
|
||||
return
|
||||
end
|
||||
Log.Info("CHARACTER_SETTING_HASHES_REQUEST (%s)", data)
|
||||
local responseData = TempTable.Acquire()
|
||||
responseData._character = data
|
||||
for _, namespace, settingKey in Settings.SyncSettingIterator() do
|
||||
responseData[namespace.."."..settingKey] = private.CalculateCharacterSettingHash(data, namespace, settingKey)
|
||||
end
|
||||
Comm.SendData(Constants.DATA_TYPES.CHARACTER_SETTING_HASHES_RESPONSE, sourcePlayer, responseData)
|
||||
TempTable.Release(responseData)
|
||||
end
|
||||
|
||||
function private.CharacterSettingHashesResponseHandler(dataType, sourceAccount, sourcePlayer, data)
|
||||
assert(dataType == Constants.DATA_TYPES.CHARACTER_SETTING_HASHES_RESPONSE)
|
||||
if not Connection.IsCharacterConnected(sourcePlayer) then
|
||||
-- we're not connected to this player
|
||||
Log.Warn("Got CHARACTER_HASHES_BROADCAST for player which isn't connected")
|
||||
return
|
||||
end
|
||||
local character = data._character
|
||||
data._character = nil
|
||||
Log.Info("CHARACTER_SETTING_HASHES_RESPONSE (%s)", character)
|
||||
for key, hash in pairs(data) do
|
||||
local namespace, settingKey = strsplit(".", key)
|
||||
if private.CalculateCharacterSettingHash(character, namespace, settingKey) ~= hash then
|
||||
-- the settings data for key changed, so request the latest data for it
|
||||
Log.Info("Setting data has changed: '%s', '%s'", character, key)
|
||||
Comm.SendData(Constants.DATA_TYPES.CHARACTER_SETTING_DATA_REQUEST, sourcePlayer, character.."."..key)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function private.CharacterSettingDataRequestHandler(dataType, sourceAccount, sourcePlayer, data)
|
||||
assert(dataType == Constants.DATA_TYPES.CHARACTER_SETTING_DATA_REQUEST)
|
||||
local character, namespace, settingKey = strsplit(".", data)
|
||||
if not Connection.IsCharacterConnected(sourcePlayer) then
|
||||
-- we're not connected to this player
|
||||
Log.Warn("Got CHARACTER_HASHES_BROADCAST for player which isn't connected")
|
||||
return
|
||||
elseif Settings.GetCharacterSyncAccountKey(character) ~= Settings.GetCurrentSyncAccountKey() then
|
||||
-- we don't own this character
|
||||
Log.Err("Request for character we don't own ('%s', '%s')", tostring(character), tostring(Settings.GetCharacterSyncAccountKey(character)))
|
||||
return
|
||||
end
|
||||
Log.Info("CHARACTER_SETTING_DATA_REQUEST (%s,%s,%s)", character, namespace, settingKey)
|
||||
local responseData = TempTable.Acquire()
|
||||
responseData.character = character
|
||||
responseData.namespace = namespace
|
||||
responseData.settingKey = settingKey
|
||||
responseData.data = Settings.Get("sync", Settings.GetSyncScopeKeyByCharacter(character), namespace, settingKey)
|
||||
Comm.SendData(Constants.DATA_TYPES.CHARACTER_SETTING_DATA_RESPONSE, sourcePlayer, responseData)
|
||||
TempTable.Release(responseData)
|
||||
end
|
||||
|
||||
function private.CharacterSettingDataResponseHandler(dataType, sourceAccount, sourcePlayer, data)
|
||||
assert(dataType == Constants.DATA_TYPES.CHARACTER_SETTING_DATA_RESPONSE)
|
||||
if not Connection.IsCharacterConnected(sourcePlayer) then
|
||||
-- we're not connected to this player
|
||||
Log.Warn("Got CHARACTER_HASHES_BROADCAST for player which isn't connected")
|
||||
return
|
||||
end
|
||||
local dataValueType = type(data.data)
|
||||
Log.Info("CHARACTER_SETTING_DATA_RESPONSE (%s,%s,%s,%s,%s)", data.character, data.namespace, data.settingKey, dataValueType, (dataValueType == "string" or dataValueType == "table") and #dataValueType or "-")
|
||||
if dataValueType == "table" then
|
||||
local tbl = Settings.Get("sync", Settings.GetSyncScopeKeyByCharacter(data.character), data.namespace, data.settingKey)
|
||||
wipe(tbl)
|
||||
for i, v in pairs(data.data) do
|
||||
tbl[i] = v
|
||||
end
|
||||
else
|
||||
Settings.Set("sync", Settings.GetSyncScopeKeyByCharacter(data.character), data.namespace, data.settingKey, data.data)
|
||||
end
|
||||
for _, callback in ipairs(private.callbacks) do
|
||||
callback()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.CalculateCharacterHash(character)
|
||||
local hash = nil
|
||||
local settingKeys = TempTable.Acquire()
|
||||
for _, namespace, settingKey in Settings.SyncSettingIterator() do
|
||||
tinsert(settingKeys, strjoin(".", namespace, settingKey))
|
||||
end
|
||||
sort(settingKeys)
|
||||
for _, key in ipairs(settingKeys) do
|
||||
hash = Math.CalculateHash(key, hash)
|
||||
local namespace, settingKey = strsplit(".", key)
|
||||
local settingValue = Settings.Get("sync", Settings.GetSyncScopeKeyByCharacter(character), namespace, settingKey)
|
||||
hash = Math.CalculateHash(settingValue, hash)
|
||||
end
|
||||
assert(hash)
|
||||
TempTable.Release(settingKeys)
|
||||
return hash
|
||||
end
|
||||
|
||||
function private.CalculateCharacterSettingHash(character, namespace, settingKey)
|
||||
return Math.CalculateHash(Settings.Get("sync", Settings.GetSyncScopeKeyByCharacter(character), namespace, settingKey))
|
||||
end
|
||||
177
LibTSM/Service/SyncClasses/RPC.lua
Normal file
177
LibTSM/Service/SyncClasses/RPC.lua
Normal file
@@ -0,0 +1,177 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local RPC = TSM.Init("Service.SyncClasses.RPC")
|
||||
local Delay = TSM.Include("Util.Delay")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local Constants = TSM.Include("Service.SyncClasses.Constants")
|
||||
local Comm = TSM.Include("Service.SyncClasses.Comm")
|
||||
local Connection = TSM.Include("Service.SyncClasses.Connection")
|
||||
local private = {
|
||||
rpcFunctions = {},
|
||||
pendingRPC = {},
|
||||
rpcSeqNum = 0,
|
||||
}
|
||||
local RPC_EXTRA_TIMEOUT = 15
|
||||
local CALLBACK_TIME_WARNING_THRESHOLD_MS = 20
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
RPC:OnModuleLoad(function()
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.RPC_CALL, private.HandleCall)
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.RPC_RETURN, private.HandleReturn)
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.RPC_PREAMBLE, private.HandlePreamble)
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function RPC.Register(name, func)
|
||||
assert(name)
|
||||
private.rpcFunctions[name] = func
|
||||
end
|
||||
|
||||
function RPC.Call(name, targetPlayer, handler, ...)
|
||||
assert(targetPlayer)
|
||||
if not Connection.IsCharacterConnected(targetPlayer) then
|
||||
return false
|
||||
end
|
||||
|
||||
assert(private.rpcFunctions[name], "Cannot call an RPC which is not also registered locally.")
|
||||
private.rpcSeqNum = private.rpcSeqNum + 1
|
||||
|
||||
local requestData = TempTable.Acquire()
|
||||
requestData.name = name
|
||||
requestData.args = TempTable.Acquire(...)
|
||||
requestData.seq = private.rpcSeqNum
|
||||
local numBytes = Comm.SendData(Constants.DATA_TYPES.RPC_CALL, targetPlayer, requestData)
|
||||
TempTable.Release(requestData.args)
|
||||
TempTable.Release(requestData)
|
||||
|
||||
local context = TempTable.Acquire()
|
||||
context.name = name
|
||||
context.handler = handler
|
||||
context.timeoutTime = time() + RPC_EXTRA_TIMEOUT + private.EstimateTransferTime(numBytes)
|
||||
private.pendingRPC[private.rpcSeqNum] = context
|
||||
Delay.AfterTime("SYNC_PENDING_RPC", 1, private.HandlePendingRPC)
|
||||
|
||||
return true, (context.timeoutTime - time()) * 2 / 3
|
||||
end
|
||||
|
||||
function RPC.Cancel(name, handler)
|
||||
for seq, info in pairs(private.pendingRPC) do
|
||||
if info.name == name and info.handler == handler then
|
||||
TempTable.Release(info)
|
||||
private.pendingRPC[seq] = nil
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Message Handlers
|
||||
-- ============================================================================
|
||||
|
||||
function private.HandleCall(dataType, _, sourcePlayer, data)
|
||||
assert(dataType == Constants.DATA_TYPES.RPC_CALL)
|
||||
if type(data) ~= "table" or type(data.name) ~= "string" or type(data.seq) ~= "number" or type(data.args) ~= "table" then
|
||||
return
|
||||
end
|
||||
if not private.rpcFunctions[data.name] then
|
||||
return
|
||||
end
|
||||
local responseData = TempTable.Acquire()
|
||||
|
||||
local startTime = debugprofilestop()
|
||||
responseData.result = TempTable.Acquire(private.rpcFunctions[data.name](unpack(data.args)))
|
||||
local timeTaken = debugprofilestop() - startTime
|
||||
if timeTaken > CALLBACK_TIME_WARNING_THRESHOLD_MS then
|
||||
Log.Warn("RPC (%s) took %0.2fms", tostring(data.name), timeTaken)
|
||||
end
|
||||
responseData.seq = data.seq
|
||||
local numBytes = Comm.SendData(Constants.DATA_TYPES.RPC_RETURN, sourcePlayer, responseData)
|
||||
TempTable.Release(responseData.result)
|
||||
TempTable.Release(responseData)
|
||||
|
||||
local transferTime = private.EstimateTransferTime(numBytes)
|
||||
if transferTime > 1 then
|
||||
-- We sent more than 1 second worth of data back, so send a preamble to allow the source to adjust its timeout accordingly.
|
||||
local preambleData = TempTable.Acquire()
|
||||
preambleData.transferTime = transferTime
|
||||
preambleData.seq = data.seq
|
||||
Comm.SendData(Constants.DATA_TYPES.RPC_PREAMBLE, sourcePlayer, preambleData)
|
||||
TempTable.Release(preambleData)
|
||||
end
|
||||
end
|
||||
|
||||
function private.HandleReturn(dataType, _, _, data)
|
||||
assert(dataType == Constants.DATA_TYPES.RPC_RETURN)
|
||||
if type(data.seq) ~= "number" or type(data.result) ~= "table" then
|
||||
return
|
||||
elseif not private.pendingRPC[data.seq] then
|
||||
return
|
||||
end
|
||||
local startTime = debugprofilestop()
|
||||
private.pendingRPC[data.seq].handler(unpack(data.result))
|
||||
local timeTaken = debugprofilestop() - startTime
|
||||
if timeTaken > CALLBACK_TIME_WARNING_THRESHOLD_MS then
|
||||
Log.Warn("RPC (%s) result handler took %0.2fms", tostring(private.pendingRPC[data.seq].name), timeTaken)
|
||||
end
|
||||
TempTable.Release(private.pendingRPC[data.seq])
|
||||
private.pendingRPC[data.seq] = nil
|
||||
end
|
||||
|
||||
function private.HandlePreamble(dataType, _, _, data)
|
||||
assert(dataType == Constants.DATA_TYPES.RPC_PREAMBLE)
|
||||
if type(data.seq) ~= "number" or type(data.transferTime) ~= "number" then
|
||||
return
|
||||
elseif not private.pendingRPC[data.seq] then
|
||||
return
|
||||
end
|
||||
-- extend the timeout
|
||||
private.pendingRPC[data.seq].timeoutTime = time() + RPC_EXTRA_TIMEOUT + data.transferTime
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.EstimateTransferTime(numBytes)
|
||||
return ceil(numBytes / (ChatThrottleLib.MAX_CPS / 2))
|
||||
end
|
||||
|
||||
function private.HandlePendingRPC()
|
||||
if not next(private.pendingRPC) then
|
||||
return
|
||||
end
|
||||
local timedOut = TempTable.Acquire()
|
||||
for seq, info in pairs(private.pendingRPC) do
|
||||
if time() > info.timeoutTime then
|
||||
tinsert(timedOut, seq)
|
||||
end
|
||||
end
|
||||
for _, seq in ipairs(timedOut) do
|
||||
local info = private.pendingRPC[seq]
|
||||
Log.Warn("RPC timed out (%s)", info.name)
|
||||
info.handler()
|
||||
TempTable.Release(info)
|
||||
private.pendingRPC[seq] = nil
|
||||
end
|
||||
TempTable.Release(timedOut)
|
||||
end
|
||||
Reference in New Issue
Block a user