sirnii's picture
Upload 1816 files
b6a38d7 verified
raw
history blame
42.4 kB
config.SwarmPublicKey = config.SwarmPublicKey or {}
config.SwarmPublicKey["dev"] = RSACreateKeyNoErr(
[[-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuDWBDpkjqJuJ1kaZGtlf
AJPS2q28oZ3Qk2hPoTDVGRzT93RmiCNGk4kQr4jtBNaoeCnAN7cUHC9A4Npww/m+
S/3LrNOIfn7inS9uBJEAowNaLf90g8YOdkyJ3aaNXJrjHyKrL5z4W/+qLB6jr0Po
yzQqcZDduy1+bAJIslYY58vPoTZkk63w55H5MdkicksnDPQxxf2Bo3WQwvYt4GlN
UrPLBP5xGXtE2DJXqsRhHfIC5gaBewcKl3oXGHDxaYMTA3p5doMpfJUtGHdh9xd/
3dIPb1rx65v7kagCE9T7LfoBWTjfi2ONUcxxsu5tD3PyTtmBHlGZP2AyGKxYHYEl
rwIDAQAB
-----END PUBLIC KEY-----]])
function GetSwarmPublicKey(host)
return config.SwarmPublicKey[host] or config.SwarmPublicKey["dev"]
end
if Platform.cmdline then return end
if FirstLoad then
netSwarmSocket = false
netDisplayName = false
netAccountId = false
netAuthProvider = false
netSwarmPing = -1
netInGame = false
netUniqueId = 1
netGameSeed = 0
netGameMaxPlayers = 0
netGamePlayers = {}
netGameAddress = false
netGameInfo = {}
netServerRealTimeDelta = 0
netServerGameTimeDelta = 0
netBufferedEvents = false
netDesync = false
netBannedReason, netBannedPeriod = false, 0
netAllowGossip = false
netRestrictedAccount = false
netEnvironment = false
netConnectThread = false
netConnectionReasons = {}
--Hashing:
HashLogSize = rawget(_G, "HashLogSize") or 16 -- in MB
HashLogPath = rawget(_G, "HashLogPath") or ""
-- Simulate network lag
netSimulateLagAvg = 0
netSimulateLagAmp = 0
netSimulateLagLastTime = 0
end
-------------------------------------------------[ Hash ]------------------------------------------------------
function NetGetHashValue()
return GetEngineVar("", "NetHashValue")
end
function NetIsHashEnabled()
if GetEngineVar("", "NetEnableUpdateHash") then
return true, NetHashUpdateReasons
else
return false, NetHashPauseReasons
end
end
function NetResetHashValue(value)
return SetEngineVar("", "NetHashValue", value or 1)
end
function NetSetUpdateHash()
Msg("NetUpdateHashReasons", NetHashUpdateReasons, NetHashPauseReasons)
local enable = next(NetHashUpdateReasons) and not next(NetHashPauseReasons) and true or false
--print("NetApplyHashReasons:", enable, "\n\tENABLE:", table.concat(table.keys(NetHashUpdateReasons), ", "), "\n\tPAUSE:", table.concat(table.keys(NetHashPauseReasons), ", "))
SetEngineVar("", "NetEnableUpdateHash", enable)
end
function OnMsg.NetUpdateHashReasons(enable_reasons)
enable_reasons.netInGame = Game and netInGame and true or nil
end
if FirstLoad then
NetHashUpdateReasons = {}
NetHashPauseReasons = {}
end
function NetPauseUpdateHash(reason)
NetHashPauseReasons[reason or false] = true
NetSetUpdateHash()
end
function NetResumeUpdateHash(reason)
NetHashPauseReasons[reason or false] = nil
NetSetUpdateHash()
end
function NetSetUpdateHashReason(reason)
NetHashUpdateReasons[reason or false] = true
NetSetUpdateHash()
end
function NetClearUpdateHashReason(reason)
NetHashUpdateReasons[reason or false] = nil
NetSetUpdateHash()
end
function OnMsg.PersistSave(data)
data.HashValue = NetGetHashValue()
end
function OnMsg.PersistLoad(data)
NetResetHashValue(data.HashValue)
end
function ShouldResetHashLogOnMapChange()
return true
end
-- stop NetUpdateHash during map loading
function OnMsg.ChangeMap()
if ShouldResetHashLogOnMapChange() then NetResetHashLog(HashLogSize) end
NetPauseUpdateHash("ChangingMap")
end
function OnMsg.ChangeMapDone()
NetResumeUpdateHash("ChangingMap")
end
function OnMsg.PreNewMap()
NetPauseUpdateHash("NewMap")
end
function OnMsg.PostNewMapLoaded()
if ShouldResetHashLogOnMapChange() then NetResetHashLog(HashLogSize) end
NetResumeUpdateHash("NewMap")
NetUpdateHash("NewMapLoaded", CurrentMap, mapdata.NetHash, MapLoadRandom, Game and Game.seed_text)
end
function OnMsg.PreLoadGame()
NetPauseUpdateHash("LoadGame")
end
function OnMsg.PostLoadGame()
NetPauseUpdateHash("LoadGame")
end
-- stop NetUpdateHash during loading a saved game
function OnMsg.UnpersistStart()
NetResetHashLog(HashLogSize)
NetPauseUpdateHash("LoadGame")
end
function OnMsg.UnpersistEnd()
NetResetHashLog(HashLogSize)
NetResumeUpdateHash("LoadGame")
end
function OnMsg.NewGame()
NetResetHashLog(HashLogSize)
NetResetHashValue()
NetSetUpdateHash()
end
function OnMsg.DoneGame(game)
NetResetHashLog(HashLogSize)
NetSetUpdateHash()
end
function NetSyncEvents.Desync(game_id, ...)
print("Desync: " .. game_id, ...)
netDesync = true
local data = GetHashLog()
NetSend("rfnLog", "desync", game_id, "txt", CompressPstr(data))
local path
if config.DesyncPath then
path = config.DesyncPath
if not string.ends_with(path, "\\") then
path = path .. "\\"
end
local username = (Platform.ps4 and netDisplayName or "") or GetUsername()
path = path .. game_id .. "-" .. username .. "-" .. netUniqueId .. ".desync.log"
print("Desync log saved at:", path)
CreateRealTimeThread(function()
local err = AsyncStringToFile(path, data)
if err then print("DumpHashLog", err) end
end)
end
Msg("GameDesynced", path, data)
end
local function InvokeObjCheat(selection, method, ...)
local objs = IsValid(selection) and { selection } or selection
for _, obj in ipairs(objs) do
if IsValid(obj) and PropObjHasMember(obj, method) then
LogCheatUsed(method, obj)
obj[method](obj, ...)
end
end
end
function NetSyncEvents.ObjCheat(selection, method, ...)
if not AreCheatsEnabled() then return end
print("ObjCheat", method)
assert(string.starts_with(method, "Cheat"))
if string.starts_with(method, "Cheat") then
Msg("ObjCheatStart", method)
procall(InvokeObjCheat, selection, method, ...)
Msg("ObjCheatEnd", method)
end
end
function NetSyncEvents.Cheat(method, ...)
if not AreCheatsEnabled() then return end
print("Cheat", method)
assert(string.starts_with(method, "Cheat"))
if string.starts_with(method, "Cheat") then
LogCheatUsed(method)
_G[method](...)
end
end
GameVar("CheatsUsed", false)
function LogCheatUsed(method, obj, ...)
CheatsUsed = CheatsUsed or {}
CheatsUsed[#CheatsUsed + 1] = { GameTime(), method, obj and obj.class or nil, obj and rawget(obj, "handle") or nil }
end
function AreCheatsUsed()
return CheatsUsed and #CheatsUsed > 0
end
function OnMsg.UnableToUnlockAchievementReasons(reasons, achievement)
if not Platform.asserts and AreCheatsUsed() then
reasons["cheats used"] = true
end
end
function _GetCheatsUsedStr()
local tbl = { "Cheats used:" }
for _, entry in ipairs(CheatsUsed) do
local time, method, class, handle = table.unpack(entry)
local obj_str = ""
if class and handle then
obj_str = string.format("%s(%d)", class, handle)
elseif class then
obj_str = class
end
tbl[#tbl + 1] = string.format("%10d %30s %s", time, method, obj_str)
end
return table.concat(tbl, "\n\t")
end
function OnMsg.BugReportStart(print_func)
if CheatsUsed then
print_func(_GetCheatsUsedStr())
end
end
if Platform.console and not Platform.developer then
function LogHash() end
end
function GetHashLog()
local res = pstr("", 2 * HashLogSize * 1024 * 1024)
NetGetHashLog(res)
res:append("\n\n\n\nMap: ", GetMap())
res:append("\nMap hash: ", tostring(mapdata.NetHash))
res:append("\nLuaRevision: ", LuaRevision)
res:append("\nAssetsRevision: ", AssetsRevision)
res:append("\nPlatform: ", PlatformName())
res:append("\nProvider: ", ProviderName())
res:append("\nVariant: ", VariantName())
res:append("\nPass grids hash: ", terrain.HashPassability())
res:append("\nPass tunnels hash: ", terrain.HashPassabilityTunnels())
res:append("\nSuspendPassEditsReasons: ", TableToLuaCode(s_SuspendPassEditsReasons))
if Platform.developer then
res:append("\nDisplayName: ", netDisplayName or "???")
res:append("\nIPs: ", LocalIPs())
res:append("\nExecutable folder: ", GetExecDirectory())
end
Msg("Desync", res)
res:append("\n\nObjects:")
local sync_objs, async_objs, tunnel_objs = {}, {}, {}
MapForEach(true, "Object", function (obj, ignore_classes)
if obj.handle and (not ignore_classes or not obj:IsKindOfClasses(ignore_classes)) then
if obj:IsKindOf("PFTunnel") then
table.insert(tunnel_objs, obj)
elseif obj:IsSyncObject() then
table.insert(sync_objs, obj)
else
table.insert(async_objs, obj)
end
end
end, config.NetDesyncIgnoreClasses)
local function HashLogCmp(obj1, obj2)
if obj1.class ~= obj2.class then return obj1.class < obj2.class end
local x1, y1, z1 = obj1:GetPosXYZ()
local x2, y2, z2 = obj2:GetPosXYZ()
if x1 ~= x2 then return x1 < x2 end
if y1 ~= y2 then return y1 < y2 end
if z1 ~= z2 then return (z1 or const.InvalidZ) < (z2 or const.InvalidZ) end
local a1 = obj1:GetAngle()
local a2 = obj2:GetAngle()
if a1 ~= a2 then return a1 < a2 end
local anim1 = obj1:GetStateText()
local anim2 = obj2:GetStateText()
if anim1 ~= anim2 then return anim1 < anim2 end
if obj1:IsKindOf("Collection") then
if obj1.Index ~= obj2.Index then return obj1.Index < obj2.Index end
end
return obj1.handle < obj2.handle
end
table.sort(sync_objs, HashLogCmp)
table.sort(async_objs, HashLogCmp)
local function HashLogCmpTunnel(obj1, obj2)
local type1 = pf.GetTunnelType(obj1)
local type2 = pf.GetTunnelType(obj2)
if type1 ~= type2 then return type1 < type2 end
if obj1.class ~= obj2.class then return obj1.class < obj2.class end
local entrance1 = pf.GetTunnelEntrance(obj1)
local entrance2 = pf.GetTunnelEntrance(obj2)
if entrance1 ~= entrance2 then return entrance1 < entrance2 end
local exit1 = pf.GetTunnelExit(obj1)
local exit2 = pf.GetTunnelExit(obj2)
if exit1 ~= exit2 then return exit1 < exit2 end
local flags1 = pf.GetTunnelFlags(obj1)
local flags2 = pf.GetTunnelFlags(obj2)
if flags1 ~= flags2 then return flags1 < flags2 end
local param1 = pf.GetTunnelParam(obj1)
local param2 = pf.GetTunnelParam(obj2)
if param1 ~= param2 then return param1 < param2 end
return obj1.handle < obj2.handle
end
table.sort(tunnel_objs, HashLogCmpTunnel)
res:append("\n\nDestlocks:")
MapForEach(true, "Destlock", function (obj)
local x, y, z = obj:GetPosXYZ()
if z then
res:appendf("\nDestlock: pos=(%d,%d,%d), radius=%d", x, y, z, obj:GetRadius())
else
res:appendf("\nDestlock: pos=(%d,%d), radius=%d", x, y, obj:GetRadius())
end
end)
res:append("\n\nTunnels:")
for i, obj in ipairs(tunnel_objs) do
res:appendf("\nSH: %9d, %s, type=%d", obj.handle, obj.class, pf.GetTunnelType(obj))
local entrance = pf.GetTunnelEntrance(obj)
if entrance:IsValidZ() then
res:appendf(", (%d,%d,%d)", entrance:xyz())
else
res:appendf(", (%d,%d)", entrance:xyz())
end
local exit = pf.GetTunnelExit(obj)
if exit:IsValidZ() then
res:appendf("->(%d,%d,%d)", exit:xyz())
else
res:appendf("->(%d,%d)", exit:xyz())
end
res:appendf(", weight=%d", pf.GetTunnelWeight(obj))
local flags = pf.GetTunnelFlags(obj)
if flags ~= 4294967295 then
res:appendf(", flags=%d", flags)
end
local param = pf.GetTunnelParam(obj)
if param ~= 0 then
res:appendf(", param=%d", param)
end
end
local efResting = const.efResting
local efPathExecObstacle = const.efPathExecObstacle
local apply_slab_flags = const.efPathSlab + const.efApplyToGrids + const.efVisible
local function GetObjHashLog(res, obj)
res:appendf("\nSH: %9d, %s", obj.handle, obj.class)
if obj:IsKindOf("Collection") then
res:appendf(", %d, %s", obj.Index, obj.Name)
end
if obj:IsValidPos() then
if obj:IsValidZ() then
res:appendf(", pos=(%d,%d,%d)", obj:GetPosXYZ())
else
res:appendf(", pos=(%d,%d)", obj:GetPosXYZ())
end
local angle = obj:GetAngle()
if angle ~= 0 then
res:appendf(", angle=%d", angle)
local axisx, axisy, axisz = obj:GetAxisXYZ()
if axisx ~= 0 or axisy ~= 0 then
res:appendf(", axis=(%d,%d,%d)", axisx, axisy, axisz)
end
end
if obj:GetCollision() then
res:appendf(", Collision")
end
if obj:GetApplyToGrids() then
res:appendf(", ApplyToGrids")
if obj:GetEnumFlags(apply_slab_flags) == apply_slab_flags then
res:appendf(", ApplyPFLevelPass")
end
end
if obj:GetEnumFlags(efResting) ~= 0 then
res:appendf(", efResting, destlock_radius=%d", pf.GetDestlockRadius(obj))
end
if obj:GetEnumFlags(efPathExecObstacle) ~= 0 then
local r = pf.GetCollisionRadius(obj)
if r and r > 0 then
res:appendf(", efPathExecObstacle radius=%d", r)
end
end
local destlock = obj:GetDestlock()
if destlock and destlock:IsValidPos() then
local x, y, z = destlock:GetPosXYZ()
if z then
res:appendf(", destlock_pos=(%d,%d,%d), destlock_radius=%d", x, y, z, destlock:GetRadius())
else
res:appendf(", destlock_pos=(%d,%d), destlock_radius=%d", x, y, destlock:GetRadius())
end
end
end
local state = obj:GetState()
if state ~= 0 then
res:appendf(", state=%s(%d)", GetStateName(state), state)
end
local command = rawget(obj, "command")
if command then
if type(command) == "function" then
res:appendf(", cmd=(func)")
else
res:appendf(", cmd=%s", command)
end
end
end
res:append("\n\nSync Objects:")
for i, obj in ipairs(sync_objs) do
GetObjHashLog(res, obj)
end
res:append("\n\nAsync Objects:")
for i, obj in ipairs(async_objs) do
GetObjHashLog(res, obj)
end
res:append("\n\nSystem Log:")
local err, log_file = AsyncFileToString(GetLogFile(), false, false, "lines")
if err then
res:append(err, "\n")
else
for _, line in ipairs(log_file) do
res:append(line, "\n")
end
end
return res
end
---------------------------------------------------[ Global functions ]-------------------------------------------------------
function NetValidate(obj)
return IsValid(obj) and obj.__ancestors.Object and obj.handle and obj or nil
end
function NetIsLocal(obj)
return IsValid(obj) and obj:NetState() == "local"
end
function NetIsRemote(obj)
return IsValid(obj) and obj:NetState() == "remote"
end
function NetIsNeutral(obj)
return IsValid(obj) and not obj:NetState()
end
function NetInteractionState(actor, target)
local actor_state = IsValid(actor) and actor:NetState()
local target_state = IsValid(target) and target:NetState()
if not actor_state and not target_state and IsValid(actor) and IsValid(target) then
-- resolve neutral interactions based on monster target (which is synced)
actor_state = rawget(actor, "monster_target") and actor.monster_target:NetState()
target_state = rawget(target, "monster_target") and target.monster_target:NetState()
end
if actor_state == "local" or (not actor_state and target_state == "local") then return "local" end
if actor_state == "remote" or (not actor_state and target_state == "remote") then return "remote" end
end
function NetSerialize(...)
if netSwarmSocket then
return netSwarmSocket:Serialize(...)
else
return Serialize(...)
end
end
function NetUnserialize(...)
if netSwarmSocket then
return netSwarmSocket:Unserialize(...)
else
return Unserialize(...)
end
end
-------------------------------------------------[ Connection ]------------------------------------------------------
-- function VariantName() return "local server" end --> Uncomment to enable working with a local server on Xbox
function PlatformGetProviderLogin(official_connection) end
function NetGetProviderLogin(official_connection)
local err, auth_provider, auth_provider_data, display_name = PlatformGetProviderLogin(official_connection)
if err then return err end
local developer = false
if not auth_provider and Platform.test and insideHG() then -- Internal testing
display_name = tostring(sockGetHostName() or "unknown") .. "-" .. (10000 + AsyncRand(90000))
auth_provider = "auto"
auth_provider_data = display_name
developer = true
end
return err or not auth_provider and "no account", auth_provider, auth_provider_data, display_name, developer
end
function NetGetAutoLogin()
if Platform.desktop then
return nil, "auto", GetInstallationId(), false
end
return "no account"
end
function NetGetPasswordLogin(user, pass)
if not user or not pass then
return "no account"
end
return nil, "pass", {user, pass}, user
end
function NetChangePassword(old_pass, new_pass, email)
if not netSwarmSocket then
return "disconnected"
end
local err = netSwarmSocket:Call("rfnChangePassword", old_pass, new_pass, email)
if not err then
Msg("PasswordChanged", old_pass, new_pass, email)
end
return err
end
-- possble errors:
-- "version" - client is too old (LuaRevision)
-- "not ready" - server is there but not accepting connections (starting, etc.)
-- "maintenance" - server is there but in maintenance and not accepting connections
-- "redirect", host, port - redirect to the proper server for this user name (handled internally)
-- "failed" - no such user or wrong password
-- "banned" - the account is banned; optionally the global var 'netBannedReason' is (exploit|abuse|pirate|bot), optional netBannedPeriod is seconds until ban is lifted (otherwise forever)
-- other - cannot establish connection
local checksum, timestamp
function NetLogin(socket, host, port, auth_provider, auth_provider_data)
local err, signed_key, aes_key, aes_iv, token
local dlcs = GetAvailableDlcList()
local id = GetInstallationId()
if not checksum and rawget(_G, "ExeChecksumAndTimestamp") then
checksum, timestamp = ExeChecksumAndTimestamp()
end
-- see if the last connection was to the same place and if it was redirected somewhere else
err = "disconnected"
socket:Disconnect()
if rawget(_G, "AccountStorage") and AccountStorage.NetLastHost == host and
AccountStorage.NetLastPort == port and
AccountStorage.NetRedirectedHost and
AccountStorage.NetRedirectedPort
then
err = socket:WaitConnect(10000, AccountStorage.NetRedirectedHost, AccountStorage.NetRedirectedPort)
if err then
AccountStorage.NetRedirectedHost = nil
AccountStorage.NetRedirectedPort = nil
end
end
if err then
err = socket:WaitConnect(10000, host, port)
end
if err then return err end
if not signed_key then
err, aes_key, aes_iv, signed_key = socket:GenRSAEncryptedKey(GetSwarmPublicKey(host), 0)
if err then return "sign" end
end
err, token = socket:Call("rfnConn", LuaRevision, signed_key, auth_provider, config.SwarmWorld, PlatformName(), ProviderName(), VariantName())
if err then return err end
socket:SetAESEncryptionKey(aes_key, aes_iv)
socket:SetOption("encrypt", true)
local err, r2, r3, r4, r5 = socket:Call("rfnAuth", token, auth_provider_data, id, GetLanguage(), os.time(), dlcs, checksum, timestamp)
if err == "redirect" and r2 and r3 then
socket:Disconnect()
err = socket:WaitConnect(10000, r2, r3)
if not err and rawget(_G, "AccountStorage") then
AccountStorage.NetLastHost = host
AccountStorage.NetLastPort = port
AccountStorage.NetRedirectedHost = r2
AccountStorage.NetRedirectedPort = r3
SaveAccountStorage(3000)
end
if not err then
err, token = socket:Call("rfnConn", LuaRevision, signed_key, auth_provider, config.SwarmWorld, PlatformName(), ProviderName(), VariantName())
if err then return err end
socket:SetAESEncryptionKey(aes_key, aes_iv)
socket:SetOption("encrypt", true)
err, r2, r3, r4, r5 = socket:Call("rfnAuth", token, auth_provider_data, id, GetLanguage(), os.time(), dlcs, checksum, timestamp)
assert(err ~= "redirect") -- we should not be redirected twice
end
end
if err == "banned" then
netBannedReason = r2 or false
netBannedPeriod = r3 or false
end
return err, r2, r3, r4, r5
end
local checksum, timestamp
function NetConnect(host, port, auth_provider, auth_provider_data, display_name, check_updates, reason)
netConnectionReasons[reason or true] = true
if netSwarmSocket then return end
netConnectThread = netConnectThread or CreateRealTimeThread(function()
local socket = NetCloudSocket:new()
local err, account_id, restricted_account, environment = NetLogin(socket, host, port, auth_provider, auth_provider_data)
if netConnectThread ~= CurrentThread() then err = "cancelled" end
if err then
socket:delete()
else
assert(not netSwarmSocket, "Finished connecting while already connected!")
netSwarmSocket = socket
netDisplayName = display_name or false
netAuthProvider = auth_provider or false
netAccountId = account_id or false
netInGame = false
netAllowGossip = config.NetGossip or false
netRestrictedAccount = restricted_account or false
netEnvironment = environment or false
local update_def, description
err, update_def, description = socket:Call("rfnUpdate", check_updates)
Msg("NetConnect")
if update_def then
Msg("ContentUpdate", update_def, description)
end
end
if netConnectThread == CurrentThread() then
netConnectThread = false
else
err = "cancelled"
end
Msg(CurrentThread(), err)
end)
local ok, err = WaitMsg(netConnectThread)
if err and err ~= "cancelled" then
NetDisconnect(reason)
end
return err
end
function NetIsConnected()
return netSwarmSocket and netSwarmSocket:IsConnected()
end
function NetDisconnect(reason, msg)
reason = reason or true
if netConnectionReasons[reason] then
netConnectionReasons[reason] = nil
if next(netConnectionReasons) == nil then
NetForceDisconnect(msg)
end
end
end
function NetForceDisconnect(msg)
netConnectionReasons = {}
netConnectThread = false
local socket = netSwarmSocket
if not socket then
return "disconnected"
end
local currGame = netInGame
NetLeaveGame(msg)
netSwarmSocket = false
netDisplayName = false
netAuthProvider = false
socket:delete()
Msg("NetDisconnect", msg, currGame)
end
----- Game
function NetCreateGame(game_type, browser, name, visible_to, info, max_players)
local err, game_id = NetCall("rfnCreateGame", game_type, browser, name, visible_to, info, max_players)
if err then return err end
return err, game_id
end
function NetJoinGame(game_type, game_id, predef_unique_id)
if not netSwarmSocket then
return "disconnected"
end
NetLeaveGame("NetJoinGame")
Msg("NetJoinGameStart", game_type, game_id, predef_unique_id)
local err, unique_id, seed, game_address, game_info, player_info, max_players
if not game_type and type(game_id) == "number" then -- game_id is game_address
err, unique_id, seed, game_address, game_info, player_info, max_players = netSwarmSocket:Call("rfnJoinGame", game_id, predef_unique_id)
else
err, unique_id, seed, game_address, game_info, player_info, max_players = netSwarmSocket:Call("rfnJoinGameByName", game_type, game_id, predef_unique_id)
end
if err then return err end
netInGame = true
netUniqueId = unique_id
netGameSeed = seed
netDesync = false
netGameAddress = game_address
netGameInfo = game_info or {}
netGamePlayers = player_info
netGameMaxPlayers = max_players or netGameMaxPlayers
Msg("NetGameJoined", game_id, unique_id)
return false, unique_id
end
function NetLeaveGame(reason)
if netInGame then
netInGame = false
Msg("NetGameLeft", reason, netGameInfo)
NetSend("rfnLeaveGame", reason)
end
netBufferedEvents = false
netUniqueId = 1
netGameSeed = 0
netGamePlayers = {}
netGameAddress = false
netGameInfo = {}
end
function NetIsHost(id) -- in a network game and being with index 1
return netInGame and (id or netUniqueId) == 1
end
function NetCloudSocket:rfnGameInfo(info)
for k, v in pairs(info) do
netGameInfo[k] = v
end
Msg("NetGameInfo", info)
end
function NetChangeGameInfo(info)
return NetGameSend("rfnGameInfo", info)
end
function OnMsg.NetDisconnect()
NetLeaveGame("disconnect")
end
function NetSearchGames(browser, name, friend_games, func, ...)
local err, games = NetCall("rfnSearchGames", browser, name, friend_games, func, ...)
if err then return err end
-- convert to a more friendly format
for i, game in ipairs(games) do
games[i] = {
address = game[1],
name = game[2],
visibility = game[3],
players = game[4],
max_players = game[5],
info = game[6],
}
end
return nil, games
end
----- Content
function CreateContentDef(filename, chunk_size)
if not filename then return "params" end
local def = {}
local dir, file, ext = SplitPath(filename)
local name = file .. ext
def.name = ext == ".bin" and file or name
chunk_size = chunk_size or config.OnlineContentChunkSize or 512*1024
def.chunk_size = chunk_size
local err, size = AsyncGetFileAttribute(filename, "size")
if err then return err end
local err, timestamp = AsyncGetFileAttribute(filename, "timestamp")
if err then return err end
def.size = size
def.timestamp = timestamp
for offset = 0, size, chunk_size do
local err, hash = AsyncFileToString(filename, Min(chunk_size, size - offset), offset, "hash32", "raw")
if err then return err end
def[#def + 1] = hash
end
return nil, def
end
function NetCloudSocket:rfnContentChunk(name, i, chunk)
Msg(string.format("ContentChunk-%s-%d", name, i), chunk)
end
function NetDownloadContent(filename, def, progress, local_def)
if not NetIsConnected() then return "disconnected" end
local err
if type(def) == "string" then
err, def = NetCall("rfnGetContentDef", def)
if err then return err end
end
if not local_def then
err, local_def = CreateContentDef(filename, def.chunk_size)
local_def = local_def or { size = 0 }
end
if local_def.size > def.size then
AsyncFileDelete(filename) -- we cannot truncate the file so we delete it
end
for offset = 0, def.size, def.chunk_size do
local i = 1 + offset / def.chunk_size
if progress then progress(offset, def.size, def.name) end
if local_def[i] ~= def[i] then
err = NetSend("rfnGetContentChunk", def.name, i)
if err then return err end
local ok, chunk = WaitMsg(string.format("ContentChunk-%s-%d", def.name, i), 30000)
if not ok then return "timeout" end
if chunk then
err = AsyncStringToFile(filename, chunk, offset, def.timestamp)
chunk:free()
if err then return err end
end
end
end
if progress then progress(def.size, def.size, def.name) end
return err
end
-----------------------------------------------[ Registration, etc. ]------------------------------------------
local function LoginSystemAccount(timeout, host, port)
local conn = MessageSocket:new()
local err = NetLogin(conn, host, port, "*register", "public")
if err then conn:delete() end
return err, conn
end
function WaitRegister(timeout, host, port, username, password, serial, email)
if not username or not password then return "bad param" end
local err, sys_account = LoginSystemAccount(timeout, host, port)
if err then return err end
err = sys_account:Call("rfnRegister", username, password, serial, email)
sys_account:Disconnect()
return err
end
-- this works only if the serial number used to create the account is provided
function WaitChangePassword(timeout, host, port, username, password, serial, email)
if not username or not password or not serial then return "bad param" end
local err, sys_account = LoginSystemAccount(timeout, host, port)
if err then return err end
err = sys_account:Call("rfnChangePassword", username, password, serial, email)
sys_account:Disconnect()
return err
end
function WaitCheckSerial(timeout, host, port, serial)
if not serial then return "bad param" end
local err, sys_account = LoginSystemAccount(timeout, host, port)
if err then return err end
err = sys_account:Call("rfnCheckSerial", serial)
sys_account:Disconnect()
return err
end
-------------------------------------------------[ NetSend/NetCall ]---------------------------------------------
function NetSend(...)
if not netSwarmSocket then return "disconnected" end
return netSwarmSocket:Send(...)
end
function NetCall(...)
if not netSwarmSocket then return "disconnected" end
return netSwarmSocket:Call(...)
end
function NetGameSend(...)
if not netSwarmSocket then return "disconnected" end
if not netInGame then return "not in game" end
return netSwarmSocket:Send("rfnGameSend", ...)
end
function NetGameCall(...)
if not netSwarmSocket then return "disconnected" end
if not netInGame then return "not in game" end
return netSwarmSocket:Call("rfnGameCall", ...)
end
function NetGameBroadcast(...)
if not netSwarmSocket then return "disconnected" end
if not netInGame then return "not in game" end
return netSwarmSocket:Send("rfnGameSend", "rfnBroadcast", ...)
end
function NetLogFile(class, filename, ext, data)
if data then
local compressed_data = CompressPstr(data)
local err = NetCall("rfnLog", class, filename, ext, compressed_data)
compressed_data:free()
return err
end
end
-------------------------------------------------[ Events ]------------------------------------------------------
if FirstLoad then
NetStats = {
events_received = 0,
events_sent = 0,
events_received_ps = 0,
events_sent_ps = 0,
}
end
function GetLagEventDelay()
if netSimulateLagAvg == 0 then
return 0
end
local real_time = RealTime()
local send_time = Max(netSimulateLagLastTime, real_time + netSimulateLagAvg + AsyncRand(2 * netSimulateLagAmp) - netSimulateLagAmp)
return send_time - real_time
end
function SendEvent(type, event, ...)
local params, err = SerializePstr(...)
if not params then
assert(false, "Network serialization error: " .. tostring(err))
return err
end
local compressed = CompressPstr(params)
if #params > #compressed + 1 then
-- string.char(255) is unused by the serialization, so it is safe to use as marker
params:clear()
params:append(string.char(255), compressed)
end
if netSimulateLagAvg > 0 then
local socket = netSwarmSocket
local lag_delay = GetLagEventDelay()
CreateRealTimeThread(function()
Sleep(lag_delay)
socket:Send("rfnGameSend", type, event, params)
end)
return
end
return netSwarmSocket:Send("rfnGameSend", type, event, params)
end
function NetEvent(event, ...)
assert(NetEvents[event])
NetStats.events_sent = NetStats.events_sent + 1
if netInGame then
return SendEvent("rfnEvent", event, ...)
end
end
function NetEchoEvent(event, ...)
assert(NetEvents[event])
if netInGame then
return SendEvent("rfnEchoEvent", event, ...)
else
if netBufferedEvents then
local params, err = SerializePstr(...)
if not params then
assert(false, "Network serialization error: " .. tostring(err))
return err
end
netBufferedEvents[#netBufferedEvents + 1] = pack_params(event, params)
return
end
local handler = NetEvents[event]
if handler then
handler(...)
end
end
end
function NetBroadcastEvent(event, ...)
if netInGame then
return SendEvent("rfnBroadcast", event, ...)
end
end
function ProcessMissingHandles()
end
function NetCloudSocket:rfnEvent(event, params)
if params:byte(1) == 255 then
params = DecompressPstr(params, 2)
end
if netBufferedEvents then
netBufferedEvents[#netBufferedEvents + 1] = pack_params(event, params)
return
end
NetStats.events_received = NetStats.events_received + 1
local handler = NetEvents[event]
if handler then
handler(Unserialize(params))
ProcessMissingHandles(event, params)
end
end
function NetStartBufferEvents()
netBufferedEvents = {}
end
function NetStopBufferEvents()
local events = netBufferedEvents
if events then
netBufferedEvents = false
if netSwarmSocket then
for i=1,#events do
procall(netSwarmSocket.rfnEvent, netSwarmSocket, unpack_params(events[i]))
end
else
for i=1,#events do
procall(NetCloudSocket.rfnEvent, nil, unpack_params(events[i]))
end
end
end
end
function OnMsg.NetConnect()
CreateRealTimeThread(function()
local lastSent, lastReceived = NetStats.events_sent, NetStats.events_received
while netSwarmSocket do
Sleep(1000)
NetStats.events_sent_ps = NetStats.events_sent - lastSent
lastSent = NetStats.events_sent
NetStats.events_received_ps = NetStats.events_received - lastReceived
lastReceived = NetStats.events_received
Msg("NetStats")
end
end)
end
-------------------------------------------------[ Players ]------------------------------------------------------
function NetChangePlayerInfo(info)
if not netInGame then return "not in game" end
local player_info = netGamePlayers[netUniqueId]
for k, v in pairs(info) do
if player_info[k] == v then
info[k] = nil
end
end
if next(info) == nil then return end
return NetGameCall("rfnPlayerInfo", info)
end
function NetCloudSocket:rfnPlayerInfo(unique_id, info)
local player = netGamePlayers[unique_id]
assert(player, "rfnPlayerInfo for unknown player " .. tostring(unique_id) .. " (did he just leave?)")
if not player then return end
for k, v in pairs(info) do
player[k] = v
end
Msg("NetPlayerInfo", player, info)
end
function NetCloudSocket:rfnPlayerJoin(info)
if not netInGame then return end -- this is us joining the game which we receive before the join completes
netGamePlayers[info.id] = info
Msg("NetPlayerJoin", info)
end
function NetCloudSocket:rfnPlayerLeft(unique_id, reason)
unique_id = unique_id or netUniqueId
local player = netGamePlayers[unique_id]
netGamePlayers[unique_id] = nil
if unique_id == netUniqueId then
NetLeaveGame(reason)
else
if player then
Msg("NetPlayerLeft", player, reason)
end
end
end
function IsInOnlineGame(account_id)
for k, v in pairs(netGamePlayers) do
if v.account_id == account_id then
return true
end
end
end
-------------------------------------------------[ Keep Alive ]------------------------------------------------------
if FirstLoad then
netKeepAliveThread = false
end
function OnMsg.NetConnect()
assert(not netKeepAliveThread)
-- this thread keeps the OnlineGame object alive (besides measuring ping)
netKeepAliveThread = CreateRealTimeThread(function()
local keep_alive_time = config.SwarmKeepAliveTime or 10 * 1000
while true do
local time = RealTime()
local err = NetCall("rfnPing")
time = RealTime() - time
netSwarmPing = time
Msg("NetPing", time)
if err then
NetForceDisconnect(true)
break
end
Sleep(keep_alive_time)
end
end)
end
function OnMsg.NetDisconnect()
if netKeepAliveThread ~= CurrentThread() then
DeleteThread(netKeepAliveThread)
end
netKeepAliveThread = false
end
----
function IsAsyncCode()
return Libs.Network ~= "sync" or not IsGameTimeThread()
end
if FirstLoad then
PauseDesyncErrorsReasons = {}
end
function SuspendDesyncErrors(reason)
PauseDesyncErrorsReasons[reason] = true
end
function ResumeDesyncErrors(reason)
PauseDesyncErrorsReasons[reason] = nil
end
function IsDesyncIgnored()
return next(PauseDesyncErrorsReasons)
end
-------------------------------------------------[ Gossip ]------------------------------------------------------
function NetGossip(gossip, ...)
if gossip and netAllowGossip then
--LogGossip(TupleToLuaCodePStr(gossip, ...))
return NetSend("rfnGossip", gossip, ...)
end
end
-------------------------------------------------[ Tickets ]------------------------------------------------------
function NetUseTicket(ticket)
if not utf8.IsStrMoniker(ticket, 3, 60) then
return "not found"
end
local err, data = NetCall("rfnUseTicket", ticket)
if err then return err, data end
if type(ticket) == "string" then
g_UsedTickets[#g_UsedTickets + 1] = string.upper(ticket)
end
return nil, Decompress(data)
end
-------------------------------------------------[ Net map utilities ]------------------------------------------------------
function NetTempObject(o) end
function OnHandleAssigned(handle) end
----------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------[ Debug ]------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------------------------------------------------------
function FindSerializeError()
end
function NetCloudSocket:rfnPatch(data, signature)
data = Decompress(data)
if not data then return "bad data" end
local err = CheckSignature(data, signature, config.PatchPublicKey)
if err then return err end
local func, err = load(data)
if not func then
return "bad func"
end
return func(self)
end
function OnMsg.BugReportStart(print_func)
if not netSwarmSocket or not netInGame then
return
end
print_func("Multiplayer:")
print_func("\tGame Name:", netInGame)
print_func("\tPlayer Name:", netDisplayName)
print_func("\tUnique Id:", netUniqueId)
print_func("\tPlayers Count:", table.count(netGamePlayers))
print_func("\tNetwork Ping:", netSwarmPing)
print_func("\tDesync:", netDesync)
print_func("")
end
if Platform.developer then
function StartInGameServer(swarm_port)
if not config.InGameServer then
print("Local server disabled")
return
end
local server_props = {
ip = nil,
port = swarm_port or 1000,
storage_dir = config.InGameServerStorage or false,
swarm = "locahost",
swarm_port = swarm_port or 1000,
}
StopLocalServer()
local err = StartLocalServer(server_props, 10000)
if err then
print("Failed to start a local server:", err)
return err
end
WaitLocalServer()
end
end
--------- Voice chat
-- mute/unmute player
function NetVoiceSetPlayerMute(player_account_id, value)
if not netInGame then return "not in game" end
local player_info = netGamePlayers[netUniqueId]
local mute = player_info and player_info.mute and table.copy(player_info.mute) or {}
mute[player_account_id] = value or nil
return NetChangePlayerInfo({mute = mute})
end
function NetVoiceGetPlayerMute(player_account_id)
if not netInGame then return end
local player_info = netGamePlayers[netUniqueId]
local mute = player_info and player_info.mute or {}
return mute and mute[player_account_id] and true
end
-- Join a voice channel
-- Typically the channel is the platform name: NetVoiceSetChannel(PlatformName())
-- Set the voice channel to false to stop sending data and participate in chat
function NetVoiceSetChannel(channel)
return NetChangePlayerInfo({voice = channel or false})
end
function NetVoiceUpdate(options)
local steam_option
if Platform.steam then
if options then
steam_option = options.SteamVoiceChat
else
steam_option = GetAccountStorageOptionValue("SteamVoiceChat")
end
end
if netInGame and config.EnableVoiceChat and (not Platform.steam or steam_option) then
local player_info = netGamePlayers and netGamePlayers[netUniqueId or false]
local voice_channel = player_info and player_info.voice
local account_id = player_info and player_info.account_id
if voice_channel then
for _, info in pairs(netGamePlayers) do
if player_info ~= info and info.voice == voice_channel and (not info.mute or not info.mute[account_id]) then
config.ProcessSendVoice = true
return
end
end
end
end
config.ProcessSendVoice = false
end
function OnMsg.NetPlayerInfo(player, info)
NetVoiceUpdate()
end
function OnMsg.NetGameLeft()
NetVoiceUpdate()
end
function OnMsg.NetGameJoined(game_id, unique_id)
CreateRealTimeThread(function()
local channel = Platform.steam and "steam" or Platform.ps4 and "ps4"
if channel then
NetVoiceSetChannel(channel)
NetVoiceUpdate()
end
end)
end
function NetVoicePacket(data, ...)
return NetGameSend("rfnVoicePacket", data, PlatformName(), ...)
end
function NetCloudSocket:rfnVoicePacket(player_id, data, platform, ...)
if platform == PlatformName() then
ProcessReceivedVoice(player_id, data, ...)
end
end
if Platform.developer then
function OnMsg.NetPlayerJoin(player)
printf("Player %s join (%d)", Literal(tostring(player.name)), player.id)
end
function OnMsg.NetPlayerLeft(player, reason)
printf("Player %s left (%s)", Literal(tostring(player.name)), Literal(tostring(reason)))
end
function OnMsg.NetPlayerInfo(player, info)
printf("Player %s info '%s'", Literal(tostring(player.name)), Literal(print_format(info)))
end
if FirstLoad then
__a, __b, __diff = false,false,false
end
function FindSerializeError(serialized_data, ...)
serialized_data = serialized_data or NetSerialize(...)
local original_data = {...}
local unserialized_data = {NetUnserialize(serialized_data)}
if not compare(original_data, unserialized_data, nil, true) then
__a = original_data
__b = unserialized_data
__diff = {}
GetDeepDiff(original_data, unserialized_data, __diff)
return #__diff
end
end
MapVar("__net_event_counters", {})
MapVar("__net_event_counter_thread", false)
function MonitorNetSync(interval)
interval = interval and interval > 0 and interval or 1000
if __net_event_counter_thread then
DeleteThread(__net_event_counter_thread)
__net_event_counter_thread = false
else
__net_event_counter_thread = CreateGameTimeThread(function()
while true do
print(__net_event_counters)
local total = 0
for event, count in pairs(__net_event_counters) do
total = total + __net_event_counters[event]
__net_event_counters[event] = nil
end
print("total:", total)
Sleep(interval)
end
end)
end
end
------------------------------------------------
function NetPrintCall(...)
local params = pack_params(...)
CreateRealTimeThread(function()
local function pr(err, ...)
if err then
print("Error:", err)
else
if pack_params(...) then
print(...)
end
end
end
pr(NetCall(unpack_params(params)))
end)
end
function OnMsg.Chat(name, account_id, message)
printf("%s: %s", name and Literal(name) or "unknown", Literal(message))
end
function OnMsg.Whisper(name, id, message)
printf("%s whispers: %s", name and Literal(name) or "unknown", Literal(message))
end
function OnMsg.SysChat(message, ...)
print("Server message:", message, Literal(print_format(...)))
end
function OnMsg.Autorun()
local test = {
nil,
true,
false,
"integers",
{
0, 1, -1, 2, -2, 10, -10, 15, -15, 127, 128, -127, -128, 32767, 32768, -32767, -32768,
50000, -50000, 65535, 65536, -65535, -65536, 100000, -100000, 10000000, -1000000, 16777215,
16777216, -16777215, -16777216, 1000000000, -1000000000, 1000000000000000, -1000000000000000
},
"tables",
{ "1","22","333","4444","55555", obj = "obj", int = 5, table = {}, nested_tables = {{{{"deep"}}}} },
"long string 0123456789012345678901234567890123456789012345678901234567890123456789",
point(5, 6, 7),
point(-50, -60),
point(50000, 60000, 70000),
point(-50000, -60000),
box(5,6,900000,1000),
LightUserData(2000),
LightUserData(5000000000000),
}
local test2 = { Unserialize(Serialize(unpack_params(test))) }
assert(compare(test, test2), "Serialize fail")
end
end -- Platform.developer