if Libs.Network ~= "async" then return end local IsValid = IsValid MapVar("NetObjects", {}, weak_keys_meta) function NetTempObject(o) NetObjects[o or false] = false end function NetObject(o) NetObjects[o or false] = true end local function IsNetObj(o) local net_obj = NetObjects[o] return net_obj ~= false and IsValid(o) and (net_obj or not IsValid(o:GetParent())) and o.__ancestors.Object and o.handle and true end local mp_print = Platform.developer and print or function() end local IsNetObj = IsNetObj -- InteractionRand MapVar("InteractionTypeSeeds", {}) -- keeps hash values for the interaction type names MapGameTimeRepeat("InteractionRandScheduleReset", nil, function() WaitServerTick(10000) ResetInteractionRand(ServerTime() / 10000) end) function ValidateInteractionSeeds() for int_type, obj_to_target in pairs(InteractionSeeds) do for obj, target_to_seed in pairs(obj_to_target) do if obj and not IsValid(obj) then obj_to_target[obj] = nil else for target in pairs(target_to_seed) do if target and not IsValid(target) then target_to_seed[target] = nil end end if next(target_to_seed) == nil then obj_to_target[obj] = nil end end end if next(obj_to_target) == nil then InteractionSeeds[int_type] = nil end end end function ClearInteraction(int_type, obj) while obj and obj.NetOwner do obj = obj.NetOwner end if not IsValid(obj) or not IsNetObj(obj) then return false end local obj_to_target = InteractionSeeds[int_type or "none"] if obj_to_target then obj_to_target[obj] = nil end return true end function InteractionRand(max, int_type, obj, target) int_type = int_type or "none" assert(type(int_type) == "string") -- avoid having objects with random handle, they cannot be unserialized remotely while obj and obj.NetOwner do obj = obj.NetOwner end obj = IsNetObj(obj) and obj or false target = target or false local obj_to_target = InteractionSeeds[int_type] if not obj_to_target then obj_to_target = setmetatable({}, weak_keys_meta) InteractionSeeds[int_type] = obj_to_target end local target_to_seed = obj_to_target[obj] if not target_to_seed then target_to_seed = setmetatable({}, weak_keys_meta) obj_to_target[obj] = target_to_seed end while target and target.NetOwner do target = target.NetOwner end local interaction_seed = target_to_seed[target] if not interaction_seed then local type_seed = InteractionTypeSeeds[int_type] if not type_seed then type_seed = xxhash(int_type) InteractionTypeSeeds[int_type] = type_seed end interaction_seed = bxor(InteractionSeed, type_seed, obj and obj.handle or 0, target and target.handle or 0) end local rand rand, interaction_seed = BraidRandom(interaction_seed, max) target_to_seed[target] = interaction_seed NetUpdateHash("InteractionRand", rand, max, int_type, obj, target) return rand, interaction_seed end -------------------------------------------------[ WAIT HANDLE RESOLVE ]------------------------------------------------------------------- if FirstLoad then HandleResolveMsg = {} end function OnHandleAssigned(handle) if HandleResolveMsg[handle] then Msg(HandleResolveMsg[handle]) HandleResolveMsg[handle] = nil end end function WaitResolveHandle(handle) local obj = HandleToObject[handle] if not obj then HandleResolveMsg[handle] = HandleResolveMsg[handle] or {} WaitMsg(HandleResolveMsg[handle], 1000) HandleResolveMsg[handle] = nil obj = HandleToObject[handle] end return obj end -------------------------------------------------[ Net map utilities ]------------------------------------------------------ local function QueryNetObjects(only_non_permanent) return MapGet(true, "Object", function(o) return IsNetObj(o) and (not only_non_permanent or o:GetGameFlags(const.gofPermanent) == 0) end) end local function SerializeInteractionSeeds() return next(InteractionSeeds) ~= nil and NetSerialize(InteractionSeeds) or nil end local function UnserializeInteractionSeeds(interact) local seeds = interact and NetUnserialize(interact) or {} for int_type, obj_to_target in pairs(seeds) do setmetatable(obj_to_target, weak_keys_meta) for obj, target_to_seed in pairs(obj_to_target) do setmetatable(target_to_seed, weak_keys_meta) end end return seeds end if Platform.developer then SerializeInteractionSeeds = function() local map1 = {} for int_type, obj_to_target in pairs(InteractionSeeds) do local map2 = setmetatable({}, weak_keys_meta) for obj, target_to_seed in pairs(obj_to_target) do local map3 = setmetatable({}, weak_keys_meta) for target, seed in pairs(target_to_seed) do if not target then map3[false] = seed elseif target.handle then map3[target.handle] = seed else assert(false, "Interaction target without handle: " .. tostring(target.class)) end end if not obj then map2[false] = map3 elseif obj.handle then map2[obj.handle] = map3 else assert(false, "Interaction object without handle: " .. tostring(obj.class)) end end map1[int_type] = map2 end return next(map1) ~= nil and NetSerialize(map1) or nil end UnserializeInteractionSeeds = function(interact) local seeds = interact and NetUnserialize(interact) or {} local map1 = {} local count = 0 for int_type, obj_handle_to_target in pairs(seeds) do local map2 = {} for obj_handle, target_handle_to_seed in pairs(obj_handle_to_target) do local map3 = {} for target_handle, seed in pairs(target_handle_to_seed) do if type(target_handle) == "number" then local target = HandleToObject[target_handle] if target then map3[target] = seed else assert(false, string.format("Missing interaction target %d from '%s'", target_handle, int_type)) end else map3[target_handle] = seed end count = count + 1 end if type(obj_handle) == "number" then local obj = HandleToObject[obj_handle] if obj then map2[obj] = map3 else printf("Missing interaction object %d from '%s'", obj_handle, int_type) assert(false, "Missing interaction object!") end else map2[obj_handle] = map3 end end map1[int_type] = map2 end return map1 end end function NetGetRandData() ValidateInteractionSeeds() local rand_data = { map = MapLoadRandom, interact = SerializeInteractionSeeds(), seed = InteractionSeed, } return rand_data end function NetSetRandData(rand_data) MapLoadRandom = rand_data.map InteractionSeeds = UnserializeInteractionSeeds(rand_data.interact) InteractionSeed = rand_data.seed end function NetGetGameState() local success, err, state, state_local = procall(function() local non_permanent_objs = QueryNetObjects(true) local net_objs = QueryNetObjects() local scenario_data, scenario_data_local = NetGetScenarioData() local game_data = {} game_data.map_name = GetMapName() game_data.map_hash = mapdata.NetHash game_data.paused = not not PauseReasons.UserPaused game_data.rand = NetGetRandData() game_data.objects = NetGetNetObjs(non_permanent_objs) game_data.props = NetGetNetObjProps(non_permanent_objs) game_data.dynamic = NetGetDynamicData(net_objs) game_data.scenario = scenario_data game_data.globals = {} Msg("NetStateGet", game_data.globals) local state, err = NetSerialize(game_data) if not state then assert(false, "Network serialization error: " .. tostring(err)) return err end FindSerializeError(state, game_data) local game_data_local = {} game_data_local.scenario = scenario_data_local game_data_local.objects = NetGetLocalObjData() game_data_local.MapLoadRandom = MapLoadRandom local state_local, err = NetSerialize(game_data_local) if not state_local then assert(false, "Network serialization error: " .. tostring(err)) return err end FindSerializeError(state_local, game_data_local) local compressed_state = Compress(state) local compressed_state_local = Compress(state_local) return false, compressed_state, compressed_state_local end) if not success then return err end return err, state, state_local end function NetSetGameState(compressed_data, compressed_data_local) if not compressed_data then return end local game_data = Decompress(compressed_data) if not game_data then return "decompress" end game_data = NetUnserialize(game_data) if not game_data then return "unserialize" end local game_data_local, err_local if compressed_data_local then local data = Decompress(compressed_data_local) if not data then err_local = "decompress" else data = NetUnserialize(data) if not data then err_local = "unserialize" elseif data.MapLoadRandom == game_data.rand.map then game_data_local = data else err_local = "version" end end end assert(game_data.map_name == GetMapName()) if game_data.map_hash ~= mapdata.NetHash then print("[NET ERROR] The local map is different version") --return "map" end local success, error = procall(function() -- remove all non-permanent objects - they will be copied from the host local non_permanent_objs = QueryNetObjects(true) DoneObjects(non_permanent_objs) NetCreateNetObjs(game_data.objects) NetSetNetObjProps(game_data.props) NetSetDynamicData(game_data.dynamic) NetSetRandData(game_data.rand) NetSetScenarioData(game_data.scenario, game_data_local and game_data_local.scenario) if game_data_local then SetLocalObjData(game_data_local.objects) end SetTimeFactor(const.DefaultTimeFactor) SetPause(game_data.paused or false) Msg("NetStateSet", game_data.globals) --> allow the dynamic objects to init some parts based on their surrounding environment or whatever) end) if not success then assert(false, tostring(error)) return "failed", err_local end return nil, err_local end function NetGetNetObjs(objects) local object_data = {} local count = 0 for _, obj in ipairs(objects) do object_data[count + 1] = obj.class object_data[count + 2] = obj.handle object_data[count + 3] = obj:GetPos() count = count + 3 end return object_data end function NetCreateNetObjs(object_data) for i=1,#object_data, 3 do local class, handle, pos = unpack_params(object_data, i, i + 2) local obj = PlaceObject(class, { handle = handle }) if obj then Object.SetPos(obj, pos) --> don't use the object's original method to avoid custom logic else mp_print("Cannot create an object", class, "with handle", handle) end end end local network_ignore_props = { Pos = true } function NetGetNetObjProps(objects) local prop_objdata = {} for i=1,#objects do local obj = objects[i] local modified_props = GetModifiedProperties( obj, nil, network_ignore_props ) if modified_props then modified_props.__obj = obj AddNetObjDebugInfo(obj, modified_props) local serialized_props, err = NetSerialize(modified_props) if serialized_props and not FindSerializeError(serialized_props, modified_props) then prop_objdata[#prop_objdata + 1] = serialized_props else mp_print("Serialize properties failed for", format_value(obj), err) if Platform.developer then DebugPrint(format_value(modified_props)) end assert(false, "Serialize properties failed") end end end return prop_objdata end function NetSetNetObjProps(prop_objdata) local warned = false for i = 1,#prop_objdata do local modified_props = NetUnserialize(prop_objdata[i]) local obj = modified_props.__obj if obj then local props = obj:GetProperties() for j = 1, #props do local id = props[j].id local value = modified_props[id] if value ~= nil then local success, error = procall(obj.SetProperty, obj, id, value) if not success then mp_print("Failed to set", obj.class, "property", id, ":", error) end end end else ShowNetObjDebugInfo(modified_props) end end end function NetGetDynamicData(objects) local dynamic_objdata = {} for _, obj in ipairs(objects) do local data = {} procall(obj.GetDynamicData, obj, data) if next(data) then data.__obj = obj AddNetObjDebugInfo(obj, data) local serialized_data, err = NetSerialize(data) if serialized_data and not FindSerializeError(serialized_data, data) then dynamic_objdata[#dynamic_objdata + 1] = serialized_data else mp_print("Serialize dynamic data failed for", format_value(obj), err) assert(false, "Serialize dynamic data failed") end end end return dynamic_objdata end function NetSetDynamicData(dynamic_objdata) for i = 1,#dynamic_objdata do local data = NetUnserialize(dynamic_objdata[i]) local obj = data.__obj if obj then local success, error = procall(obj.SetDynamicData, obj, data) if not success then mp_print("Failed to set", obj.class, "dynamic data:", error) end else ShowNetObjDebugInfo(data) end end end function NetAdjustTime(time) return time and (GameTime() - time) or nil end function NetGetLocalObjData() local objects = MapGet(true, "LootObj", function(o) return not IsNetObj(o) and o:GetGameFlags(const.gofPermanent) == 0 end) local objdata = {} for _, obj in ipairs(objects) do local data = {} data.__class = obj.class data.__pos = obj:GetPos() data.__props = GetModifiedProperties( obj, nil, network_ignore_props ) procall(obj.GetDynamicData, obj, data) if next(data) then local serialized_data, err = NetSerialize(data) if serialized_data and not FindSerializeError(serialized_data, data) then objdata[#objdata + 1] = serialized_data else mp_print("Serialize dynamic data failed for", format_value(obj), err) assert(false, "Serialize dynamic data failed") end end end return objdata end function SetLocalObjData(objdata) for i = 1, #objdata do local data = NetUnserialize(objdata[i]) local obj = PlaceObject(data.__class) if obj then NetTempObject(obj) Object.SetPos(obj, data.__pos) --> don't use the object's original method to avoid custom logic local modified_props = data.__props local props = obj:GetProperties() for j = 1, #props do local id = props[j].id local value = modified_props[id] if value ~= nil then local success, error = procall(obj.SetProperty, obj, id, value) if not success then mp_print("Failed to set", obj.class, "property", id, ":", error) end end end local success, error = procall(obj.SetDynamicData, obj, data) if not success then mp_print("Failed to set", obj.class, "dynamic data:", error) end end end end -- Pause/Resume function SetPause(pause) local paused = not not PauseReasons.UserPaused if paused == pause then return end if pause then Pause("UserPaused") else Resume("UserPaused") end end function TogglePause() local paused = not not PauseReasons.UserPaused if netSwarmSocket then NetEchoEvent("SetPause", not paused) else local pause = not paused SetPause(pause) return pause end end function NetEvents.SetPause(pause) SetPause(pause) end -------------------------------------------------[ Server time ]------------------------------------------------------ function ServerTime() return IsRealTimeThread() and (RealTime() + netServerRealTimeDelta) or (GameTime() + netServerGameTimeDelta) end function NetSetServerTime(server_time) if server_time then local estimated_rt = RealTime() + netServerRealTimeDelta local estimated_gt = GameTime() + netServerGameTimeDelta log("servertime", server_time, ", gt error", estimated_gt - server_time, ", rt error", estimated_rt - server_time) local new_rt_delta = server_time - RealTime() local new_gt_delta = server_time - GameTime() if netServerRealTimeDelta ~= new_rt_delta or netServerGameTimeDelta ~= new_gt_delta then netServerRealTimeDelta = new_rt_delta netServerGameTimeDelta = new_gt_delta Msg("ServerTimeUpdate") end else netServerRealTimeDelta = 0 netServerGameTimeDelta = 0 end end function WaitServerTime(time) while WaitMsg("ServerTimeUpdate", time - ServerTime()) do end end function WaitServerTick(tick_interval, tick_phase) WaitServerTime(ServerTime() / tick_interval * tick_interval + tick_interval + (tick_phase or 0)) end function OnMsg.NetPing(server_time) NetSetServerTime(server_time) end -- SetTimeFactor if FirstLoad then __SetTimeFactor = SetTimeFactor end NetEvents.SetTimeFactor = __SetTimeFactor function SetTimeFactor(time_factor, sync) if sync then NetEchoEvent("SetTimeFactor", time_factor) else __SetTimeFactor(time_factor) end end -- Debug function AddNetObjDebugInfo() end function ReportMissingObj() end function ShowNetObjDebugInfo() end if Platform.developer then MapVar("__missing_net_objs", {}) function ReportMissingObj(handle, class, pos) if handle and not __missing_net_objs[handle] then __missing_net_objs[handle] = true mp_print("Missing", class or "object", "with handle", handle, "at", pos) end end function AddNetObjDebugInfo(obj, data) data.__class = obj.class data.__handle = obj.handle data.__pos = obj:GetVisualPos() end function ShowNetObjDebugInfo(data) ReportMissingObj(data.__handle, data.__class, data.__pos) end end