|
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 = {} |
|
|
|
|
|
HashLogSize = rawget(_G, "HashLogSize") or 16 |
|
HashLogPath = rawget(_G, "HashLogPath") or "" |
|
|
|
|
|
netSimulateLagAvg = 0 |
|
netSimulateLagAmp = 0 |
|
netSimulateLagLastTime = 0 |
|
end |
|
|
|
|
|
|
|
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 |
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
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 |
|
|
|
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 |
|
|
|
|
|
|
|
|
|
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 |
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
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") |
|
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 |
|
|
|
|
|
|
|
|
|
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 |
|
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) |
|
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 |
|
|
|
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 |
|
|
|
|
|
|
|
|
|
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) |
|
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 |
|
|
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
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 |
|
|
|
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 |
|
|
|
|
|
|
|
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 |
|
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 |
|
|
|
|
|
|
|
if FirstLoad then |
|
netKeepAliveThread = false |
|
end |
|
|
|
function OnMsg.NetConnect() |
|
assert(not netKeepAliveThread) |
|
|
|
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 |
|
|
|
|
|
|
|
function NetGossip(gossip, ...) |
|
if gossip and netAllowGossip then |
|
|
|
return NetSend("rfnGossip", gossip, ...) |
|
end |
|
end |
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
function NetTempObject(o) end |
|
function OnHandleAssigned(handle) end |
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
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 |
|
|