diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ed29ee712..5d2be59e8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -114,10 +114,10 @@ function ExampleClass:yetAnotherPublicFunction(param) end ---Constructor of the class. This is mandatory! ----@param objectToInheritFrom ExampleClass This can be any object that you want to inherit from. +---@param objectOfExampleClass ExampleClass ---@return ExampleClass -function ExampleClass:new(objectToInheritFromOrObjectItself, customProfiler, importClass, foo, bar) - local exampleObject = objectToInheritFromOrObjectItself or self or {} -- Use self if this is called as a class constructor +function ExampleClass:new(objectOfExampleClass, customProfiler, importClass, foo, bar) + objectOfExampleClass = objectOfExampleClass or self or {} -- Use self if this is called as a class constructor setmetatable(exampleObject, self) self.__index = self @@ -130,7 +130,7 @@ function ExampleClass:new(objectToInheritFromOrObjectItself, customProfiler, imp self.bar = bar or require("bar"):new() customProfiler:stop("ExampleClass:new", cpc) - return exampleObject + return objectOfExampleClass end ---Still need to return the class table at the end of the file, diff --git a/mods/noita-mp/files/scripts/Gui.lua b/mods/noita-mp/files/scripts/Gui.lua index 7f9233efd..41c246362 100644 --- a/mods/noita-mp/files/scripts/Gui.lua +++ b/mods/noita-mp/files/scripts/Gui.lua @@ -1,6 +1,25 @@ ---Everything regarding ImGui: Credits to @dextercd --- @class Gui -local Gui = {} +local Gui = { + --[[ Imports ]] + + ---@type Client + client = nil, + ---@type CustomProfiler + customProfiler = nil, + ---@type GuidUtils + guidUtils = nil, + ---@type ImGui + imGui = nil, + ---@type MinaUtils + minaUtils = nil, + ---@type NoitaMpSettings + noitaMpSettings = nil, + + --[[ Attributes ]] + + +} if not load_imgui then function OnWorldInitialized() @@ -14,14 +33,6 @@ if not load_imgui then error("Missing ImGui.", 2) end -local imGui = load_imgui({ version = "1.11.0", mod = "noita-mp" }) - -local CustomProfiler = require("CustomProfiler") -local NoitaMpSettings = require("NoitaMpSettings") -local MinaUtils = require("MinaUtils") -local GuidUtils = require("GuidUtils") -local Client = require("Client") - --- Can't know the width before creating the window.. Just an initial value, it's updated to the real value once we can call imgui.GetWindowWidth() local menuBarWidth = 100 local function getMenuBarPosition(position) @@ -710,4 +721,33 @@ function Gui.new() return self end +---Gui constructor. +---@param guiObject Gui|nil optional +---@param client Client required +---@param customProfiler CustomProfiler required +---@param guidUtils GuidUtils|nil optional +---@param minaUtils MinaUtils|nil optional +---@param noitaMpSettings NoitaMpSettings|nil optional +---@return Gui +function Gui:new(guiObject, client, customProfiler, guidUtils, minaUtils, noitaMpSettings) + guiObject = guiObject or self or {} -- Use self if this is called as a class constructor + setmetatable(guiObject, self) + self.__index = self + + local cpc = customProfiler:start("ExampleClass:new") + + -- Initialize all imports to avoid recursive imports + self.client = client or error("Client is required!", 2) + self.noitaMpSettings = noitaMpSettings or require("NoitaMpSettings") + :new(nil, customProfiler, self, nil, nil, nil, nil, nil, nil) + self.customProfiler = customProfiler or require("CustomProfiler") + :new(nil, nil, noitaMpSettings, nil, nil, nil, nil) + self.guidUtils = guidUtils or require("GuidUtils")--:new() + self.imGui = load_imgui({ version = "1.11.0", mod = "noita-mp" }) + self.minaUtils = minaUtils or require("MinaUtils"):new() + + customProfiler:stop("ExampleClass:new", cpc) + return guiObject +end + return Gui diff --git a/mods/noita-mp/files/scripts/NoitaMpSettings.lua b/mods/noita-mp/files/scripts/NoitaMpSettings.lua index 75c03278e..504a389be 100644 --- a/mods/noita-mp/files/scripts/NoitaMpSettings.lua +++ b/mods/noita-mp/files/scripts/NoitaMpSettings.lua @@ -5,8 +5,8 @@ local NoitaMpSettings = { ---@type CustomProfiler customProfiler = nil, - ---@type guiI - guiI = nil, + ---@type Gui + gui = nil, ---@type FileUtils fileUtils = nil, ---@type json @@ -150,7 +150,7 @@ function NoitaMpSettings:get(key, dataType) return convertToDataType(self, "", dataType) end self.customProfiler:stop("NoitaMpSettings.get", cpc) - return convertToDataType(self, cachedSettings[key], dataType) + return convertToDataType(self, self.cachedSettings[key], dataType) end ---Loads the settings from the settings file and put those into the cached settings. @@ -173,15 +173,15 @@ function NoitaMpSettings:save() end self.fileUtils.WriteFile(settingsFilePath, self.json.encode(self.cachedSettings)) - if self.guiI then - self.guiI.setShowSettingsSaved(true) + if self.gui then + self.gui.setShowSettingsSaved(true) end end ---NoitaMpSettings constructor. ----@param noitaMpSettingsObject NoitaMpSettings|nil +---@param noitaMpSettings NoitaMpSettings|nil ---@param customProfiler CustomProfiler|nil ----@param guiI guiI required +---@param gui Gui required ---@param fileUtils FileUtils|nil ---@param json json|nil ---@param lfs LuaFileSystem|nil @@ -189,23 +189,44 @@ end ---@param utils Utils|nil ---@param winapi winapi|nil ---@return NoitaMpSettings -function NoitaMpSettings:new(noitaMpSettingsObject, customProfiler, guiI, fileUtils, json, lfs, logger, utils, winapi) - local noitaMpSettings = noitaMpSettingsObject or self or {} -- Use self if this is called as a class constructor - setmetatable(noitaMpSettings, self) - self.__index = self +function NoitaMpSettings:new(noitaMpSettings, customProfiler, gui, fileUtils, json, lfs, logger, utils, winapi) + noitaMpSettings = setmetatable(noitaMpSettings or self, NoitaMpSettings) -- Initialize all imports to avoid recursive imports - self.customProfiler = customProfiler or require("CustomProfiler"):new(nil, nil, self, nil, nil, nil, nil) + if not noitaMpSettings.customProfiler then + self.customProfiler = customProfiler or require("CustomProfiler"):new(nil, nil, self, nil, nil, nil, nil) + end local cpc = self.customProfiler:start("NoitaMpSettings:new") - self.guiI = guiI or error("NoitaMpSettings:new requires a guiI object", 2) - self.fileUtils = fileUtils or require("FileUtils"):new() - self.json = json or require("json") - self.lfs = lfs or require("lfs") - self.logger = logger or require("Logger"):new(nil, customProfiler) - self.utils = utils or require("Utils"):new() - self.winapi = winapi or require("winapi") - - customProfiler:stop("ExampleClass:new", cpc) + + if not noitaMpSettings.gui then + self.gui = gui --or error("NoitaMpSettings:new requires a Gui object", 2) + end + + if not noitaMpSettings.fileUtils then + self.fileUtils = fileUtils or self.customProfiler.fileUtils or require("FileUtils")--:new() + end + + if not noitaMpSettings.json then + self.json = json or require("json") + end + + if not noitaMpSettings.lfs then + self.lfs = lfs or require("lfs") + end + + if not noitaMpSettings.logger then + self.logger = logger or require("Logger"):new(nil, self.customProfiler) + end + + if not noitaMpSettings.utils then + self.utils = utils or self.customProfiler.utils or require("Utils")--:new() + end + + if not noitaMpSettings.winapi then + self.winapi = winapi or self.customProfiler.winapi or require("winapi") + end + + self.customProfiler:stop("ExampleClass:new", cpc) return noitaMpSettings end diff --git a/mods/noita-mp/files/scripts/net/Client.lua b/mods/noita-mp/files/scripts/net/Client.lua index ff4e234e5..aeefa47c5 100644 --- a/mods/noita-mp/files/scripts/net/Client.lua +++ b/mods/noita-mp/files/scripts/net/Client.lua @@ -1,40 +1,15 @@ ----@class Client Inherit client class from sock.lua#newClient -local Client = { - --- Imports - customProfiler = require("CustomProfiler"), - entityUtils = require("EntityUtils"), - guidUtils = require("GuidUtils"), - logger = require("Logger"), - messagePack = require("MessagePack"), - minaUtils = require("MinaUtils"), - networkUtils = require("NetworkUtils"), - noitaMpSettings = require("NoitaMpSettings"), - noitaPatcherUtils = require("NoitaPatcherUtils"), - sock = require("sock"), - zstandard = require("zstd"), - - --- Attributes - iAm = "CLIENT", - name = Client.noitaMpSettings.get("noita-mp.nickname", "string"), - -- guid might not be set here or will be overwritten at the end of the constructor. @see setGuid - guid = self.noitaMpSettings.get("noita-mp.guid", "string"), - nuid = nil, - acknowledgeMaxSize = 500, - transform = { x = 0, y = 0 }, - health = { current = 99, max = 100 }, - serverInfo = {}, - otherClients = {}, - missingMods = nil, - requiredMods = nil, - syncedMods = false -} - - ----Defualt enhanced serialization function +local sock = require("sock") +---@class Client : SockClient Inherit client class from sock.lua#newClient +local Client = setmetatable({ + -- when a class inherits from another class, all additional imports and attributes are defined in :new() ! +}, { __index = sock.getClientClass() }) +Client.__index = Client + +---Default enhanced serialization function ---@param value any ---@return unknown function Client:serialize(value) - local cpc = self.customProfiler:start("ClientInit.setConfigSettings.serialize") + local cpc = self.customProfiler:start("Client.setConfigSettings.serialize") self.logger:trace(self.logger.channels.network, ("Serializing value: %s"):format(value)) local serialized = self.messagePack.pack(value) @@ -54,15 +29,15 @@ function Client:serialize(value) self.logger:debug(self.logger.channels.network, ("Serialized and compressed value: %s"):format(compressed)) zstd:free() - self.customProfiler:stop("ClientInit.setConfigSettings.serialize", cpc2) + self.customProfiler:stop("Client.setConfigSettings.serialize", cpc2) return compressed end ----Defualt enhanced serialization function +---Default enhanced serialization function ---@param value any ---@return unknown function Client:deserialize(value) - local cpc = self.customProfiler:start("ClientInit.setConfigSettings.deserialize") + local cpc = self.customProfiler:start("Client.setConfigSettings.deserialize") self.logger:debug(self.logger.channels.network, ("Serialized and compressed value: %s"):format(value)) local zstd, zstdError = self.zstandard:new() -- new zstd instance for every serialization, otherwise it will crash @@ -76,1022 +51,1091 @@ function Client:deserialize(value) error("Error while decompressing: " .. err, 2) end self.logger:debug(self.logger.channels.network, ("Uncompressed size: %s"):format(string.len(decompressed))) - local deserialized = messagePack.unpack(decompressed) - Logger.debug(Logger.channels.network, ("Deserialized and uncompressed value: %s"):format(deserialized)) + local deserialized = self.messagePack.unpack(decompressed) + self.logger:debug(self.logger.channels.network, ("Deserialized and uncompressed value: %s"):format(deserialized)) zstd:free() - CustomProfiler.stop("ClientInit.setConfigSettings.deserialize", cpc3) + self.customProfiler:stop("Client.setConfigSettings.deserialize", cpc) return deserialized end - - - - --- Set clients guid - local function setGuid() - local cpc1 = CustomProfiler.start("ClientInit.setGuid") - local guid = NoitaMpSettings.get("noita-mp.guid", "string") - - if guid == "" or GuidUtils.isPatternValid(guid) == false then - guid = GuidUtils:getGuid() - NoitaMpSettings.set("noita-mp.guid", guid) - self.guid = guid - Logger.debug(Logger.channels.network, "Clients guid set to " .. guid) - else - Logger.debug(Logger.channels.network, "Clients guid was already set to " .. guid) - end - - if DebugGetIsDevBuild() then - guid = guid .. self.iAm - end - CustomProfiler.stop("ClientInit.setGuid", cpc1) +---Sets the guid of the client. +---@param self Client +---@param guid string|nil +local setGuid = function(self, guid) + local cpc1 = self.customProfiler:start("Client.setGuid") + local guid = self.noitaMpSettings:get("noita-mp.guid", "string") + + if self.utils.IsEmpty(guid) or self.guidUtils.isPatternValid(guid) == false then + guid = self.guidUtils:getGuid() + self.noitaMpSettings.set("noita-mp.guid", guid) + self.guid = guid + self.logger:debug(self.logger.channels.network, "Clients guid set to " .. guid) + else + self.logger:debug(self.logger.channels.network, "Clients guid was already set to " .. guid) end - - --- Send acknowledgement - local function sendAck(networkMessageId, event) - local cpc2 = CustomProfiler.start("ClientInit.sendAck") - if not event then - error("event is nil", 2) - end - local data = { networkMessageId, event, NetworkUtils.events.acknowledgement.ack, os.clock() } - self:send(NetworkUtils.events.acknowledgement.name, data) - Logger.debug(Logger.channels.network, ("Sent ack with data = %s"):format(Utils.pformat(data))) - CustomProfiler.stop("ClientInit.sendAck", cpc2) + if DebugGetIsDevBuild() then + guid = guid .. self.iAm end + self.customProfiler:stop("Client.setGuid", cpc1) +end - --- onAcknowledgement - local function onAcknowledgement(data) - local cpc3 = CustomProfiler.start("ClientInit.onAcknowledgement") - Logger.debug(Logger.channels.network, "onAcknowledgement: Acknowledgement received.", Utils.pformat(data)) - - if Utils.IsEmpty(data.networkMessageId) then - error(("onAcknowledgement data.networkMessageId is empty: %s"):format(data.networkMessageId), 2) - end - - if not data.networkMessageId then - error(("Unable to get acknowledgement with networkMessageId = %s, data = %s, peer = %s") - :format(networkMessageId, Utils.pformat(data), Utils.pformat(self)), 2) - end +---Sends acknowledgement for a specific network event. +---@private +---@param self Client +---@param networkMessageId number +---@param event string @see self.networkUtils.events +local sendAck = function(self, networkMessageId, event) + local cpc = self.customProfiler:start("Client.sendAck") + if not event then + error("event is nil", 2) + end + local data = { networkMessageId, event, self.networkUtils.events.acknowledgement.ack, os.clock() } + self:send(self.networkUtils.events.acknowledgement.name, data) + self.logger.debug(self.logger.channels.network, ("Sent ack with data = %s"):format(self.utils.pformat(data))) + self.customProfiler:stop("Client.sendAck", cpc) +end - if Utils.IsEmpty(data.event) then - error(("onAcknowledgement data.event is empty: %s"):format(data.event), 2) - end - if Utils.IsEmpty(data.status) then - error(("onAcknowledgement data.status is empty: %s"):format(data.status), 2) - end +---Callback when acknowledgement received. +---@private +---@param self Client +---@param data table data = { "networkMessageId", "event", "status", "ackedAt" } +local onAcknowledgement = function(self, data) + local cpc = self.customProfiler:start("Client.onAcknowledgement") + self.logger:debug(self.logger.channels.network, "onAcknowledgement: Acknowledgement received.", self.utils.pformat(data)) - if Utils.IsEmpty(data.ackedAt) then - error(("onAcknowledgement data.ackedAt is empty: %s"):format(data.ackedAt), 2) - end + if self.utils.IsEmpty(data.networkMessageId) then + error(("onAcknowledgement data.networkMessageId is empty: %s"):format(data.networkMessageId), 2) + end - if not self.clientCacheId then - self.clientCacheId = GuidUtils.toNumber(peer.guid) - end + if not data.networkMessageId then + error(("Unable to get acknowledgement with networkMessageId = %s, data = %s, peer = %s") + :format(data.networkMessageId, self.utils.pformat(data), self.utils.pformat(self)), 2) + end - local cachedData = NetworkCacheUtils.get(self.guid, data.networkMessageId, data.event) - if Utils.IsEmpty(cachedData) then - NetworkCacheUtils.logAll() - error(("Unable to get cached data, because it is nil '%s'"):format(cachedData), 2) - end - if Utils.IsEmpty(cachedData.dataChecksum) or type(cachedData.dataChecksum) ~= "string" then - NetworkCacheUtils.logAll() - error(("Unable to get cachedData.dataChecksum, because it is nil '%s' or checksum is not of type string, type: %s") - :format(cachedData.dataChecksum, type(cachedData.dataChecksum)), 2) - end - -- update previous cached network message - NetworkCacheUtils.ack(self.guid, data.networkMessageId, data.event, - data.status, os.clock(), cachedData.sendAt, cachedData.dataChecksum) + if self.utils.IsEmpty(data.event) then + error(("onAcknowledgement data.event is empty: %s"):format(data.event), 2) + end - if NetworkCache.size() > self.acknowledgeMaxSize then - NetworkCache.removeOldest() - end - CustomProfiler.stop("ClientInit.onAcknowledgement", cpc3) + if self.utils.IsEmpty(data.status) then + error(("onAcknowledgement data.status is empty: %s"):format(data.status), 2) end + if self.utils.IsEmpty(data.ackedAt) then + error(("onAcknowledgement data.ackedAt is empty: %s"):format(data.ackedAt), 2) + end - --- onConnect - --- Callback when connected to server. - --- @param data number not in use atm - local function onConnect(data) - local cpc4 = CustomProfiler.start("ClientInit.onConnect") - Logger.debug(Logger.channels.network, "Connected to server!", Utils.pformat(data)) + if not self.clientCacheId then + self.clientCacheId = self.guidUtils.toNumber(self.peer.guid) -- TODO where does `peer` come from? + end - if Utils.IsEmpty(data) then - error(("onConnect data is empty: %s"):format(data), 3) - end + local cachedData = self.networkCacheUtils.get(self.guid, data.networkMessageId, data.event) + if self.utils.IsEmpty(cachedData) then + self.networkCacheUtils.logAll() + error(("Unable to get cached data, because it is nil '%s'"):format(cachedData), 2) + end + if self.utils.IsEmpty(cachedData.dataChecksum) or type(cachedData.dataChecksum) ~= "string" then + self.networkCacheUtils.logAll() + error(("Unable to get cachedData.dataChecksum, because it is nil '%s' or checksum is not of type string, type: %s") + :format(cachedData.dataChecksum, type(cachedData.dataChecksum)), 2) + end + -- update previous cached network message + self.networkCacheUtils.ack(self.guid, data.networkMessageId, data.event, + data.status, os.clock(), cachedData.sendAt, cachedData.dataChecksum) - self.sendMinaInformation() + if self.networkCache.size() > self.acknowledgeMaxSize then + self.networkCache.removeOldest() + end + self.customProfiler:stop("Client.onAcknowledgement", cpc) +end - self:send(NetworkUtils.events.needModList.name, { NetworkUtils.getNextNetworkMessageId(), {}, {} }) +---Callback when connected to server. +---@private +---@param self Client +---@param data table data = { "networkMessageId", "name", "guid", "transform" } +local onConnect = function(self, data) + local cpc = self.customProfiler:start("Client.onConnect") + self.logger:debug(self.logger.channels.network, "Connected to server!", self.utils.pformat(data)) - -- sendAck(data.networkMessageId) - CustomProfiler.stop("ClientInit.onConnect", cpc4) + if self.utils.IsEmpty(data) then + error(("onConnect data is empty: %s"):format(data), 3) end + self.sendMinaInformation() - --- onConnect2 - --- Callback when one of the other clients connected. - --- @param data table data = { "name", "guid" } @see NetworkUtils.events.connect2.schema - local function onConnect2(data) - local cpc5 = CustomProfiler.start("ClientInit.onConnect2") - Logger.debug(Logger.channels.network, "Another client connected.", Utils.pformat(data)) + self:send(self.networkUtils.events.needModList.name, { self.networkUtils.getNextNetworkMessageId(), {}, {} }) - if Utils.IsEmpty(data.networkMessageId) then - error(("onConnect2 data.networkMessageId is empty: %s"):format(data.networkMessageId), 3) - end + -- sendAck(self, data.networkMessageId) + self.customProfiler:stop("Client.onConnect", cpc) +end - if Utils.IsEmpty(data.name) then - error(("onConnect2 data.name is empty: %s"):format(data.name), 3) - end - if Utils.IsEmpty(data.guid) then - error(("onConnect2 data.guid is empty: %s"):format(data.guid), 3) - end - table.insertIfNotExist(self.otherClients, { name = data.name, guid = data.guid, transofrm = data.transform }) +---Callback when one of the other clients connected. +---@private +---@param self Client +---@param data table data = { "name", "guid" } @see self.networkUtils.events.connect2.schema +local onConnect2 = function(self, data) + local cpc = self.customProfiler:start("Client.onConnect2") + self.logger:debug(self.logger.channels.network, "Another client connected.", self.utils.pformat(data)) - sendAck(data.networkMessageId, NetworkUtils.events.connect2.name) - CustomProfiler.stop("ClientInit.onConnect2", cpc5) + if self.utils.IsEmpty(data.networkMessageId) then + error(("onConnect2 data.networkMessageId is empty: %s"):format(data.networkMessageId), 3) end + if self.utils.IsEmpty(data.name) then + error(("onConnect2 data.name is empty: %s"):format(data.name), 3) + end - --- onDisconnect - --- Callback when disconnected from server. - --- @param data number data(.code) = 0 - local function onDisconnect(data) - local cpc6 = CustomProfiler.start("ClientInit.onDisconnect") - Logger.debug(Logger.channels.network, "Disconnected from server!", Utils.pformat(data)) - - if Utils.IsEmpty(data) then - error(("onDisconnect data is empty: %s"):format(data), 3) - end - - if self.serverInfo.nuid then - EntityUtils.destroyByNuid(self, self.serverInfo.nuid) - end - - -- TODO remove all NUIDS from entities. I now need a nuid-entityId-cache. - local nuid, entityId = GlobalsUtils.getNuidEntityPair(self.nuid) - NetworkVscUtils.addOrUpdateAllVscs(entityId, self.name, self.guid, nil) - - self.nuid = nil - self.otherClients = {} - self.serverInfo = {} - - -- sendAck(data.networkMessageId) - CustomProfiler.stop("ClientInit.onDisconnect", cpc6) + if self.utils.IsEmpty(data.guid) then + error(("onConnect2 data.guid is empty: %s"):format(data.guid), 3) end + table.insertIfNotExist(self.otherClients, { name = data.name, guid = data.guid, transofrm = data.transform }) - --- onDisconnect2 - --- Callback when one of the other clients disconnected. - --- @param data table data { "name", "guid" } @see NetworkUtils.events.disconnect2.schema - local function onDisconnect2(data) - local cpc7 = CustomProfiler.start("ClientInit.onDisconnect2") - Logger.debug(Logger.channels.network, "onDisconnect2: Another client disconnected.", Utils.pformat(data)) + sendAck(self, data.networkMessageId, self.networkUtils.events.connect2.name) + self.customProfiler:stop("Client.onConnect2", cpc) +end - if Utils.IsEmpty(data.networkMessageId) then - error(("onDisconnect2 data.networkMessageId is empty: %s"):format(data.networkMessageId), 3) - end - if Utils.IsEmpty(data.name) then - error(("onDisconnect2 data.name is empty: %s"):format(data.name), 3) - end - if Utils.IsEmpty(data.guid) then - error(("onDisconnect2 data.guid is empty: %s"):format(data.guid), 3) - end +---Callback when disconnected from server. +---@private +---@param self Client +---@param data number data(.code) = 0 +local onDisconnect = function(self, data) + local cpc = self.customProfiler:start("Client.onDisconnect") + self.logger.debug(self.logger.channels.network, "Disconnected from server!", self.utils.pformat(data)) - for i = 1, #self.otherClients do - if data.guid == self.otherClients[i].guid then - table.remove(self.otherClients, i) - break - end - end + if self.utils.IsEmpty(data) then + error(("onDisconnect data is empty: %s"):format(data), 3) + end - sendAck(data.networkMessageId, NetworkUtils.events.disconnect2.name) - CustomProfiler.stop("ClientInit.onDisconnect2", cpc7) + if self.serverInfo.nuid then + self.entityUtils.destroyByNuid(self, self.serverInfo.nuid) end + -- TODO remove all NUIDS from entities. I now need a nuid-entityId-cache. + local nuid, entityId = self.globalsUtils.getNuidEntityPair(self.nuid) + self.networkVscUtils.addOrUpdateAllVscs(entityId, self.name, self.guid, nil) - --- onMinaInformation - --- Callback when Server sent his minaInformation to the client - --- @param data table data @see NetworkUtils.events.minaInformation.schema - local function onMinaInformation(data) - local cpc8 = CustomProfiler.start("ClientInit.onMinaInformation") - Logger.debug(Logger.channels.network, "onMinaInformation: Player info received.", Utils.pformat(data)) + self.nuid = nil + self.otherClients = {} + self.serverInfo = {} - if Utils.IsEmpty(data.networkMessageId) then - error(("onMinaInformation data.networkMessageId is empty: %s"):format(data.networkMessageId), 2) - end + -- sendAck(self, data.networkMessageId) + self.customProfiler:stop("Client.onDisconnect", cpc) +end - if Utils.IsEmpty(data.version) then - error(("onMinaInformation data.version is empty: %s"):format(data.version), 2) - end - if Utils.IsEmpty(data.name) then - error(("onMinaInformation data.name is empty: %s"):format(data.name), 2) - end +---Callback when one of the other clients disconnected. +---@private +---@param self Client +---@param data table data { "name", "guid" } @see self.networkUtils.events.disconnect2.schema +local onDisconnect2 = function(self, data) + local cpc = self.customProfiler:start("Client.onDisconnect2") + self.logger.debug(self.logger.channels.network, "onDisconnect2: Another client disconnected.", self.utils.pformat(data)) - if Utils.IsEmpty(data.guid) then - error(("onMinaInformation data.guid is empty: %s"):format(data.guid), 2) - end + if self.utils.IsEmpty(data.networkMessageId) then + error(("onDisconnect2 data.networkMessageId is empty: %s"):format(data.networkMessageId), 3) + end - if Utils.IsEmpty(data.entityId) or data.entityId == -1 then - error(("onMinaInformation data.entityId is empty: %s"):format(data.entityId), 2) - end + if self.utils.IsEmpty(data.name) then + error(("onDisconnect2 data.name is empty: %s"):format(data.name), 3) + end - -- if Utils.IsEmpty(data.nuid) or data.nuid == -1 then - -- error(("onMinaInformation data.nuid is empty: %s"):format(data.nuid), 2) - -- end + if self.utils.IsEmpty(data.guid) then + error(("onDisconnect2 data.guid is empty: %s"):format(data.guid), 3) + end - if Utils.IsEmpty(data.transform) then - error(("onMinaInformation data.transform is empty: %s"):format(data.transform), 2) + for i = 1, #self.otherClients do + if data.guid == self.otherClients[i].guid then + table.remove(self.otherClients, i) + break end + end - if Utils.IsEmpty(data.health) then - error(("onMinaInformation data.health is empty: %s"):format(data.health), 2) - end + sendAck(self, data.networkMessageId, self.networkUtils.events.disconnect2.name) + self.customProfiler:stop("Client.onDisconnect2", cpc) +end - if data.guid == self.guid then - Logger.warn(Logger.channels.network, - ("onMinaInformation: Clients GUID %s isn't unique! Server will fix this!"):format(self.guid)) - end - if FileUtils.GetVersionByFile() ~= tostring(data.version) then - error(("Version mismatch: NoitaMP version of Server: %s and your version: %s") - :format(data.version, FileUtils.GetVersionByFile()), 3) - self.disconnect() - end +---Callback when Server sent his minaInformation to the client +---@private +---@param self Client +---@param data table data @see self.networkUtils.events.minaInformation.schema +local onMinaInformation = function(self, data) + local cpc = self.customProfiler:start("Client.onMinaInformation") + self.logger.debug(self.logger.channels.network, "onMinaInformation: Player info received.", self.utils.pformat(data)) - self.serverInfo.version = data.version - self.serverInfo.name = data.name - self.serverInfo.guid = data.guid - self.serverInfo.entityId = data.entityId - self.serverInfo.nuid = data.nuid - self.serverInfo.transform = data.transform - self.serverInfo.health = data.health + if self.utils.IsEmpty(data.networkMessageId) then + error(("onMinaInformation data.networkMessageId is empty: %s"):format(data.networkMessageId), 2) + end - sendAck(data.networkMessageId, NetworkUtils.events.minaInformation.name) - CustomProfiler.stop("ClientInit.onMinaInformation", cpc8) + if self.utils.IsEmpty(data.version) then + error(("onMinaInformation data.version is empty: %s"):format(data.version), 2) end + if self.utils.IsEmpty(data.name) then + error(("onMinaInformation data.name is empty: %s"):format(data.name), 2) + end - --- onNewGuid - --- Callback when Server sent a new GUID for a specific client. - --- @param data table data { "networkMessageId", "oldGuid", "newGuid" } - local function onNewGuid(data) - local cpc9 = CustomProfiler.start("ClientInit.onNewGuid") - Logger.debug(Logger.channels.network, ("onNewGuid: New GUID from server received."):format(Utils.pformat(data))) + if self.utils.IsEmpty(data.guid) then + error(("onMinaInformation data.guid is empty: %s"):format(data.guid), 2) + end - if Utils.IsEmpty(data.networkMessageId) then - error(("onNewGuid data.networkMessageId is empty: %s"):format(data.networkMessageId), 2) - end + if self.utils.IsEmpty(data.entityId) or data.entityId == -1 then + error(("onMinaInformation data.entityId is empty: %s"):format(data.entityId), 2) + end - if Utils.IsEmpty(data.oldGuid) then - error(("onNewGuid data.oldGuid is empty: %s"):format(data.oldGuid), 2) - end + -- if self.utils.IsEmpty(data.nuid) or data.nuid == -1 then + -- error(("onMinaInformation data.nuid is empty: %s"):format(data.nuid), 2) + -- end - if Utils.IsEmpty(data.newGuid) then - error(("onNewGuid data.newGuid is empty: %s"):format(data.newGuid), 2) - end + if self.utils.IsEmpty(data.transform) then + error(("onMinaInformation data.transform is empty: %s"):format(data.transform), 2) + end - if data.oldGuid == self.guid then - local entityId = MinaUtils.getLocalMinaInformation().entityId - local compOwnerName, compOwnerGuid, compNuid = NetworkVscUtils.getAllVscValuesByEntityId(entityId) + if self.utils.IsEmpty(data.health) then + error(("onMinaInformation data.health is empty: %s"):format(data.health), 2) + end - self.guid = data.newGuid - NoitaMpSettings.set("noita-mp.guid", self.guid) - NetworkVscUtils.addOrUpdateAllVscs(entityId, compOwnerName, self.guid, compNuid) - else - for i = 1, #self.otherClients do - if self.otherClients[i].guid == data.oldGuid then - self.otherClients[i].guid = data.newGuid - end - end - end - sendAck(data.networkMessageId, NetworkUtils.events.newGuid.name) - CustomProfiler.stop("ClientInit.onNewGuid", cpc9) + if data.guid == self.guid then + self.logger.warn(self.logger.channels.network, + ("onMinaInformation: Clients GUID %s isn't unique! Server will fix this!"):format(self.guid)) end + if FileUtils.GetVersionByFile() ~= tostring(data.version) then + error(("Version mismatch: NoitaMP version of Server: %s and your version: %s") + :format(data.version, FileUtils.GetVersionByFile()), 3) + self.disconnect() + end - --- onSeed - --- Callback when Server sent his seed to the client - --- @param data table data { networkMessageId, seed } - local function onSeed(data) - local cpc10 = CustomProfiler.start("ClientInit.onSeed") - Logger.debug(Logger.channels.network, "onSeed: Seed from server received.", Utils.pformat(data)) - - if Utils.IsEmpty(data.networkMessageId) then - error(("onSeed data.networkMessageId is empty: %s"):format(data.networkMessageId), 3) - end + self.serverInfo.version = data.version + self.serverInfo.name = data.name + self.serverInfo.guid = data.guid + self.serverInfo.entityId = data.entityId + self.serverInfo.nuid = data.nuid + self.serverInfo.transform = data.transform + self.serverInfo.health = data.health - if Utils.IsEmpty(data.seed) then - error(("onSeed data.seed is empty: %s"):format(data.seed), 3) - end + sendAck(self, data.networkMessageId, self.networkUtils.events.minaInformation.name) + self.customProfiler:stop("Client.onMinaInformation", cpc) +end - local serversSeed = tonumber(data.seed) - Logger.info(Logger.channels.network, - ("Client received servers seed (%s) and stored it. Reloading map with that seed!") - :format(serversSeed)) - local localSeed = tonumber(StatsGetValue("world_seed")) - if localSeed ~= serversSeed then - if not DebugGetIsDevBuild() then - Utils.ReloadMap(serversSeed) - end - end - local entityId = MinaUtils.getLocalMinaEntityId() - local name = MinaUtils.getLocalMinaName() - local guid = MinaUtils.getLocalMinaGuid() - if not NetworkVscUtils.hasNetworkLuaComponents(entityId) then - NetworkVscUtils.addOrUpdateAllVscs(entityId, name, guid, nil) - end - if not NetworkVscUtils.hasNuidSet(entityId) then - self.sendNeedNuid(name, guid, entityId) - end +---Callback when Server sent a new GUID for a specific client. +---@private +---@param self Client +---@param data table data { "networkMessageId", "oldGuid", "newGuid" } +local onNewGuid = function(self, data) + local cpc = self.customProfiler:start("Client.onNewGuid") + self.logger.debug(self.logger.channels.network, ("onNewGuid: New GUID from server received."):format(self.utils.pformat(data))) - sendAck(data.networkMessageId, NetworkUtils.events.seed.name) - CustomProfiler.stop("ClientInit.onSeed", cpc10) + if self.utils.IsEmpty(data.networkMessageId) then + error(("onNewGuid data.networkMessageId is empty: %s"):format(data.networkMessageId), 2) end + if self.utils.IsEmpty(data.oldGuid) then + error(("onNewGuid data.oldGuid is empty: %s"):format(data.oldGuid), 2) + end - --- onNewNuid - --- Callback when Server sent a new nuid to the client - --- @param data table data { networkMessageId, owner { name, guid }, localEntityId, newNuid, x, y, rotation, - --- velocity { x, y }, filename } - local function onNewNuid(data) - local cpc11 = CustomProfiler.start("ClientInit.onNewNuid") - Logger.debug(Logger.channels.network, ("Received a new nuid! data = %s"):format(Utils.pformat(data))) + if self.utils.IsEmpty(data.newGuid) then + error(("onNewGuid data.newGuid is empty: %s"):format(data.newGuid), 2) + end - if Utils.IsEmpty(data.networkMessageId) then - error(("onNewNuid data.networkMessageId is empty: %s"):format(data.networkMessageId), 3) - end + if data.oldGuid == self.guid then + local entityId = self.minaUtils.getLocalMinaInformation().entityId + local compOwnerName, compOwnerGuid, compNuid = self.networkVscUtils.getAllVscValuesByEntityId(entityId) - if Utils.IsEmpty(data.owner) then - error(("onNewNuid data.owner is empty: %s"):format(Utils.pformat(data.owner)), 3) + self.guid = data.newGuid + self.noitaMpSettings.set("noita-mp.guid", self.guid) + self.networkVscUtils.addOrUpdateAllVscs(entityId, compOwnerName, self.guid, compNuid) + else + for i = 1, #self.otherClients do + if self.otherClients[i].guid == data.oldGuid then + self.otherClients[i].guid = data.newGuid + end end + end - if Utils.IsEmpty(data.localEntityId) then - error(("onNewNuid data.localEntityId is empty: %s"):format(data.localEntityId), 3) - end + sendAck(self, data.networkMessageId, self.networkUtils.events.newGuid.name) + self.customProfiler:stop("Client.onNewGuid", cpc) +end - if Utils.IsEmpty(data.newNuid) then - error(("onNewNuid data.newNuid is empty: %s"):format(data.newNuid), 3) - end - if Utils.IsEmpty(data.x) then - error(("onNewNuid data.x is empty: %s"):format(data.x), 3) - end +---Callback when Server sent his seed to the client +---@private +---@param self Client +---@param data table data { networkMessageId, seed } +local onSeed = function(self, data) + local cpc = self.customProfiler:start("Client.onSeed") + self.logger.debug(self.logger.channels.network, "onSeed: Seed from server received.", self.utils.pformat(data)) - if Utils.IsEmpty(data.y) then - error(("onNewNuid data.y is empty: %s"):format(data.y), 3) - end + if self.utils.IsEmpty(data.networkMessageId) then + error(("onSeed data.networkMessageId is empty: %s"):format(data.networkMessageId), 3) + end - if Utils.IsEmpty(data.rotation) then - error(("onNewNuid data.rotation is empty: %s"):format(data.rotation), 3) - end + if self.utils.IsEmpty(data.seed) then + error(("onSeed data.seed is empty: %s"):format(data.seed), 3) + end - if Utils.IsEmpty(data.velocity) then - error(("onNewNuid data.velocity is empty: %s"):format(Utils.pformat(data.velocity)), 3) - end + local serversSeed = tonumber(data.seed) + self.logger.info(self.logger.channels.network, + ("Client received servers seed (%s) and stored it. Reloading map with that seed!") + :format(serversSeed)) - if Utils.IsEmpty(data.filename) then - error(("onNewNuid data.filename is empty: %s"):format(data.filename), 3) + local localSeed = tonumber(StatsGetValue("world_seed")) + if localSeed ~= serversSeed then + if not DebugGetIsDevBuild() then + self.utils.ReloadMap(serversSeed) end + end - if Utils.IsEmpty(data.health) then - error(("onNewNuid data.health is empty: %s"):format(data.health), 3) - end + local entityId = self.minaUtils.getLocalMinaEntityId() + local name = self.minaUtils.getLocalMinaName() + local guid = self.minaUtils.getLocalMinaGuid() + if not self.networkVscUtils.hasNetworkLuaComponents(entityId) then + self.networkVscUtils.addOrUpdateAllVscs(entityId, name, guid, nil) + end + if not self.networkVscUtils.hasNuidSet(entityId) then + self.sendNeedNuid(name, guid, entityId) + end - if Utils.IsEmpty(data.isPolymorphed) then - error(("onNewNuid data.isPolymorphed is empty: %s"):format(data.isPolymorphed), 3) - end + sendAck(self, data.networkMessageId, self.networkUtils.events.seed.name) + self.customProfiler:stop("Client.onSeed", cpc) +end - local owner = data.owner - local localEntityId = data.localEntityId - local newNuid = data.newNuid - local x = data.x - local y = data.y - local rotation = data.rotation - local velocity = data.velocity - local filename = data.filename - local health = data.health - local isPolymorphed = data.isPolymorphed - - if owner.guid == MinaUtils.getLocalMinaInformation().guid then - if localEntityId == MinaUtils.getLocalMinaInformation().entityId then - self.nuid = newNuid - end - end - EntityUtils.spawnEntity(owner, newNuid, x, y, rotation, velocity, filename, localEntityId, health, - isPolymorphed) +-- ---Callback when Server sent a new nuid to the client +-- ---@private +-- ---@param self Client +-- ---@param data table data { networkMessageId, owner { name, guid }, localEntityId, newNuid, x, y, rotation, velocity { x, y }, filename } +-- local onNewNuid = function(self, data) +-- local cpc = self.customProfiler:start("Client.onNewNuid") +-- self.logger.debug(self.logger.channels.network, ("Received a new nuid! data = %s"):format(self.utils.pformat(data))) + +-- if self.utils.IsEmpty(data.networkMessageId) then +-- error(("onNewNuid data.networkMessageId is empty: %s"):format(data.networkMessageId), 3) +-- end + +-- if self.utils.IsEmpty(data.owner) then +-- error(("onNewNuid data.owner is empty: %s"):format(self.utils.pformat(data.owner)), 3) +-- end + +-- if self.utils.IsEmpty(data.localEntityId) then +-- error(("onNewNuid data.localEntityId is empty: %s"):format(data.localEntityId), 3) +-- end + +-- if self.utils.IsEmpty(data.newNuid) then +-- error(("onNewNuid data.newNuid is empty: %s"):format(data.newNuid), 3) +-- end + +-- if self.utils.IsEmpty(data.x) then +-- error(("onNewNuid data.x is empty: %s"):format(data.x), 3) +-- end + +-- if self.utils.IsEmpty(data.y) then +-- error(("onNewNuid data.y is empty: %s"):format(data.y), 3) +-- end + +-- if self.utils.IsEmpty(data.rotation) then +-- error(("onNewNuid data.rotation is empty: %s"):format(data.rotation), 3) +-- end + +-- if self.utils.IsEmpty(data.velocity) then +-- error(("onNewNuid data.velocity is empty: %s"):format(self.utils.pformat(data.velocity)), 3) +-- end + +-- if self.utils.IsEmpty(data.filename) then +-- error(("onNewNuid data.filename is empty: %s"):format(data.filename), 3) +-- end + +-- if self.utils.IsEmpty(data.health) then +-- error(("onNewNuid data.health is empty: %s"):format(data.health), 3) +-- end + +-- if self.utils.IsEmpty(data.isPolymorphed) then +-- error(("onNewNuid data.isPolymorphed is empty: %s"):format(data.isPolymorphed), 3) +-- end + +-- local owner = data.owner +-- local localEntityId = data.localEntityId +-- local newNuid = data.newNuid +-- local x = data.x +-- local y = data.y +-- local rotation = data.rotation +-- local velocity = data.velocity +-- local filename = data.filename +-- local health = data.health +-- local isPolymorphed = data.isPolymorphed + +-- if owner.guid == self.minaUtils.getLocalMinaInformation().guid then +-- if localEntityId == self.minaUtils.getLocalMinaInformation().entityId then +-- self.nuid = newNuid +-- end +-- end + +-- self.entityUtils.spawnEntity(owner, newNuid, x, y, rotation, velocity, filename, localEntityId, health, +-- isPolymorphed) + +-- sendAck(self, data.networkMessageId, self.networkUtils.events.newNuid.name) +-- self.customProfiler:stop("Client.onNewNuid", cpc) +-- end + +---Callback when Server sent a new nuid to the client. +---@param self Client +---@param data table data { networkMessageId, ownerName, ownerGuid, entityId, serializedEntityString, nuid, x, y, initialSerializedEntityString } +local onNewNuid = function(self, data) + local cpc = self.customProfiler:start("Client.onNewNuid") + self.logger.debug(self.logger.channels.network, + ("Received a new nuid onNewNuid! data = %s"):format(self.utils.pformat(data))) + + if self.utils.IsEmpty(data.networkMessageId) then + error(("onNewNuid data.networkMessageId is empty: %s"):format(data.networkMessageId), 2) + end - sendAck(data.networkMessageId, NetworkUtils.events.newNuid.name) - CustomProfiler.stop("ClientInit.onNewNuid", cpc11) + if self.utils.IsEmpty(data.ownerName) then + error(("onNewNuid data.ownerName is empty: %s"):format(self.utils.pformat(data.ownerName)), 2) end - -- TODO: this is the new onNewNuid - local function onNewNuid(data) - local cpc32 = CustomProfiler.start("ClientInit.onNewNuid") - Logger.debug(Logger.channels.network, - ("Received a new nuid onNewNuid! data = %s"):format(Utils.pformat(data))) + if self.utils.IsEmpty(data.ownerGuid) then + error(("onNewNuid data.ownerGuid is empty: %s"):format(self.utils.pformat(data.ownerGuid)), 2) + end - if Utils.IsEmpty(data.networkMessageId) then - error(("onNewNuid data.networkMessageId is empty: %s"):format(data.networkMessageId), 2) - end + if self.utils.IsEmpty(data.entityId) then + error(("onNewNuid data.entityId is empty: %s"):format(data.entityId), 2) + end - if Utils.IsEmpty(data.ownerName) then - error(("onNewNuid data.ownerName is empty: %s"):format(Utils.pformat(data.ownerName)), 2) - end + if self.utils.IsEmpty(data.serializedEntityString) then + error(("onNewNuid data.serializedEntityString is empty: %s"):format(data.serializedEntityString), 2) + end - if Utils.IsEmpty(data.ownerGuid) then - error(("onNewNuid data.ownerGuid is empty: %s"):format(Utils.pformat(data.ownerGuid)), 2) - end + if self.utils.IsEmpty(data.nuid) then + error(("onNewNuid data.nuid is empty: %s"):format(data.nuid), 2) + end - if Utils.IsEmpty(data.entityId) then - error(("onNewNuid data.entityId is empty: %s"):format(data.entityId), 2) - end + if self.utils.IsEmpty(data.x) then + error(("onNewNuid data.x is empty: %s"):format(data.x), 2) + end - if Utils.IsEmpty(data.serializedEntityString) then - error(("onNewNuid data.serializedEntityString is empty: %s"):format(data.serializedEntityString), 2) - end + if self.utils.IsEmpty(data.y) then + error(("onNewNuid data.y is empty: %s"):format(data.y), 2) + end - if Utils.IsEmpty(data.nuid) then - error(("onNewNuid data.nuid is empty: %s"):format(data.nuid), 2) - end + if self.utils.IsEmpty(data.initialSerializedEntityString) then + error(("onNewNuid data.initialSerializedEntityString is empty: %s"):format(data.initialSerializedEntityString), 2) + end - if Utils.IsEmpty(data.x) then - error(("onNewNuid data.x is empty: %s"):format(data.x), 2) - end + -- FOR TESTING ONLY, DO NOT MERGE + --print(self.utils.pformat(data)) + --os.exit() - if Utils.IsEmpty(data.y) then - error(("onNewNuid data.y is empty: %s"):format(data.y), 2) - end + --if ownerGuid == self.minaUtils.getLocalMinaInformation().guid then + -- if entityId == self.minaUtils.getLocalMinaInformation().entityId then + -- self.nuid = newNuid + -- end + --end - if Utils.IsEmpty(data.initialSerializedEntityString) then - error(("onNewNuid data.initialSerializedEntityString is empty: %s"):format(data.initialSerializedEntityString), 2) - end + local nuid, entityId = self.globalsUtils.getNuidEntityPair(data.nuid) - -- FOR TESTING ONLY, DO NOT MERGE - --print(Utils.pformat(data)) - --os.exit() - - --if ownerGuid == MinaUtils.getLocalMinaInformation().guid then - -- if entityId == MinaUtils.getLocalMinaInformation().entityId then - -- self.nuid = newNuid - -- end - --end - - local nuid, entityId = GlobalsUtils.getNuidEntityPair(data.nuid) - - if Utils.IsEmpty(entityId) then - local closestEntityId = EntityGetClosest(data.x, data.y) - local initialSerializedEntityString = NoitaComponentUtils.getInitialSerializedEntityString(closestEntityId) - if initialSerializedEntityString == data.initialSerializedEntityString then - entityId = closestEntityId - else - entityId = EntityCreateNew(data.nuid) - end + if self.utils.IsEmpty(entityId) then + local closestEntityId = EntityGetClosest(data.x, data.y) + local initialSerializedEntityString = self.noitaComponentUtils.getInitialSerializedEntityString(closestEntityId) + if initialSerializedEntityString == data.initialSerializedEntityString then + entityId = closestEntityId else - if EntityCache.contains(entityId) then - local cachedEntity = EntityCache.get(entityId) - end + entityId = EntityCreateNew(data.nuid) end + else + if self.entityCache.contains(entityId) then + local cachedEntity = self.entityCache.get(entityId) + end + end - entityId = NoitaPatcherUtils.deserializeEntity(entityId, data.serializedEntityString, data.x, data.y) --EntitySerialisationUtils.deserializeEntireRootEntity(data.serializedEntity, data.nuid) + entityId = self.noitaPatcherUtils.deserializeEntity(entityId, data.serializedEntityString, data.x, data.y) --EntitySerialisationUtils.deserializeEntireRootEntity(data.serializedEntity, data.nuid) - -- include exclude list of entityIds which shouldn't be spawned - -- if filename:contains("player.xml") then - -- filename = "mods/noita-mp/data/enemies_gfx/client_player_base.xml" - -- end + -- include exclude list of entityIds which shouldn't be spawned + -- if filename:contains("player.xml") then + -- filename = "mods/noita-mp/data/enemies_gfx/client_player_base.xml" + -- end - -- local entityId = EntityLoad(filename, x, y) - -- if not EntityGetIsAlive(entityId) then - -- return - -- end + -- local entityId = EntityLoad(filename, x, y) + -- if not EntityGetIsAlive(entityId) then + -- return + -- end - local compIds = EntityGetAllComponents(entityId) or {} - for i = 1, #compIds do - local compId = compIds[i] - local compType = ComponentGetTypeName(compId) - if table.contains(EntityUtils.remove.byComponentsName, compType) or - table.contains(EntitySerialisationUtils.ignore.byComponentsType) then - EntityRemoveComponent(entityId, compId) - end + local compIds = EntityGetAllComponents(entityId) or {} + for i = 1, #compIds do + local compId = compIds[i] + local compType = ComponentGetTypeName(compId) + if table.contains(self.entityUtils.remove.byComponentsName, compType) or + table.contains(EntitySerialisationUtils.ignore.byComponentsType) then + EntityRemoveComponent(entityId, compId) end + end + + sendAck(self, data.networkMessageId, self.networkUtils.events.newNuid.name) + self.customProfiler:stop("Client.onNewNuid", cpc) +end - sendAck(data.networkMessageId, NetworkUtils.events.newNuid.name) - CustomProfiler.stop("ClientInit.onNewNuid", cpc32) +---Callback when entity data received. +---@private +---@param self Client +---@param data table data { networkMessageId, owner { name, guid }, localEntityId, nuid, x, y, rotation, velocity { x, y }, health } +local onEntityData = function(self, data) + local cpc = self.customProfiler:start("Client.onEntityData") + self.logger.debug(self.logger.channels.network, ("Received entityData for nuid = %s! data = %s") + :format(data.nuid, self.utils.pformat(data))) + + if self.utils.IsEmpty(data.networkMessageId) then + error(("onNewNuid data.networkMessageId is empty: %s"):format(data.networkMessageId), 3) end - local function onEntityData(data) - local cpc12 = CustomProfiler.start("ClientInit.onEntityData") - Logger.debug(Logger.channels.network, ("Received entityData for nuid = %s! data = %s") - :format(data.nuid, Utils.pformat(data))) + if self.utils.IsEmpty(data.owner) then + error(("onNewNuid data.owner is empty: %s"):format(self.utils.pformat(data.owner)), 3) + end - if Utils.IsEmpty(data.networkMessageId) then - error(("onNewNuid data.networkMessageId is empty: %s"):format(data.networkMessageId), 3) - end + --if self.utils.IsEmpty(data.localEntityId) then + -- error(("onNewNuid data.localEntityId is empty: %s"):format(data.localEntityId), 3) + --end - if Utils.IsEmpty(data.owner) then - error(("onNewNuid data.owner is empty: %s"):format(Utils.pformat(data.owner)), 3) - end + if self.utils.IsEmpty(data.nuid) then + error(("onNewNuid data.nuid is empty: %s"):format(data.nuid), 3) + end - --if Utils.IsEmpty(data.localEntityId) then - -- error(("onNewNuid data.localEntityId is empty: %s"):format(data.localEntityId), 3) - --end + if self.utils.IsEmpty(data.x) then + error(("onNewNuid data.x is empty: %s"):format(data.x), 3) + end - if Utils.IsEmpty(data.nuid) then - error(("onNewNuid data.nuid is empty: %s"):format(data.nuid), 3) - end + if self.utils.IsEmpty(data.y) then + error(("onNewNuid data.y is empty: %s"):format(data.y), 3) + end - if Utils.IsEmpty(data.x) then - error(("onNewNuid data.x is empty: %s"):format(data.x), 3) - end + if self.utils.IsEmpty(data.rotation) then + error(("onNewNuid data.rotation is empty: %s"):format(data.rotation), 3) + end - if Utils.IsEmpty(data.y) then - error(("onNewNuid data.y is empty: %s"):format(data.y), 3) - end + if self.utils.IsEmpty(data.velocity) then + error(("onNewNuid data.velocity is empty: %s"):format(self.utils.pformat(data.velocity)), 3) + end - if Utils.IsEmpty(data.rotation) then - error(("onNewNuid data.rotation is empty: %s"):format(data.rotation), 3) - end + if self.utils.IsEmpty(data.health) then + error(("onNewNuid data.health is empty: %s"):format(data.health), 3) + end - if Utils.IsEmpty(data.velocity) then - error(("onNewNuid data.velocity is empty: %s"):format(Utils.pformat(data.velocity)), 3) - end + local owner = data.owner + local gNuid, localEntityId = self.globalsUtils.getNuidEntityPair(data.nuid) + local nuid = data.nuid + local x = data.x + local y = data.y + local rotation = data.rotation + local velocity = data.velocity + local health = data.health + + if self.nuid ~= nuid then + self.noitaComponentUtils.setEntityData(localEntityId, x, y, rotation, velocity, health) + else + self.logger.warn(self.logger.channels.network, ("Received entityData for self.nuid = %s! data = %s") + :format(data.nuid, self.utils.pformat(data))) + end - if Utils.IsEmpty(data.health) then - error(("onNewNuid data.health is empty: %s"):format(data.health), 3) - end + -- sendAck(self, data.networkMessageId) do not send ACK for position data, network will explode + self.customProfiler:stop("Client.onEntityData", cpc) +end - local owner = data.owner - local gNuid, localEntityId = GlobalsUtils.getNuidEntityPair(data.nuid) - local nuid = data.nuid - local x = data.x - local y = data.y - local rotation = data.rotation - local velocity = data.velocity - local health = data.health - - if self.nuid ~= nuid then - NoitaComponentUtils.setEntityData(localEntityId, x, y, rotation, velocity, health) +---Callback when dead nuids received. +---@param self Client +---@param data table data { networkMessageId, deadNuids } +local onDeadNuids = function(self, data) + local cpc = self.customProfiler:start("Client.onDeadNuids") + local deadNuids = data.deadNuids or data or {} + for i = 1, #deadNuids do + local deadNuid = deadNuids[i] + if self.utils.IsEmpty(deadNuid) or deadNuid == "nil" then + error(("onDeadNuids deadNuid is empty: %s"):format(deadNuid), 2) else - Logger.warn(Logger.channels.network, ("Received entityData for self.nuid = %s! data = %s") - :format(data.nuid, Utils.pformat(data))) - end - - -- sendAck(data.networkMessageId) do not send ACK for position data, network will explode - CustomProfiler.stop("ClientInit.onEntityData", cpc12) - end - - local function onDeadNuids(data) - local cpc13 = CustomProfiler.start("ClientInit.onDeadNuids") - local deadNuids = data.deadNuids or data or {} - for i = 1, #deadNuids do - local deadNuid = deadNuids[i] - if Utils.IsEmpty(deadNuid) or deadNuid == "nil" then - error(("onDeadNuids deadNuid is empty: %s"):format(deadNuid), 2) - else - EntityUtils.destroyByNuid(self, deadNuid) - GlobalsUtils.removeDeadNuid(deadNuid) - EntityCache.deleteNuid(deadNuid) - end + self.entityUtils.destroyByNuid(self, deadNuid) + self.globalsUtils.removeDeadNuid(deadNuid) + self.entityCache.deleteNuid(deadNuid) end - CustomProfiler.stop("Client.onDeadNuids", cpc13) end + self.customProfiler:stop("Client.onDeadNuids", cpc) +end - local function onNeedModList(data) - local cpc = CustomProfiler.start("Client.onNeedModList") - local activeMods = ModGetActiveModIDs() - local function contains(elem) - for _, value in pairs(activeMods) do - if value == elem then - return true - end +---Callback when mod list is requested. +---@param self Client +---@param data table data { networkMessageId, workshop, external } +local onNeedModList = function(self, data) + local cpc = self.customProfiler:start("Client.onNeedModList") + local activeMods = ModGetActiveModIDs() + local function contains(elem) + for _, value in pairs(activeMods) do + if value == elem then + return true end - return false end + return false + end - self.requiredMods = data.workshop - local conflicts = {} - for _, v in ipairs(data.workshop) do - if not contains(v.name) then - table.insert(conflicts, v) - end + self.requiredMods = data.workshop + local conflicts = {} + for _, v in ipairs(data.workshop) do + if not contains(v.name) then + table.insert(conflicts, v) end - for _, v in ipairs(data.external) do - table.insert(self.requiredMods, v) - if not contains(v.name) then - table.insert(conflicts, v) - end + end + for _, v in ipairs(data.external) do + table.insert(self.requiredMods, v) + if not contains(v.name) then + table.insert(conflicts, v) end + end - -- Display a prompt if mod conflicts are detected - if #conflicts > 0 then - self.missingMods = conflicts - Logger.info("Mod conflicts detected: Missing " .. table.concat(conflicts, ", ")) - end - CustomProfiler.stop("ClientInit.onNeedModList", cpc) + -- Display a prompt if mod conflicts are detected + if #conflicts > 0 then + self.missingMods = conflicts + self.logger.info("Mod conflicts detected: Missing " .. table.concat(conflicts, ", ")) end + self.customProfiler:stop("Client.onNeedModList", cpc) +end - local function onNeedModContent(data) - local cpc = CustomProfiler.start("ClientInit.onNeedModContent") - for _, v in ipairs(data.items) do - local modName = v.name - local modID = v.workshopID - local modData = v.data - if modID == "0" then +---Callback when mod content is requested. +---@param self Client +---@param data table data { networkMessageId, items } +local onNeedModContent = function(self, data) + local cpc = self.customProfiler:start("Client.onNeedModContent") + for _, v in ipairs(data.items) do + local modName = v.name + local modID = v.workshopID + local modData = v.data + if modID == "0" then + if not FileUtils.IsDirectory((FileUtils.GetAbsolutePathOfNoitaRootDirectory() .. "/mods/%s/"):format(modName)) then + local fileName = ("%s_%s_mod_sync.7z"):format(tostring(os.date("!")), modName) + FileUtils.WriteBinaryFile(FileUtils.GetAbsoluteDirectoryPathOfNoitaMP() .. "/" .. fileName, modData) + FileUtils.Extract7zipArchive(FileUtils.GetAbsoluteDirectoryPathOfNoitaMP(), fileName, + (FileUtils.GetAbsolutePathOfNoitaRootDirectory() .. "/mods/%s/"):format(modName)) + end + else + if not FileUtils.IsDirectory(("C:/Program Files (x86)/Steam/steamapps/workshop/content/881100/%s/"):format(modID)) then if not FileUtils.IsDirectory((FileUtils.GetAbsolutePathOfNoitaRootDirectory() .. "/mods/%s/"):format(modName)) then local fileName = ("%s_%s_mod_sync.7z"):format(tostring(os.date("!")), modName) FileUtils.WriteBinaryFile(FileUtils.GetAbsoluteDirectoryPathOfNoitaMP() .. "/" .. fileName, modData) FileUtils.Extract7zipArchive(FileUtils.GetAbsoluteDirectoryPathOfNoitaMP(), fileName, (FileUtils.GetAbsolutePathOfNoitaRootDirectory() .. "/mods/%s/"):format(modName)) end - else - if not FileUtils.IsDirectory(("C:/Program Files (x86)/Steam/steamapps/workshop/content/881100/%s/"):format(modID)) then - if not FileUtils.IsDirectory((FileUtils.GetAbsolutePathOfNoitaRootDirectory() .. "/mods/%s/"):format(modName)) then - local fileName = ("%s_%s_mod_sync.7z"):format(tostring(os.date("!")), modName) - FileUtils.WriteBinaryFile(FileUtils.GetAbsoluteDirectoryPathOfNoitaMP() .. "/" .. fileName, modData) - FileUtils.Extract7zipArchive(FileUtils.GetAbsoluteDirectoryPathOfNoitaMP(), fileName, - (FileUtils.GetAbsolutePathOfNoitaRootDirectory() .. "/mods/%s/"):format(modName)) - end - end end end - CustomProfiler.stop("ClientInit.onNeedModContent", cpc) - end - - -- self:on( - -- "entityAlive", - -- function(data) - -- logger:debug(Utils.pformat(data)) - - -- em:DespawnEntity(data.owner, data.localEntityId, data.nuid, data.isAlive) - -- end - -- ) - - -- self:on( - -- "entityState", - -- function(data) - -- logger:debug(Utils.pformat(data)) - - -- local nc = em:GetNetworkComponent(data.owner, data.localEntityId, data.nuid) - -- if nc then - -- EntityApplyTransform(nc.local_entity_id, data.x, data.y, data.rot) - -- else - -- logger:warn(logger.channels.network, - -- "Got entityState, but unable to find the network component!" .. - -- " owner(%s, %s), localEntityId(%s), nuid(%s), x(%s), y(%s), rot(%s), velocity(x %s, y %s), health(%s)", - -- data.owner.name, - -- data.owner.guid, - -- data.localEntityId, - -- data.nuid, - -- data.x, - -- data.y, - -- data.rot, - -- data.velocity.x, - -- data.velocity.y, - -- data.health - -- ) - -- end - -- end - -- ) - - - --- setCallbackAndSchemas - --- Sets callbacks and schemas of the client. - local function setCallbackAndSchemas() - local cpc14 = CustomProfiler.start("ClientInit.setCallbackAndSchemas") - --self:setSchema(NetworkUtils.events.connect, { "code" }) - self:on(NetworkUtils.events.connect.name, onConnect) - - self:setSchema(NetworkUtils.events.connect2.name, NetworkUtils.events.connect2.schema) - self:on(NetworkUtils.events.connect2.name, onConnect2) - - --self:setSchema(NetworkUtils.events.disconnect, { "code" }) - self:on(NetworkUtils.events.disconnect.name, onDisconnect) - - self:setSchema(NetworkUtils.events.disconnect2.name, NetworkUtils.events.disconnect2.schema) - self:on(NetworkUtils.events.disconnect2.name, onDisconnect2) - - self:setSchema(NetworkUtils.events.acknowledgement.name, NetworkUtils.events.acknowledgement.schema) - self:on(NetworkUtils.events.acknowledgement.name, onAcknowledgement) - - self:setSchema(NetworkUtils.events.seed.name, NetworkUtils.events.seed.schema) - self:on(NetworkUtils.events.seed.name, onSeed) - - self:setSchema(NetworkUtils.events.minaInformation.name, NetworkUtils.events.minaInformation.schema) - self:on(NetworkUtils.events.minaInformation.name, onMinaInformation) - - self:setSchema(NetworkUtils.events.newGuid.name, NetworkUtils.events.newGuid.schema) - self:on(NetworkUtils.events.newGuid.name, onNewGuid) - - self:setSchema(NetworkUtils.events.newNuid.name, NetworkUtils.events.newNuid.schema) - self:on(NetworkUtils.events.newNuid.name, onNewNuid) - - -- self:setSchema(NetworkUtils.events.entityData.name, NetworkUtils.events.entityData.schema) - -- self:on(NetworkUtils.events.entityData.name, onEntityData) - - self:setSchema(NetworkUtils.events.deadNuids.name, NetworkUtils.events.deadNuids.schema) - self:on(NetworkUtils.events.deadNuids.name, onDeadNuids) - - self:setSchema(NetworkUtils.events.needModList.name, NetworkUtils.events.needModList.schema) - self:on(NetworkUtils.events.needModList.name, onNeedModList) - - self:setSchema(NetworkUtils.events.needModContent.name, NetworkUtils.events.needModContent.schema) - self:on(NetworkUtils.events.needModContent.name, onNeedModContent) - - self:setSchema(NetworkUtils.events.newNuid.name, NetworkUtils.events.newNuid.schema) - self:on(NetworkUtils.events.newNuid.name, onNewNuid) - - CustomProfiler.stop("ClientInit.setCallbackAndSchemas", cpc14) - end - - --- Some inheritance: Save parent function (not polluting global 'self' space) - local sockClientConnect = sockClient.connect - --- Connects to a server on ip and port. Both can be nil, then ModSettings will be used. - --- @param ip string localhost or 127.0.0.1 or nil - --- @param port number? port number from 1 to max of 65535 or nil - --- @param code number connection code 0 = connecting first time, 1 = connected second time with loaded seed - function self.connect(ip, port, code) - local cpc16 = CustomProfiler.start("ClientInit.connect") - - if self:isConnecting() or self:isConnected() then - Logger.warn(Logger.channels.network, ("Client is still connected to %s:%s. Disconnecting!") - :format(self:getAddress(), self:getPort())) - self:disconnect() - end + end + self.customProfiler:stop("Client.onNeedModContent", cpc) +end - if not ip then - local cpc29 = CustomProfiler.start("ModSettingGet") - ip = tostring(ModSettingGet("noita-mp.connect_server_ip")) - CustomProfiler.stop("ModSettingGet", cpc29) - end - if not port then - local cpc30 = CustomProfiler.start("ModSettingGet") - port = tonumber(ModSettingGet("noita-mp.connect_server_port")) or error("noita-mp.connect_server_port wasn't a number") - CustomProfiler.stop("ModSettingGet", cpc30) - end +---Sets callbacks and schemas of the client. +---@param self Client +local setCallbackAndSchemas = function(self) + local cpc = self.customProfiler:start("Client.setCallbackAndSchemas") + --self:setSchema(self.networkUtils.events.connect, { "code" }) + self:on(self.networkUtils.events.connect.name, onConnect) - port = tonumber(port) or error("noita-mp.connect_server_port wasn't a number") + self:setSchema(self.networkUtils.events.connect2.name, self.networkUtils.events.connect2.schema) + self:on(self.networkUtils.events.connect2.name, onConnect2) - Logger.info(Logger.channels.network, ("Trying to connect to server on %s:%s"):format(ip, port)) - if not self.host then - self:establishClient(ip, port) - end + --self:setSchema(self.networkUtils.events.disconnect, { "code" }) + self:on(self.networkUtils.events.disconnect.name, onDisconnect) + + self:setSchema(self.networkUtils.events.disconnect2.name, self.networkUtils.events.disconnect2.schema) + self:on(self.networkUtils.events.disconnect2.name, onDisconnect2) + + self:setSchema(self.networkUtils.events.acknowledgement.name, self.networkUtils.events.acknowledgement.schema) + self:on(self.networkUtils.events.acknowledgement.name, onAcknowledgement) + + self:setSchema(self.networkUtils.events.seed.name, self.networkUtils.events.seed.schema) + self:on(self.networkUtils.events.seed.name, onSeed) + + self:setSchema(self.networkUtils.events.minaInformation.name, self.networkUtils.events.minaInformation.schema) + self:on(self.networkUtils.events.minaInformation.name, onMinaInformation) + + self:setSchema(self.networkUtils.events.newGuid.name, self.networkUtils.events.newGuid.schema) + self:on(self.networkUtils.events.newGuid.name, onNewGuid) - GamePrintImportant("ClientInit is trying to connect to server..", - "You are trying to connect to " .. self:getAddress() .. ":" .. self:getPort() .. "!", - "" - ) + self:setSchema(self.networkUtils.events.newNuid.name, self.networkUtils.events.newNuid.schema) + self:on(self.networkUtils.events.newNuid.name, onNewNuid) - sockClientConnect(self, code) + -- self:setSchema(self.networkUtils.events.entityData.name, self.networkUtils.events.entityData.schema) + -- self:on(self.networkUtils.events.entityData.name, onEntityData) - -- FYI: If you want to send data after connected, do it in the "connect" callback function - CustomProfiler.stop("ClientInit.connect", cpc16) + self:setSchema(self.networkUtils.events.deadNuids.name, self.networkUtils.events.deadNuids.schema) + self:on(self.networkUtils.events.deadNuids.name, onDeadNuids) + + self:setSchema(self.networkUtils.events.needModList.name, self.networkUtils.events.needModList.schema) + self:on(self.networkUtils.events.needModList.name, onNeedModList) + + self:setSchema(self.networkUtils.events.needModContent.name, self.networkUtils.events.needModContent.schema) + self:on(self.networkUtils.events.needModContent.name, onNeedModContent) + + self:setSchema(self.networkUtils.events.newNuid.name, self.networkUtils.events.newNuid.schema) + self:on(self.networkUtils.events.newNuid.name, onNewNuid) + + self.customProfiler:stop("Client.setCallbackAndSchemas", cpc) +end + +---Connects to a server on ip and port. Both can be nil, then ModSettings will be used. Inherit from sock.connect. +---@param ip string|nil localhost or 127.0.0.1 or nil +---@param port number|nil port number from 1 to max of 65535 or nil +---@param code number|nil connection code 0 = connecting first time, 1 = connected second time with loaded seed +---@see sock.connect +function Client:connect(ip, port, code) + local cpc = self.customProfiler:start("Client.connect") + + if self:isConnecting() or self:isConnected() then + self.logger:warn(self.logger.channels.network, ("Client is still connected to %s:%s. Disconnecting!") + :format(self:getAddress(), self:getPort())) + self:disconnect() end - --- Some inheritance: Save parent function (not polluting global 'self' space) - local sockClientDisconnect = sockClient.disconnect - function self.disconnect() - local cpc17 = CustomProfiler.start("ClientInit.disconnect") - if self.isConnected() then - sockClientDisconnect(self) - else - Logger.info(Logger.channels.network, "Client isn't connected, no need to disconnect!") - end - CustomProfiler.stop("ClientInit.disconnect", cpc17) + if not ip then + local cpc29 = self.customProfiler:start("ModSettingGet") + ip = tostring(ModSettingGet("noita-mp.connect_server_ip")) + self.customProfiler:stop("ModSettingGet", cpc29) end - --#endregion + if not port then + local cpc30 = self.customProfiler:start("ModSettingGet") + port = tonumber(ModSettingGet("noita-mp.connect_server_port")) or error("noita-mp.connect_server_port wasn't a number") + self.customProfiler:stop("ModSettingGet", cpc30) + end - --#region Additional methods + port = tonumber(port) or error("noita-mp.connect_server_port wasn't a number") - local sockClientIsConnected = sockClient.isConnected - function self.isConnected() - return sockClientIsConnected(self) + self.logger.info(self.logger.channels.network, ("Trying to connect to server on %s:%s"):format(ip, port)) + if not self.host then + self:establishClient(ip, port) end - --local lastFrames = 0 - --local diffFrames = 0 - --local fps30 = 0 - local prevTime = 0 - --- Some inheritance: Save parent function (not polluting global 'self' space) - local sockClientUpdate = sockClient.update - --- Updates the Client by checking for network events and handling them. - function self.update(startFrameTime) - local cpc18 = CustomProfiler.start("ClientInit.update") - if not self.isConnected() and not self:isConnecting() or self:isDisconnected() then - CustomProfiler.stop("ClientInit.update", cpc18) - return - end - - self.sendMinaInformation() + GamePrintImportant("Client is trying to connect to server..", + "You are trying to connect to " .. self:getAddress() .. ":" .. self:getPort() .. "!", + "" + ) - --EntityUtils.destroyClientEntities() - --EntityUtils.processEntityNetworking() - --EntityUtils.initNetworkVscs() + -- Inheritance: https://ozzypig.com/2018/05/10/object-oriented-programming-in-lua-part-5-inheritance + self.sock.connect(self, code) --sockClientConnect(self, code) - EntityUtils.syncEntities(startFrameTime) + -- FYI: If you want to send data after connected, do it in the "connect" callback function + self.customProfiler:stop("Client.connect", cpc16) +end - local nowTime = GameGetRealWorldTimeSinceStarted() * 1000 -- *1000 to get milliseconds - local elapsedTime = nowTime - prevTime - local cpc31 = CustomProfiler.start("ModSettingGet") - local oneTickInMs = 1000 / tonumber(ModSettingGet("noita-mp.tick_rate")) - CustomProfiler.stop("ModSettingGet", cpc31) - if elapsedTime >= oneTickInMs then - prevTime = nowTime - --updateVariables() +---Disconnects from the server. Inherit from sock.disconnect. +---@see sock.disconnect +function Client:disconnect() + local cpc = self.customProfiler:start("Client.disconnect") + if self.isConnected() then + -- Inheritance: https://ozzypig.com/2018/05/10/object-oriented-programming-in-lua-part-5-inheritance + self.sock.disconnect(self) --sockClientDisconnect(self) + else + self.logger.info(self.logger.channels.network, "Client isn't connected, no need to disconnect!") + end + self.customProfiler:stop("Client.disconnect", cpc) +end - --EntityUtils.destroyClientEntities() - --EntityUtils.syncEntityData() - EntityUtils.syncDeadNuids() - end +---Returns true if the client is connected to the server. Inherit from sock.isConnected. +---@return boolean +function Client:isConnected() + -- Inheritance: https://ozzypig.com/2018/05/10/object-oriented-programming-in-lua-part-5-inheritance + return self.sock.isConnected(self) +end - sockClientUpdate(self) - CustomProfiler.stop("ClientInit.update", cpc18) +local prevTime = 0 +---Updates the Client by checking for network events and handling them. Inherit from sock.update. +---@param startFrameTime number required +---@see sock.update +function Client:update(startFrameTime) + local cpc = self.customProfiler:start("Client.update") + if not self.isConnected() and not self:isConnecting() or self:isDisconnected() then + self.customProfiler:stop("Client.update", cpc) + return end - --- Some inheritance: Save parent function (not polluting global 'self' space) - local sockClientSend = sockClient.send - function self:send(event, data) - local cpc19 = CustomProfiler.start("ClientInit.send") - if type(data) ~= "table" then - error(("Data is not type of table: %s"):format(data), 2) - return false - end + self.sendMinaInformation() - if NetworkUtils.alreadySent(self, event, data) then - Logger.debug(Logger.channels.network, ("Network message for %s for data %s already was acknowledged.") - :format(event, Utils.pformat(data))) - CustomProfiler.stop("ClientInit.send", cpc19) - return false - end + --self.entityUtils.destroyClientEntities() + --self.entityUtils.processEntityNetworking() + --self.entityUtils.initNetworkVscs() - local networkMessageId = sockClientSend(self, event, data) + self.entityUtils.syncEntities(startFrameTime) - if event ~= NetworkUtils.events.acknowledgement.name then - if NetworkUtils.events[event].isCacheable == true then - NetworkCacheUtils.set(self.guid, networkMessageId, event, - NetworkUtils.events.acknowledgement.sent, 0, os.clock(), data) - end - end - CustomProfiler.stop("ClientInit.send", cpc19) - return true + local nowTime = GameGetRealWorldTimeSinceStarted() * 1000 -- *1000 to get milliseconds + local elapsedTime = nowTime - prevTime + local cpc31 = self.customProfiler:start("ModSettingGet") + local oneTickInMs = 1000 / tonumber(ModSettingGet("noita-mp.tick_rate")) + self.customProfiler:stop("ModSettingGet", cpc31) + if elapsedTime >= oneTickInMs then + prevTime = nowTime + --updateVariables() + + --self.entityUtils.destroyClientEntities() + --self.entityUtils.syncEntityData() + self.entityUtils.syncDeadNuids() end - ---Sends a message to the server that the client needs a nuid. - ---@param ownerName string - ---@param ownerGuid string - ---@param entityId number - function self.sendNeedNuid(ownerName, ownerGuid, entityId) - local cpc20 = CustomProfiler.start("ClientInit.sendNeedNuid") + -- Inheritance: https://ozzypig.com/2018/05/10/object-oriented-programming-in-lua-part-5-inheritance + self.sock.update(self) --sockClientUpdate(self) + self.customProfiler:stop("Client.update", cpc) +end - if not ownerName then - error("ownerName is nil") - end - if not ownerGuid then - error("ownerGuid is nil") - end - if not entityId then - error("entityId is nil") - end +---Sends a message to the server. Inherit from sock.send. +---@param event string required +---@param data table required +---@return boolean true if message was sent, false if not +---@see sock.send +function Client:send(event, data) + local cpc = self.customProfiler:start("Client.send") + if type(data) ~= "table" then + error(("Data is not type of table: %s"):format(data), 2) + return false + end - if not EntityGetIsAlive(entityId) then - return - end + if self.networkUtils.alreadySent(self, event, data) then + self.logger.debug(self.logger.channels.network, ("Network message for %s for data %s already was acknowledged.") + :format(event, self.utils.pformat(data))) + self.customProfiler:stop("Client.send", cpc) + return false + end - local x, y = EntityGetTransform(entityId) - local initialBase64String, md5Hash = NoitaPatcherUtils.serializeEntity(entityId) - local data = { - NetworkUtils.getNextNetworkMessageId(), ownerName, ownerGuid, entityId, x, y, - NoitaComponentUtils.getInitialSerializedEntityString(entityId), initialBase64String - } + -- Inheritance: https://ozzypig.com/2018/05/10/object-oriented-programming-in-lua-part-5-inheritance + local networkMessageId = self.sock.send(self, event, data) --sockClientSend(self, event, data) - if isTestLuaContext then - print(("Sending need nuid for entity %s with data %s"):format(entityId, Utils.pformat(data))) + if event ~= self.networkUtils.events.acknowledgement.name then + if self.networkUtils.events[event].isCacheable == true then + self.networkCacheUtils.set(self.guid, networkMessageId, event, + self.networkUtils.events.acknowledgement.sent, 0, os.clock(), data) end - - self:send(NetworkUtils.events.needNuid.name, data) - CustomProfiler.stop("ClientInit.sendNeedNuid", cpc20) end + self.customProfiler:stop("Client.send", cpc) + return true +end - function self.sendLostNuid(nuid) - local cpc21 = CustomProfiler.start("ClientInit.sendLostNuid") - local data = { NetworkUtils.getNextNetworkMessageId(), nuid } - local sent = self:send(NetworkUtils.events.lostNuid.name, data) - CustomProfiler.stop("ClientInit.sendLostNuid", cpc21) - return sent +---Sends a message to the server that the client needs a nuid. +---@param ownerName string +---@param ownerGuid string +---@param entityId number +function Client:sendNeedNuid(ownerName, ownerGuid, entityId) + local cpc = self.customProfiler:start("Client.sendNeedNuid") + + if not ownerName then + error("ownerName is nil") + end + if not ownerGuid then + error("ownerGuid is nil") + end + if not entityId then + error("entityId is nil") end - function self.sendEntityData(entityId) - local cpc22 = CustomProfiler.start("ClientInit.sendEntityData") - if not EntityGetIsAlive(entityId) then - return - end + if not EntityGetIsAlive(entityId) then + return + end - --local compOwnerName, compOwnerGuid, compNuid = NetworkVscUtils.getAllVscValuesByEntityId(entityId) - local compOwnerName, compOwnerGuid, compNuid, filename, health, rotation, velocity, x, y = NoitaComponentUtils.getEntityData(entityId) - local data = { - NetworkUtils.getNextNetworkMessageId(), { compOwnerName, compOwnerGuid }, compNuid, x, y, rotation, velocity, health - } - - if Utils.IsEmpty(compNuid) then - -- this can happen, when entity spawned on client and network is slow - Logger.debug(Logger.channels.network, "Unable to send entity data, because nuid is empty.") - self.sendNeedNuid(compOwnerName, compOwnerGuid, entityId) - return - end + local x, y = EntityGetTransform(entityId) + local initialBase64String, md5Hash = self.noitaPatcherUtils.serializeEntity(entityId) + local data = { + self.networkUtils.getNextNetworkMessageId(), ownerName, ownerGuid, entityId, x, y, + self.noitaComponentUtils.getInitialSerializedEntityString(entityId), initialBase64String + } - if MinaUtils.getLocalMinaInformation().guid == compOwnerGuid then - self:send(NetworkUtils.events.entityData.name, data) - end - CustomProfiler.stop("ClientInit.sendEntityData", cpc22) - end - - function self.sendDeadNuids(deadNuids) - local cpc23 = CustomProfiler.start("ClientInit.sendDeadNuids") - local data = { - NetworkUtils.getNextNetworkMessageId(), deadNuids - } - local sent = self:send(NetworkUtils.events.deadNuids.name, data) - onDeadNuids(deadNuids) - CustomProfiler.stop("ClientInit.sendDeadNuids", cpc23) - return sent - end - - function self.sendMinaInformation() - local cpc24 = CustomProfiler.start("ClientInit.sendMinaInformation") - local minaInfo = MinaUtils.getLocalMinaInformation() - local name = minaInfo.name - local guid = minaInfo.guid - local entityId = minaInfo.entityId or -1 - local nuid = minaInfo.nuid or -1 - local transform = minaInfo.transform - local health = minaInfo.health - local data = { - NetworkUtils.getNextNetworkMessageId(), FileUtils.GetVersionByFile(), name, guid, entityId, nuid, transform, health - } - local sent = self:send(NetworkUtils.events.minaInformation.name, data) - CustomProfiler.stop("ClientInit.sendMinaInformation", cpc24) - return sent - end - - --- Checks if the current local user is a client - --- @return boolean iAm true if client - function self.amIClient() - --local cpc24 = CustomProfiler.start("ClientInit.amIClient") DO NOT PROFILE, stack overflow error! See CustomProfiler.lua - if not _G.Server.amIServer() then - --CustomProfiler.stop("ClientInit.amIClient", cpc24) - return true - end - --CustomProfiler.stop("ClientInit.amIClient", cpc24) - return false + if isTestLuaContext then + print(("Sending need nuid for entity %s with data %s"):format(entityId, self.utils.pformat(data))) end - --- Mainly for profiling. Returns then network cache, aka acknowledge. - --- @return number cacheSize - function self.getAckCacheSize() - return NetworkCache.size() + self:send(self.networkUtils.events.needNuid.name, data) + self.customProfiler:stop("Client.sendNeedNuid", cpc) +end + +---Sends a message that the client has a nuid, but no linked entity. +---@param nuid number required +---@return boolean true if message was sent, false if not +function Client:sendLostNuid(nuid) + local cpc = self.customProfiler:start("Client.sendLostNuid") + local data = { self.networkUtils.getNextNetworkMessageId(), nuid } + local sent = self:send(self.networkUtils.events.lostNuid.name, data) + self.customProfiler:stop("Client.sendLostNuid", cpc) + return sent +end + +---Sends entity data to the server. +---@param entityId number required +function Client:sendEntityData(entityId) + local cpc = self.customProfiler:start("Client.sendEntityData") + if not EntityGetIsAlive(entityId) then + return end - --#endregion + --local compOwnerName, compOwnerGuid, compNuid = self.networkVscUtils.getAllVscValuesByEntityId(entityId) + local compOwnerName, compOwnerGuid, compNuid, filename, health, rotation, velocity, x, y = self.noitaComponentUtils.getEntityData(entityId) + local data = { + self.networkUtils.getNextNetworkMessageId(), { compOwnerName, compOwnerGuid }, compNuid, x, y, rotation, velocity, health + } + + if self.utils.IsEmpty(compNuid) then + -- this can happen, when entity spawned on client and network is slow + self.logger.debug(self.logger.channels.network, "Unable to send entity data, because nuid is empty.") + self.sendNeedNuid(compOwnerName, compOwnerGuid, entityId) + return + end + if self.minaUtils.getLocalMinaInformation().guid == compOwnerGuid then + self:send(self.networkUtils.events.entityData.name, data) + end + self.customProfiler:stop("Client.sendEntityData", cpc) +end - -- Apply some private methods +---Sends dead nuids to the server. +---@param deadNuids table required +---@return boolean true if message was sent, false if not +function Client:sendDeadNuids(deadNuids) + local cpc = self.customProfiler:start("Client.sendDeadNuids") + local data = { + self.networkUtils.getNextNetworkMessageId(), deadNuids + } + local sent = self:send(self.networkUtils.events.deadNuids.name, data) + onDeadNuids(deadNuids) + self.customProfiler:stop("Client.sendDeadNuids", cpc) + return sent +end - setGuid() - setConfigSettings() - setCallbackAndSchemas() +---Sends mina information to the server. +---@return boolean +function Client:sendMinaInformation() + local cpc = self.customProfiler:start("Client.sendMinaInformation") + local minaInfo = self.minaUtils.getLocalMinaInformation() + local name = minaInfo.name + local guid = minaInfo.guid + local entityId = minaInfo.entityId or -1 + local nuid = minaInfo.nuid or -1 + local transform = minaInfo.transform + local health = minaInfo.health + local data = { + self.networkUtils.getNextNetworkMessageId(), FileUtils.GetVersionByFile(), name, guid, entityId, nuid, transform, health + } + local sent = self:send(self.networkUtils.events.minaInformation.name, data) + self.customProfiler:stop("Client.sendMinaInformation", cpc) + return sent +end - CustomProfiler.stop("ClientInit.new", cpc) - return self +---Checks if the current local user is a client. +---@return boolean true if client, false if not +---@see Server.amIServer +function Client:amIClient() + --local cpc24 = self.customProfiler:start("Client.amIClient") DO NOT PROFILE, stack overflow error! See self.customProfiler.lua + if not self.server.amIServer() then + --self.customProfiler:stop("Client.amIClient", cpc24) + return true + end + --self.customProfiler:stop("Client.amIClient", cpc24) + return false end +---Mainly for profiling. Returns then network cache, aka acknowledge. +---@return number cacheSize +function Client:getAckCacheSize() + return self.networkCache.size() +end ----Class constructor ----@param tOrSockClient Client|SockClient +---Client constructor. Inherited from sock.Client. +---@param clientObject Client|nil +---@param serverOrAddress string|nil +---@param port number|nil +---@param maxChannels number|nil +---@param server Server required ---@return Client -function Client:new(tOrSockClient) - local cpc = CustomProfiler.start("Client:new") - local t = tOrSockClient or {} - setmetatable(t, self) - self.__index = self - - self:setSerialization(serialize, deserialize) - self:setTimeout(320, 50000, 100000) - CustomProfiler.stop("Client:new", cpc) - return t +function Client:new(clientObject, serverOrAddress, port, maxChannels, server) + ---@class Client : SockClient + clientObject = + setmetatable(clientObject or sock.newClient(serverOrAddress, port, maxChannels), Client) or --Inherits from sock.Client + error("Unable to create new sock client!", 2) + + --[[ Imports ]] + --Initialize all imports to avoid recursive imports + + if not clientObject.noitaMpSettings then + clientObject.noitaMpSettings = require("NoitaMpSettings") + :new(nil, nil, nil, nil, nil, nil, nil, nil, nil) + end + if not clientObject.customProfiler then + clientObject.customProfiler = --[[self.noitaMpSettings.customProfiler or]] require("CustomProfiler") + :new(nil, nil, clientObject.noitaMpSettings, nil, nil, nil, nil) + end + local cpc = clientObject.customProfiler:start("Client:new") + + if not clientObject.entityUtils then + clientObject.entityUtils = require("EntityUtils") + end + if not clientObject.guidUtils then + clientObject.guidUtils = require("GuidUtils") + end + -- clientObject.logger is sock.logger by default. Has to be repalced with NoitaMP logger. + if not clientObject.logger or clientObject.logger ~= clientObject.noitaMpSettings.logger then + clientObject.logger = clientObject.noitaMpSettings.logger or require("Logger"):new(nil, clientObject.customProfiler) + end + + if not clientObject.messagePack then + clientObject.messagePack = require("MessagePack") + end + if not clientObject.minaUtils then + clientObject.minaUtils = require("MinaUtils") + end + if not clientObject.networkUtils then + clientObject.networkUtils = require("NetworkUtils") + end + if not clientObject.noitaPatcherUtils then + clientObject.noitaPatcherUtils = require("NoitaPatcherUtils") + end + if not clientObject.server then + clientObject.server = server --or error("Server is nil!", 2) + end + if not clientObject.sock then + clientObject.sock = require("sock") + end + if not clientObject.zstandard then + clientObject.zstandard = require("zstd") + end + if not clientObject.utils then + clientObject.utils = clientObject.noitaMpSettings.utils or require("Utils")--:new() + end + + --[[ Attributes ]] + + clientObject.acknowledgeMaxSize = 500 + clientObject.guid = nil + clientObject.health = { current = 99, max = 100 } + clientObject.iAm = "CLIENT" + clientObject.missingMods = nil + clientObject.name = nil + clientObject.nuid = nil + clientObject.otherClients = {} + clientObject.requiredMods = nil + clientObject.serverInfo = {} + clientObject.syncedMods = false + clientObject.transform = { x = 0, y = 0 } + + -- Functions for initialization + + clientObject:setSerialization(clientObject.serialize, clientObject.deserialize) + clientObject:setTimeout(320, 50000, 100000) + setCallbackAndSchemas(clientObject) + + clientObject.name = clientObject.noitaMpSettings:get("noita-mp.nickname", "string") + clientObject.guid = clientObject.noitaMpSettings:get("noita-mp.guid", "string") + setGuid(clientObject) + + clientObject.customProfiler:stop("Client:new", cpc) + return clientObject end return Client diff --git a/mods/noita-mp/files/scripts/util/CustomProfiler.lua b/mods/noita-mp/files/scripts/util/CustomProfiler.lua index fc8ce7902..3f279a07e 100644 --- a/mods/noita-mp/files/scripts/util/CustomProfiler.lua +++ b/mods/noita-mp/files/scripts/util/CustomProfiler.lua @@ -134,7 +134,7 @@ function CustomProfiler:getSize() end ---CustomProfiler constructor. ----@param customProfilerObject CustomProfiler|nil require("CustomProfiler") or nil +---@param customProfiler CustomProfiler|nil require("CustomProfiler") or nil ---@param fileUtils FileUtils|nil can be nil ---@param noitaMpSettings NoitaMpSettings required ---@param plotly plotly|nil can be nil @@ -142,22 +142,33 @@ end ---@param utils Utils|nil can be nil ---@param winapi winapi|nil can be nil ---@return CustomProfiler -function CustomProfiler:new(customProfilerObject, fileUtils, noitaMpSettings, plotly, socket, utils, winapi) - local customProfiler = customProfilerObject or self or {} -- Use self if this is called as a class constructor - setmetatable(customProfiler, self) - self.__index = self +function CustomProfiler:new(customProfiler, fileUtils, noitaMpSettings, plotly, socket, utils, winapi) + ---@class CustomProfiler + customProfiler = setmetatable(customProfiler or self, CustomProfiler) - local cpc = self:start("CustomProfiler:new") + local cpc = customProfiler:start("CustomProfiler:new") -- Initialize all imports to avoid recursive imports - self.fileUtils = fileUtils or require("FileUtils") --:new() - self.noitaMpSettings = noitaMpSettings or require("NoitaMpSettings") --:new() - self.plotly = plotly or require("plotly") --:new() - self.socket = socket or require("socket") - self.utils = utils or require("Utils") --:new() - self.winapi = winapi or require("winapi") - - self:stop("CustomProfiler:new", cpc) + if not customProfiler.fileUtils then + customProfiler.fileUtils = fileUtils or require("FileUtils") --:new() + end + if not customProfiler.noitaMpSettings then + customProfiler.noitaMpSettings = noitaMpSettings or error("CustomProfiler:new requires a NoitaMpSettings object", 2) + end + if not customProfiler.plotly then + customProfiler.plotly = plotly or require("plotly") --:new() + end + if not customProfiler.socket then + customProfiler.socket = socket or require("socket") + end + if not customProfiler.utils then + customProfiler.utils = utils or require("Utils") --:new() + end + if not customProfiler.winapi then + customProfiler.winapi = winapi or require("winapi") + end + + customProfiler:stop("CustomProfiler:new", cpc) return customProfiler end diff --git a/mods/noita-mp/files/scripts/util/EntityCache.lua b/mods/noita-mp/files/scripts/util/EntityCache.lua index a9f8977bc..c491237d9 100644 --- a/mods/noita-mp/files/scripts/util/EntityCache.lua +++ b/mods/noita-mp/files/scripts/util/EntityCache.lua @@ -126,12 +126,14 @@ function EntityCache:usage() return EntityCacheC.usage() end ----comment ----@param entityCacheObject EntityCache|nil ----@param otherClassesIfRequireLoop any +---EntityCache constructor +---@param entityCacheObject EntityCache|nil optional +---@param customProfiler CustomProfiler required +---@param entityUtils EntityUtils|nil optional +---@param utils Utils|nil optional ---@return EntityCache function EntityCache:new(entityCacheObject, customProfiler, entityUtils, utils) - local entityCacheObject = entityCacheObject or self or {} -- Use self if this is called as a class constructor + entityCacheObject = entityCacheObject or self or {} -- Use self if this is called as a class constructor setmetatable(entityCacheObject, self) self.__index = self diff --git a/mods/noita-mp/files/scripts/util/Logger.lua b/mods/noita-mp/files/scripts/util/Logger.lua index 31ef3e875..4693e65eb 100644 --- a/mods/noita-mp/files/scripts/util/Logger.lua +++ b/mods/noita-mp/files/scripts/util/Logger.lua @@ -131,7 +131,7 @@ function Logger:new(loggerObject, customProfiler) local cpc = customProfiler:start("Logger:new") -- Initialize all imports to avoid recursive imports - self.customProfiler = customProfiler or error("Logger:new requires a CustomProfiler object", 2) + self.customProfiler = customProfiler self:trace(self.channels.initialize, "Logger was initialized!") self.customProfiler:stop("Logger:new", cpc) diff --git a/mods/noita-mp/files/scripts/util/NetworkUtils.lua b/mods/noita-mp/files/scripts/util/NetworkUtils.lua index e98dbe75f..a3c855d0a 100644 --- a/mods/noita-mp/files/scripts/util/NetworkUtils.lua +++ b/mods/noita-mp/files/scripts/util/NetworkUtils.lua @@ -1,16 +1,3 @@ --- OOP class definition is found here: Closure approach --- http://lua-users.org/wiki/ObjectOrientationClosureApproach --- Naming convention is found here: --- http://lua-users.org/wiki/LuaStyleGuide#:~:text=Lua%20internal%20variable%20naming%20%2D%20The,but%20not%20necessarily%2C%20e.g.%20_G%20. - - ---- 'Imports' - ---local Utils = require("Utils") - - ---- NetworkUtils - NetworkUtils = {} NetworkUtils.networkMessageIdCounter = 0 diff --git a/mods/noita-mp/init.lua b/mods/noita-mp/init.lua index 83d4f58d8..c81053ea7 100644 --- a/mods/noita-mp/init.lua +++ b/mods/noita-mp/init.lua @@ -8,19 +8,20 @@ end dofile("mods/noita-mp/files/scripts/init/init_.lua") local np = require("noitapatcher") -- Need to be initialized before everything else, otherwise Noita will crash -local guiI = require("Gui").new() +local client = require("Client"):new() -local noitaMpSettings = require("NoitaMpSettings") +local gui = require("Gui"):new(nil, client, client.customProfiler, nil, nil, client.noitaMpSettings) +client.noitaMpSettings.gui = gui + +local noitaMpSettings = client.noitaMpSettings or require("NoitaMpSettings") :new(nil, nil, guiI, nil, nil, nil, nil, nil, nil) -local customProfiler = require("CustomProfiler") +---@class CustomProfiler +local customProfiler = client.customProfiler or require("CustomProfiler") :new(noitaMpSettings.customProfiler, noitaMpSettings.fileUtils, noitaMpSettings, nil, nil, noitaMpSettings.utils, noitaMpSettings.winapi) customProfiler.testNumber = 10 customProfiler.testString = "reset" -local Client = require("Client") -local client = Client:new() - local entityUtils = require("EntityUtils"):new(nil, require("Client"):new(), customProfiler) local FileUtils = require("FileUtils") local Logger = require("Logger") diff --git a/mods/noita-mp/lua_modules/share/lua/5.1/sock.lua b/mods/noita-mp/lua_modules/share/lua/5.1/sock.lua index 4543b4456..c0014a4aa 100644 --- a/mods/noita-mp/lua_modules/share/lua/5.1/sock.lua +++ b/mods/noita-mp/lua_modules/share/lua/5.1/sock.lua @@ -3,6 +3,7 @@ -- * [Examples](https://github.com/camchenry/sock.lua/tree/master/examples) -- @module sock +---@class sock local sock = { _VERSION = 'sock.lua v0.3.0', _DESCRIPTION = 'A Lua networking library for LÖVE games', @@ -244,6 +245,9 @@ end ---@class SockServer local Server = {} local Server_mt = { __index = Server } +sock.getServerMetatable = function() + return Server_mt +end --- NoitaMp Moved this part from newServer to Server:start ---@param ip string @@ -287,7 +291,7 @@ function Server:update() while event do if event.type == "connect" then - local eventClient = ClientInit.new(sock.newClient(event.peer)) + local eventClient = Client:new(sock.newClient(event.peer)) eventClient:establishClient(event.peer) eventClient:setSerialization(self.serialize, self.deserialize) eventClient.clientCacheId = GuidUtils.toNumber(eventClient.guid) @@ -819,6 +823,9 @@ end ---@class SockClient local Client = {} local Client_mt = { __index = Client } +sock.getClientClass = function() + return Client +end function Client:establishClient(serverOrAddress, port) serverOrAddress = serverOrAddress or self.address