444 lines
14 KiB
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
|