initial commit

This commit is contained in:
Gitea
2020-11-13 14:13:12 -05:00
commit 05df49ff60
368 changed files with 128754 additions and 0 deletions

View 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

View 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

View 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),
}

View 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

View 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