TradeSkillMaster/LibTSM/Service/SyncClasses/Connection.lua

444 lines
14 KiB
Lua

-- ------------------------------------------------------------------------------ --
-- 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