From 8bf05294cb36a85436bce638618c20aff1822f87 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Tue, 15 Oct 2024 19:13:00 +0200 Subject: [PATCH 01/83] Update annotations --- engine/User.lua | 5 +++-- engine/User/CUIMapPreview.lua | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/engine/User.lua b/engine/User.lua index 63c82e050a..f2b205cd6a 100644 --- a/engine/User.lua +++ b/engine/User.lua @@ -709,14 +709,15 @@ end ---@alias UILobbyProtocols "UDP" | "TCP" | "None --- For internal use by `CreateLobbyComm()` ----@param lobbyComClass fa-class +---@generic T +---@param lobbyComClass T ---@param protocol UILobbyProtocols ---@param localPort number ---@param maxConnections number ---@param playerName string ---@param playerUID? string ---@param natTraversalProvider? userdata ----@return UILobbyCommunication +---@return T function InternalCreateLobby(lobbyComClass, protocol, localPort, maxConnections, playerName, playerUID, natTraversalProvider) end diff --git a/engine/User/CUIMapPreview.lua b/engine/User/CUIMapPreview.lua index caaf400b4a..9d2c634a91 100644 --- a/engine/User/CUIMapPreview.lua +++ b/engine/User/CUIMapPreview.lua @@ -9,11 +9,13 @@ end --- ---@param textureName string +---@return boolean function CUIMapPreview:SetTexture(textureName) end --- ---@param mapName string +---@return boolean function CUIMapPreview:SetTextureFromMap(mapName) end From 914a3d9868d7b5b48d12e3073c3cf4b1ded21c48 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Tue, 15 Oct 2024 19:13:55 +0200 Subject: [PATCH 02/83] Create a standalone and efficient map preview --- .../lobby/autolobby/AutolobbyMapPreview.lua | 218 ++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 lua/ui/lobby/autolobby/AutolobbyMapPreview.lua diff --git a/lua/ui/lobby/autolobby/AutolobbyMapPreview.lua b/lua/ui/lobby/autolobby/AutolobbyMapPreview.lua new file mode 100644 index 0000000000..c1b5f04678 --- /dev/null +++ b/lua/ui/lobby/autolobby/AutolobbyMapPreview.lua @@ -0,0 +1,218 @@ +--****************************************************************************************************** +--** Copyright (c) 2024 Willem 'Jip' Wijnia +--** +--** Permission is hereby granted, free of charge, to any person obtaining a copy +--** of this software and associated documentation files (the "Software"), to deal +--** in the Software without restriction, including without limitation the rights +--** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +--** copies of the Software, and to permit persons to whom the Software is +--** furnished to do so, subject to the following conditions: +--** +--** The above copyright notice and this permission notice shall be included in all +--** copies or substantial portions of the Software. +--** +--** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +--** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +--** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +--** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +--** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +--** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +--** SOFTWARE. +--****************************************************************************************************** + +local UIUtil = import("/lua/ui/uiutil.lua") +local Group = import("/lua/maui/group.lua").Group +local MapPreview = import("/lua/ui/controls/mappreview.lua").MapPreview +local LayoutHelpers = import("/lua/maui/layouthelpers.lua") +local MapUtil = import("/lua/ui/maputil.lua") +local TexturePool = import("/lua/ui/texturepool.lua").TexturePool +local ACUButton = import("/lua/ui/controls/acubutton.lua").ACUButton +local gameColors = import("/lua/gamecolors.lua").GameColors + +local MapUtil = import("/lua/ui/maputil.lua") + +---@class UIAutolobbyMapPreview : Group +---@field Preview MapPreview +---@field Border Control +---@field Scenario? string +---@field ScenarioInfo? UIScenarioInfo +---@field EnergyIcon Bitmap # Acts as a pool +---@field MassIcon Bitmap # Acts as a pool +---@field WreckageIcon Bitmap # Acts as a pool +---@field IconTrash TrashBag # Trashbag that contains all icons +---@field MassIcons Bitmap[] +---@field EnergyIcons Bitmap[] +---@field WreckageIcons Bitmap[] +AutolobbyMapPreview = ClassUI(Group) { + + ---@param self UIAutolobbyMapPreview + ---@param parent Control + __init = function(self, parent) + Group.__init(self, parent) + + self.Preview = MapPreview(self) + self.Border = UIUtil.SurroundWithBorder(self.Preview, '/scx_menu/lan-game-lobby/frame/') + + self.EnergyIcon = UIUtil.CreateBitmap(self, "/game/build-ui/icon-energy_bmp.dds") + self.MassIcon = UIUtil.CreateBitmap(self, "/game/build-ui/icon-mass_bmp.dds") + self.WreckageIcon = UIUtil.CreateBitmap(self, "/scx_menu/lan-game-lobby/mappreview/wreckage.dds") + + self.IconTrash = TrashBag() + self.EnergyIcons = {} + self.MassIcons = {} + self.WreckageIcons = {} + end, + + ---@param self UIAutolobbyMapPreview + ---@param parent Control + __post_init = function(self, parent) + LayoutHelpers.LayoutFor(self.Preview) + :Fill(self) + :End() + + LayoutHelpers.LayoutFor(self.EnergyIcon) + :Hide() + :End() + + LayoutHelpers.LayoutFor(self.MassIcon) + :Hide() + :End() + + LayoutHelpers.LayoutFor(self.WreckageIcon) + :Hide() + :End() + end, + + --- Creates an icon that shares the texture with a source. + --- + --- This function is private and should not be called from outside the class. + ---@param self UIAutolobbyMapPreview + ---@param scenarioWidth number + ---@param scenarioHeight number + ---@param px number + ---@param pz number + ---@param source Bitmap + ---@return Bitmap + _CreateIcon = function(self, scenarioWidth, scenarioHeight, px, pz, source) + local size = self.Width() + + local xOffset = 0 + local xFactor = 1 + local yOffset = 0 + local yFactor = 1 + if scenarioWidth > scenarioHeight then + local ratio = scenarioHeight / scenarioWidth -- 1/2 + yOffset = ((size / ratio) - size) / 4 + yFactor = ratio + else + local ratio = scenarioWidth / scenarioHeight + xOffset = ((size / ratio) - size) / 4 + xFactor = ratio + end + + -- create an icon + local icon = UIUtil.CreateBitmapColor(self, 'ffffff') + + -- share the texture + icon:ShareTextures(source) + + local x = xOffset + (px / scenarioWidth) * (size - 2) * xFactor - 4 + local z = yOffset + (pz / scenarioHeight) * (size - 2) * yFactor - 4 + + -- position it + LayoutHelpers.LayoutFor(icon) + :Width(14) + :Height(14) + :AtLeftTopIn(self, x, z) + + -- make it disposable + self.IconTrash:Add(icon) + + return icon + end, + + --- Creates the map preview. + --- + --- This function is private and should not be called from outside the class. + ---@param self UIAutolobbyMapPreview + ---@param scenarioInfo UIScenarioInfo + _UpdatePreview = function(self, scenarioInfo) + if not self.Preview:SetTexture(scenarioInfo.preview) then + self.Preview:SetTextureFromMap(scenarioInfo.map) + end + end, + + --- Creates icons for resource markers. + --- + --- This function is private and should not be called from outside the class. + ---@param self UIAutolobbyMapPreview + ---@param scenarioInfo UIScenarioInfo + _UpdateMarkers = function(self, scenarioInfo) + local scenarioWidth = scenarioInfo.size[1] + local scenarioHeight = scenarioInfo.size[2] + + -- load in the save file + self.ScenarioSave = {} + doscript('/lua/dataInit.lua', self.ScenarioSave) + doscript(scenarioInfo.save, self.ScenarioSave) + + local allmarkers = self.ScenarioSave.Scenario.MasterChain['_MASTERCHAIN_'].Markers + if not allmarkers then + return + end + for key, marker in allmarkers do + if marker['type'] == "Mass" then + table.insert(self.MassIcons, + self:_CreateIcon( + scenarioWidth, scenarioHeight, + marker.position[1], marker.position[3], + self.MassIcon + ) + ) + elseif marker['type'] == "Hydrocarbon" then + table.insert(self.EnergyIcons, + self:_CreateIcon( + scenarioWidth, scenarioHeight, + marker.position[1], marker.position[3], + self.EnergyIcon + ) + ) + end + end + end, + + --- Creates icons for wreckages. + --- + --- This function is private and should not be called from outside the class. + ---@param self UIAutolobbyMapPreview + ---@param scenarioInfo UIScenarioInfo + _UpdateWreckages = function(self, scenarioInfo) + -- TODO + end, + + --- Creates icons for spawn locations. + --- + --- This function is private and should not be called from outside the class. + ---@param self UIAutolobbyMapPreview + ---@param scenarioInfo UIScenarioInfo + _UpdateSpawnLocations = function(self, scenarioInfo) + -- TODO + end, + + ---@param self UIAutolobbyMapPreview + ---@param scenario string # a reference to a _scenario.lua file + UpdateScenario = function(self, scenario) + -- clear up previous iteration + self.IconTrash:Destroy() + self.Preview:ClearTexture() + + self.Scenario = scenario + self.ScenarioInfo = MapUtil.LoadScenario(scenario) + if self.ScenarioInfo then + self:_UpdatePreview(self.ScenarioInfo) + self:_UpdateMarkers(self.ScenarioInfo) + self:_UpdateWreckages(self.ScenarioInfo) + self:_UpdateSpawnLocations(self.ScenarioInfo) + end + end, +} From 6a535f300f3ab479d1cc6f2c08745dec6ffb6399 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Thu, 17 Oct 2024 20:44:32 +0200 Subject: [PATCH 03/83] Initial setup with a working preview --- engine/User/CLobby.lua | 63 +- lua/lazyvar.lua | 2 +- lua/ui/lobby/autolobby.lua | 967 ++++++++++-------- lua/ui/lobby/autolobby/AutolobbyInterface.lua | 167 +++ .../lobby/autolobby/AutolobbyMapPreview.lua | 34 +- .../autolobby/AutolobbyMessageHandlers.lua | 162 +++ 6 files changed, 942 insertions(+), 453 deletions(-) create mode 100644 lua/ui/lobby/autolobby/AutolobbyInterface.lua create mode 100644 lua/ui/lobby/autolobby/AutolobbyMessageHandlers.lua diff --git a/engine/User/CLobby.lua b/engine/User/CLobby.lua index a45c48197a..7f757a39b1 100644 --- a/engine/User/CLobby.lua +++ b/engine/User/CLobby.lua @@ -1,8 +1,13 @@ ---@meta ----@class moho.lobby_methods : Destroyable + + +---@class moho.lobby_methods : Destroyable, InternalObject local CLobby = {} +--- "0", "1", "2", etc. +---@alias UILobbyPlayerId string + ---@alias GPGNetAddress string | number ---@class Peer @@ -13,8 +18,56 @@ local CLobby = {} ---@field quiet number ---@field status string +--- A piece of data that is one can send with `BroadcastData` or `SendData` to other player(s) in the lobby. +---@class UILobbyReceivedMessage : table +---@field SenderID UILobbyPlayerId # Set by the engine, allows us to identify the source. +---@field SenderName string # Set by the engine, nickname of the source. +---@field Type string # Type of message + +--- A piece of data that is one can send with `BroadcastData` or `SendData` to other player(s) in the lobby. +---@class UILobbyData : table +---@field Type string # Type of message + +--- All the following fields are read by the engine upon launching the lobby to setup the scenario. +---@class UILobbyLaunchGameOptionsConfiguration +---@field UnitCap any # Read by the engine to determine the initial unit cap. See also the globals `GetArmyUnitCap`, `GetArmyUnitCostTotal` and `SetArmyUnitCap` to manipulate it throughout the scenario. +---@field CheatsEnabled any # Read by the engine to determine whether cheats are enabled. +---@field FogOfWar any # Read by the engine to determine how to manage the fog of war. +---@field NoRushOption any # Read by the engine to create the anti-rush mechanic. +---@field PrebuiltUnits any # Read by the engine to create initial, prebuilt units. +---@field ScenarioFile any # Read by the engine to load the scenario of the game. +---@field Timeouts any # Read by the engine to determine the behavior of time outs. +---@field CivilianAlliance any # Read by the engine to determine the alliance towards civilians. +---@field GameSpeed any # Read by the engine to determine the behavior of game speed (adjustments). + +---@class UILobbyLaunchGameModsConfiguration +---@field name string # Read by the engine, TODO +---@field uid string # Read by the engine, TODO + +---@class UILobbyLaunchObserverConfiguration +---@field OwnerID UILobbyPlayerId # Read by the engine, TODO +---@field PlayerName string # Read by the engine, TODO + +---@class UILobbyLaunchPlayerConfiguration +---@field ArmyName string # Read by the engine, TODO +---@field PlayerName string # Read by the engine, TODO +---@field Civilian boolean # Read by the engine, TODO +---@field Human boolean # Read by the engine, TODO +---@field AIPersonality string # Read by the engine iff Human is false +---@field ArmyColor number # Read by the engine, is mapped to a color by reading the values of `lua\GameColors.lua`. +---@field PlayerColor number # Read by the engine, is mapped to a color by reading the values of `lua\GameColors.lua` +---@field Faction number # Read by the engine to determine the faction of the player. +---@field OwnerID UILobbyPlayerId # Read by the engine, TODO + +--- All the following fields are read by the engine upon launching the lobby. +---@class UILobbyLaunchConfiguration +---@field GameMods UILobbyLaunchGameModsConfiguration[] # ModInfo[] +---@field GameOptions UILobbyLaunchGameOptionsConfiguration # GameOptions +---@field Observers UILobbyLaunchObserverConfiguration # PlayerData[] +---@field PlayerOptions UILobbyLaunchPlayerConfiguration[] # PlayerData[] + --- Broadcasts information to all peers. See `SendData` for sending to a specific peer. ----@param data CommunicationData +---@param data UILobbyData function CLobby:BroadcastData(data) end @@ -45,7 +98,7 @@ function CLobby:EjectPeer(targetID, reason) end --- Retrieves the local client identifier. ----@return number +---@return UILobbyPlayerId function CLobby:GetLocalPlayerID() end @@ -87,7 +140,7 @@ function CLobby:JoinGame(address, remotePlayerName, remotePlayerUID) end --- ----@param gameConfig GameData +---@param gameConfig UILobbyLaunchConfiguration function CLobby:LaunchGame(gameConfig) end @@ -106,7 +159,7 @@ end --- Sends data to a specific peer. See `BroadcastData` for sending to all peers. ---@param targetID string ----@param data CommunicationData +---@param data UILobbyData function CLobby:SendData(targetID, data) end diff --git a/lua/lazyvar.lua b/lua/lazyvar.lua index ec623eed6c..14a6f5d390 100644 --- a/lua/lazyvar.lua +++ b/lua/lazyvar.lua @@ -9,7 +9,7 @@ local setmetatable = setmetatable -- Set this true to get tracebacks in error messages. It slows down lazyvars a lot, -- so don't use except when debugging. -local ExtendedErrorMessages = false +local ExtendedErrorMessages = true local EvalContext = nil local WeakKeyMeta = { __mode = 'k' } diff --git a/lua/ui/lobby/autolobby.lua b/lua/ui/lobby/autolobby.lua index d20479e4e7..301cbcaea4 100644 --- a/lua/ui/lobby/autolobby.lua +++ b/lua/ui/lobby/autolobby.lua @@ -12,497 +12,588 @@ --* through command line arguments. --***************************************************************************** -local UIUtil = import("/lua/ui/uiutil.lua") -local LayoutHelpers = import("/lua/maui/layouthelpers.lua") -local Group = import("/lua/maui/group.lua").Group -local MenuCommon = import("/lua/ui/menus/menucommon.lua") -local LobbyComm = import("/lua/ui/lobby/lobbycomm.lua") -local gameColors = import("/lua/gamecolors.lua").GameColors -local utils = import("/lua/system/utils.lua") - -local ConnectionStatus = import("/lua/ui/lobby/autolobby-classes.lua").ConnectionStatus - - - -local parent = false -local localPlayerName = false -local requiredPlayers = false - -local currentDialog = false -local connectionStatusGUI = false - -local localPlayerID = false - - ---- The default game information for an automatch. This should typically never be changed directly --- as the server can change game options as it wishes since PR 3385. -local gameInfo = { - GameOptions = { - Score = 'no', - TeamSpawn = 'fixed', - TeamLock = 'locked', - Victory = 'demoralization', - Timeouts = '3', - CheatsEnabled = 'false', - CivilianAlliance = 'enemy', - RevealCivilians = 'Yes', - GameSpeed = 'normal', - FogOfWar = 'explored', - UnitCap = '1500', - PrebuiltUnits = 'Off', - Share = 'FullShare', - ShareUnitCap = 'allies', - DisconnectionDelay02 = '90', - - -- yep, great - Ranked = true, - Unranked = 'No', - }, - PlayerOptions = {}, - Observers = {}, - GameMods = {}, +--****************************************************************************************************** +--** Copyright (c) 2024 Willem 'Jip' Wijnia +--** +--** Permission is hereby granted, free of charge, to any person obtaining a copy +--** of this software and associated documentation files (the "Software"), to deal +--** in the Software without restriction, including without limitation the rights +--** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +--** copies of the Software, and to permit persons to whom the Software is +--** furnished to do so, subject to the following conditions: +--** +--** The above copyright notice and this permission notice shall be included in all +--** copies or substantial portions of the Software. +--** +--** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +--** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +--** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +--** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +--** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +--** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +--** SOFTWARE. +--****************************************************************************************************** + +local Utils = import("/lua/system/utils.lua") + +local MohoLobbyMethods = moho.lobby_methods +local DebugComponent = import("/lua/shared/components/DebugComponent.lua").DebugComponent + +local AutolobbyMessageHandlers = import("/lua/ui/lobby/autolobby/AutolobbyMessageHandlers.lua").AutolobbyMessageHandlers + +local AutolobbyEngineStrings = { + -- General info strings + ['Connecting'] = "Connecting to Game", + ['AbortConnect'] = "Abort Connect", + ['TryingToConnect'] = "Connecting...", + ['TimedOut'] = "%s timed out.", + ['TimedOutToHost'] = "Timed out to host.", + ['Ejected'] = "You have been ejected: %s", + ['ConnectionFailed'] = "Connection failed: %s", + ['LaunchFailed'] = "Launch failed: %s", + ['LobbyFull'] = "The game lobby is full.", + + -- Error reasons + ['StartSpots'] = "The map does not support this number of players.", + ['NoConfig'] = "No valid game configurations found.", + ['NoObservers'] = "Observers not allowed.", + ['KickedByHost'] = "Kicked by host.", + ['GameLaunched'] = "Game was launched.", + ['NoLaunchLimbo'] = "No clients allowed in limbo at launch", + ['HostLeft'] = "Host abandoned lobby", + ['LaunchRejected'] = "Some players are using an incompatible client version.", } -local Strings = LobbyComm.Strings - -local lobbyComm = false - -local connectedTo = {} -local peerLaunchStatuses = {} - --- Cancels automatching and closes the game -local function CleanupAndExit() - if lobbyComm then - lobbyComm:Destroy() - end - ExitApplication() -end - --- Replace the currently displayed dialog (there is only 1). -local function SetDialog(...) - if currentDialog then - currentDialog:Destroy() - end - - currentDialog = UIUtil.ShowInfoDialog(unpack(arg)) -end - --- Create PlayerInfo for our local player from command line options -local function MakeLocalPlayerInfo(name) - local result = LobbyComm.GetDefaultPlayerOptions(name) - result.Human = true - - -- Game must have factions for players or else it won't start, so default to UEF. - result.Faction = 1 - local factionData = import("/lua/factions.lua") - for index, tbl in factionData.Factions do - if HasCommandLineArg("/" .. tbl.Key) then - result.Faction = index - break - end - end - - result.Team = tonumber(GetCommandLineArg("/team", 1)[1]) - result.StartSpot = tonumber(GetCommandLineArg("/startspot", 1)[1]) or false - - result.DEV = tonumber(GetCommandLineArg("/deviation", 1)[1] or 500) - result.MEAN = tonumber(GetCommandLineArg("/mean", 1)[1] or 1500) - result.NG = tonumber(GetCommandLineArg("/numgames", 1)[1] or 0) - result.DIV = (GetCommandLineArg("/division", 1)[1]) or "" - result.SUBDIV = (GetCommandLineArg("/subdivision", 1)[1]) or "" - result.PL = math.floor(result.MEAN - 3 * result.DEV) - LOG('Local player info: ' .. repr(result)) - return result -end - -function wasConnected(peer) - return table.find(connectedTo, peer) ~= nil -end - -function FindSlotForID(id) - for k,player in gameInfo.PlayerOptions do - if player.OwnerID == id and player.Human then - return k - end - end - return nil -end - -function IsPlayer(id) - return FindSlotForID(id) ~= nil -end - -local function HostAddPlayer(senderId, playerInfo) - playerInfo.OwnerID = senderId - - local slot = playerInfo.StartSpot or 1 - if not playerInfo.StartSpot then - while gameInfo.PlayerOptions[slot] do - slot = slot + 1 - end - playerInfo.StartSpot = slot - end - - playerInfo.PlayerName = lobbyComm:MakeValidPlayerName(playerInfo.OwnerID,playerInfo.PlayerName) - -- TODO: Should colors be based on teams? - playerInfo.PlayerColor = gameColors.TMMColorOrder[slot] - - gameInfo.PlayerOptions[slot] = playerInfo -end +---@class UIAutolobbyPlayer: UILobbyLaunchPlayerConfiguration +---@field StartSpot number +---@field DEV number # Related to rating/divisions +---@field MEAN number # Related to rating/divisions +---@field NG number # Related to rating/divisions +---@field DIV string # Related to rating/divisions +---@field SUBDIV string # Related to rating/divisions +---@field PL number # Related to rating/divisions + +---@type UIAutolobbyCommunications | false +local AutolobbyCommunicationsInstance = false + +--- Responsible for the behavior of the automated lobby. +---@class UIAutolobbyCommunications : moho.lobby_methods, DebugComponent +---@field Trash TrashBag +---@field InterfaceTrash TrashBag +---@field LocalID UILobbyPlayerId # a number that is stringified +---@field LocalPlayerName string # nickname +---@field LocalConnectedTo table # list of other player identifiers that we're connected to +---@field OthersConnectedTo table> # list of list ofother player identifiers that other players are connected to +---@field HostID UILobbyPlayerId +---@field GameOptions UILobbyLaunchGameOptionsConfiguration # Is synced from the host to the others. +---@field PlayerOptions UIAutolobbyPlayer[] # Is synced from the host to the others. +AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { + + BackgroundTextures = { + "/menus02/background-paint01_bmp.dds", + "/menus02/background-paint02_bmp.dds", + "/menus02/background-paint03_bmp.dds", + "/menus02/background-paint04_bmp.dds", + "/menus02/background-paint05_bmp.dds", + }, ---- Waits to receive confirmation from all players as to whether they share the same --- game options. Is used to reject a game when this is not the case. Typically --- this happens when the players do not share the same (FAF) client. -local function WaitLaunchAccepted() - while true do - local allAccepted = true - for _, status in peerLaunchStatuses do - if status == 'Rejected' then - return false - elseif not status or status ~= 'Accepted' then - allAccepted = false + ---@param self UIAutolobbyCommunications + __init = function(self) + self.Trash = TrashBag() + self.InterfaceTrash = self.Trash:Add(TrashBag()) + + self.LocalID = "-1" + self.LocalPlayerName = "Charlie" + self.ConnectedTo = {} + self.OthersConnectedTo = {} + self.HostID = "-1" + + self.GameOptions = self:CreateLocalGameOptions() + self.PlayerOptions = {} + end, + + ---@param self UIAutolobbyCommunications + __init_post = function(self) + + end, + + --- Creates a table that represents the local player settings. This represents the initial player. It can be edited by the host accordingly. + ---@param self UIAutolobbyCommunications + ---@return UIAutolobbyPlayer + CreateLocalPlayer = function(self) + ---@type UIAutolobbyPlayer + local info = {} + + info.Team = 1 + info.PlayerColor = 1 + info.ArmyColor = 1 + info.Human = true + info.Civilian = false + + -- determine player name + info.PlayerName = self.LocalPlayerName or self:GetLocalPlayerName() or "player" + + -- retrieve faction + info.Faction = 1 + local factionData = import("/lua/factions.lua") + for index, tbl in factionData.Factions do + if HasCommandLineArg("/" .. tbl.Key) then + info.Faction = index break end end - if allAccepted then - return true - end - WaitSeconds(1) - end -end - --- Check if we can launch the game and then do so. To launch the game we need --- to be connected to the correct number of players as configured by the --- command line args. -local function CheckForLaunch() - local important = {} - for slot,player in gameInfo.PlayerOptions do - GpgNetSend('PlayerOption', player.OwnerID, 'StartSpot', slot) - GpgNetSend('PlayerOption', player.OwnerID, 'Army', slot) - GpgNetSend('PlayerOption', player.OwnerID, 'Faction', player.Faction) - GpgNetSend('PlayerOption', player.OwnerID, 'Color', player.PlayerColor) - - if not table.find(important, player.OwnerID) then - table.insert(important, player.OwnerID) - end - end - -- counts the number of players in the game. Include yourself by default. - local playercount = 1 - for k,id in important do - if id ~= localPlayerID then - local peer = lobbyComm:GetPeer(id) - if peer.status ~= 'Established' then - return - end - if not table.find(peer.establishedPeers, localPlayerID) then - return - end - playercount = playercount + 1 - for k2,other in important do - if id ~= other and not table.find(peer.establishedPeers, other) then - return - end + -- retrieve team and start spot + info.Team = tonumber(GetCommandLineArg("/team", 1)[1]) + info.StartSpot = tonumber(GetCommandLineArg("/startspot", 1)[1]) or false + + -- retrieve rating + info.DEV = tonumber(GetCommandLineArg("/deviation", 1)[1]) or 500 + info.MEAN = tonumber(GetCommandLineArg("/mean", 1)[1]) or 1500 + info.NG = tonumber(GetCommandLineArg("/numgames", 1)[1]) or 0 + info.DIV = (GetCommandLineArg("/division", 1)[1]) or "" + info.SUBDIV = (GetCommandLineArg("/subdivision", 1)[1]) or "" + info.PL = math.floor(info.MEAN - 3 * info.DEV) + + return info + end, + + --- Creates a table that represents the local game options. + ---@param self UIAutolobbyCommunications + ---@return UILobbyLaunchGameOptionsConfiguration + CreateLocalGameOptions = function(self) + ---@type UILobbyLaunchGameOptionsConfiguration + local options = { + Score = 'no', + TeamSpawn = 'fixed', + TeamLock = 'locked', + Victory = 'demoralization', + Timeouts = '3', + CheatsEnabled = 'false', + CivilianAlliance = 'enemy', + RevealCivilians = 'Yes', + GameSpeed = 'normal', + FogOfWar = 'explored', + UnitCap = '1500', + PrebuiltUnits = 'Off', + Share = 'FullShare', + ShareUnitCap = 'allies', + DisconnectionDelay02 = '90', + + -- yep, great + Ranked = true, + Unranked = 'No', + } + + -- process game options from the command line + for name, value in Utils.GetCommandLineArgTable("/gameoptions") do + if name and value then + options[name] = value + else + LOG("Malformed gameoption. ignoring name: " .. repr(name) .. " and value: " .. repr(value)) end end - end - if playercount < requiredPlayers then - return - end + return options + end, - local allRatings = {} - local allDivisions = {} - for k,v in gameInfo.PlayerOptions do - if v.Human and v.PL then - allRatings[v.PlayerName] = v.PL - if v.DIV ~= "unlisted" then - local divisiontext = v.DIV - if v.SUBDIV and v.SUBDIV ~="" then - divisiontext = divisiontext .. ' ' .. v.SUBDIV - end - allDivisions[v.PlayerName]= divisiontext - end - -- Initialize peer launch statuses - peerLaunchStatuses[v.OwnerID] = false + --- A thread to indicate that we're still around. Various properties such as ping are not updated + --- until a message is received. This thread introduces occasional traffic between players. + ---@param self UIAutolobbyCommunications + IsAliveThread = function(self) + while not IsDestroyed(self) do + self:BroadcastData({ Type = "IsAlive" }) + WaitSeconds(1.0) end - end - -- We don't need to wait for a launch status from ourselves - peerLaunchStatuses[localPlayerID] = nil - gameInfo.GameOptions['Ratings'] = allRatings - gameInfo.GameOptions['Divisions'] = allDivisions - - LOG("Host launching game.") - lobbyComm:BroadcastData({ Type = 'Launch', GameInfo = gameInfo }) - LOG(repr(gameInfo)) - - ForkThread(function() - if WaitLaunchAccepted() then - lobbyComm:LaunchGame(gameInfo) - return + end, + + --------------------------------------------------------------------------- + --#region Engine interface + + --- Broadcasts data to all (connected) peers. + ---@param self UIAutolobbyCommunications + ---@param data UILobbyData + BroadcastData = function(self, data) + self:DebugSpew("BroadcastData", data.Type) + if not AutolobbyMessageHandlers[data.Type] then + self:DebugWarn("Broadcasting unknown message type", data.Type) end - LOG("Some players rejected the launch! " .. repr(peerLaunchStatuses)) - SetDialog(parent, Strings.LaunchRejected, "", CleanupAndExit) - end) -end - - -local function CreateUI() - - LOG("Don't mind me x2") - - if currentDialog ~= false then - MenuCommon.MenuCleanup() - currentDialog:Destroy() - currentDialog = false - end - - -- control layout - if not parent then parent = UIUtil.CreateScreenGroup(GetFrame(0), "Lobby CreateUI ScreenGroup") end - - local background = MenuCommon.SetupBackground(GetFrame(0)) - - SetDialog(parent, "Setting up automatch...") - - -- construct the connection status GUI and position it right below the dialog - connectionStatusGUI = ConnectionStatus(GetFrame(0)) - LayoutHelpers.CenteredBelow(connectionStatusGUI, currentDialog, 20) - LayoutHelpers.DepthOverParent(connectionStatusGUI, background, 1) -end - - --- LobbyComm Callbacks -local function InitLobbyComm(protocol, localPort, desiredPlayerName, localPlayerUID, natTraversalProvider) - local LobCreateFunc = import("/lua/ui/lobby/lobbycomm.lua").CreateLobbyComm - local lob = LobCreateFunc(protocol, localPort, desiredPlayerName, localPlayerUID, natTraversalProvider) - if not lob then - error('Creating lobby using protocol ' .. repr(protocol) .. ' and port ' .. tostring(localPort) .. ' failed.') - end - lobbyComm = lob - - lobbyComm.Connecting = function(self) - SetDialog(parent, Strings.Connecting, "", CleanupAndExit) - end - - lobbyComm.ConnectionFailed = function(self, reason) - LOG("CONNECTION FAILED " .. reason) - SetDialog(parent, LOCF(Strings.ConnectionFailed, reason), "", CleanupAndExit) - end - - lobbyComm.LaunchFailed = function(self, reasonKey) - LOG("LAUNCH FAILED") - SetDialog(parent, LOCF(Strings.LaunchFailed,LOC(reasonKey)), "", CleanupAndExit) - end - - lobbyComm.Ejected = function(self, reason) - LOG("EJECTED " .. reason) - SetDialog(parent, Strings.Ejected, "", CleanupAndExit) - end - - lobbyComm.ConnectionToHostEstablished = function(self, myID, newLocalName, theHostID) - LOG("CONNECTED TO HOST") - hostID = theHostID - localPlayerName = newLocalName - localPlayerID = myID - - -- Ok, I'm connected to the host. Now request to become a player - self:SendData(hostID, { Type = 'AddPlayer', PlayerInfo = MakeLocalPlayerInfo(newLocalName), }) - end - - lobbyComm.DataReceived = function(self, data) - LOG('DATA RECEIVED: ', reprsl(data)) + return MohoLobbyMethods.BroadcastData(self, data) + end, + + --- (Re)Connects to a peer. + ---@param self any + ---@param address any + ---@param name any + ---@param uid any + ---@return nil + ConnectToPeer = function(self, address, name, uid) + self:DebugSpew("ConnectToPeer", address, name, uid) + return MohoLobbyMethods.ConnectToPeer(self, address, name, uid) + end, + + --- ??? + ---@param self UIAutolobbyCommunications + ---@return nil + DebugDump = function(self) + self:DebugSpew("DebugDump") + return MohoLobbyMethods.DebugDump(self) + end, + + --- Destroys the C-object and all the (UI) entities in the trash bag. + ---@param self UIAutolobbyCommunications + ---@return nil + Destroy = function(self) + self:DebugSpew("Destroy") + + self.Trash:Destroy() + return MohoLobbyMethods.Destroy(self) + end, + + --- Disconnects from a peer. + --- See also `ConnectToPeer` to connect + ---@param self UIAutolobbyCommunications + ---@param uid any + ---@return nil + DisconnectFromPeer = function(self, uid) + self:DebugSpew("DisconnectFromPeer", uid) + return MohoLobbyMethods.DisconnectFromPeer(self, uid) + end, + + + EjectPeer = function(self, uid, reason) + self:DebugSpew("EjectPeer", uid, reason) + return MohoLobbyMethods.EjectPeer(self, uid, reason) + end, + + GetLocalPlayerID = function(self) + self:DebugSpew("GetLocalPlayerID") + return MohoLobbyMethods.GetLocalPlayerID(self) + end, + + GetLocalPlayerName = function(self) + self:DebugSpew("GetLocalPlayerName") + return MohoLobbyMethods.GetLocalPlayerName(self) + end, + + GetLocalPort = function(self) + self:DebugSpew("GetLocalPort") + return MohoLobbyMethods.GetLocalPort(self) + end, + + GetPeer = function(self, uid) + self:DebugSpew("GetPeer", uid) + return MohoLobbyMethods.GetPeer(self, uid) + end, + + GetPeers = function(self) + self:DebugSpew("GetPeers") + return MohoLobbyMethods.GetPeers(self) + end, + + HostGame = function(self) + self:DebugSpew("HostGame") + return MohoLobbyMethods.HostGame(self) + end, + + IsHost = function(self) + self:DebugSpew("IsHost") + return MohoLobbyMethods.IsHost(self) + end, + + JoinGame = function(self, address, remotePlayerName, remotePlayerUID) + self:DebugSpew("JoinGame", address, remotePlayerName, remotePlayerUID) + return MohoLobbyMethods.JoinGame(self, address, remotePlayerName, remotePlayerUID) + end, + + LaunchGame = function(self, gameConfig) + self:DebugSpew("LaunchGame", gameConfig) + return MohoLobbyMethods.LaunchGame(self, gameConfig) + end, + + MakeValidGameName = function(self, name) + + self:DebugSpew("MakeValidGameName", name) + return MohoLobbyMethods.MakeValidGameName(self, name) + end, + + MakeValidPlayerName = function(self, uid, name) + self:DebugSpew("MakeValidPlayerName", uid, name) + return MohoLobbyMethods.MakeValidPlayerName(self, uid, name) + end, + + ---@param self UIAutolobbyCommunications + ---@param uid UILobbyPlayerId + ---@param data UILobbyData + ---@return nil + SendData = function(self, uid, data) + self:DebugSpew("SendData", uid, data.Type) + if not AutolobbyMessageHandlers[data.Type] then + self:DebugWarn("Sending unknown message type", data.Type, "to", uid) + end - if data.Type == 'LaunchStatus' then - peerLaunchStatuses[data.SenderID] = data.Status + return MohoLobbyMethods.SendData(self, uid, data) + end, + + --#endregion + + --------------------------------------------------------------------------- + --#region Connection events + + --- Called by the engine as we're trying to host a lobby. + ---@param self UIAutolobbyCommunications + Hosting = function(self) + self:DebugSpew("Hosting") + + self.LocalID = self:GetLocalPlayerID() + self.LocalPlayerName = self:GetLocalPlayerName() + self.HostID = self:GetLocalPlayerID() + + -- occasionally send data over the network to create pings on screen + self.Trash:Add(ForkThread(self.IsAliveThread, self)) + + -- update UI for game options + import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() + :UpdateGameOptions(self.GameOptions) + end, + + --- Called by the engine as we're trying to join a lobby. + ---@param self UIAutolobbyCommunications + Connecting = function(self) + self:DebugSpew("Connecting") + end, + + --- Called by the engine when the connection fails. + ---@param self UIAutolobbyCommunications + ---@param reason string # reason for connection failure, populated by the engine + ConnectionFailed = function(self, reason) + self:DebugSpew("ConnectionFailed", reason) + end, + + --- Called by the engine when the connection succeeds with the host. + ---@param self UIAutolobbyCommunications + ---@param localId string + ---@param hostId string + ConnectionToHostEstablished = function(self, localId, newLocalName, hostId) + self:DebugSpew("ConnectionToHostEstablished", localId, newLocalName, hostId) + self.LocalPlayerName = newLocalName + self.LocalID = localId + self.HostID = hostId + + -- occasionally send data over the network to create pings on screen + self.Trash:Add(ForkThread(self.IsAliveThread, self)) + self:SendData(self.HostID, { Type = "AddPlayer", PlayerOptions = self:CreateLocalPlayer() }) + end, + + --- Called by the engine when a peer establishes a connection. + ---@param self UIAutolobbyCommunications + ---@param playerId string + ---@param playerConnectedTo string[] # all established conenctions for the given player + EstablishedPeers = function(self, playerId, playerConnectedTo) + self:DebugSpew("EstablishedPeers", playerId, reprs(playerConnectedTo)) + end, + + --#endregion + + --------------------------------------------------------------------------- + --#region Lobby events + + --- Called by the engine when you are ejected from a lobby. + ---@param self UIAutolobbyCommunications + ---@param reason string # reason for disconnection, populated by the host + Ejected = function(self, reason) + self:DebugSpew("Ejected", reason) + end, + + --- ??? + ---@param self UIAutolobbyCommunications + ---@param text string + SystemMessage = function(self, text) + self:DebugSpew("SystemMessage", text) + end, + + --- Called by the engine when we receive data from other players. There is no checking to see if the data is legitimate, these need to be done in Lua. + --- + --- Data can be send via `BroadcastData` and/or `SendData`. + ---@param self UIAutolobbyCommunications + ---@param data UILobbyReceivedMessage + DataReceived = function(self, data) + self:DebugSpew("DataReceived", data.Type, data.SenderID, data.SenderName) + + ---@type UIAutolobbyMessageHandler? + local messageType = AutolobbyMessageHandlers[data.Type] + + -- verify that the message type exists + if not messageType then + self:DebugError('Unknown message received: ', data.Type) return end - if self:IsHost() then - -- Host Messages - if data.Type == 'AddPlayer' then - HostAddPlayer(data.SenderID, data.PlayerInfo) - end - else - -- Non-Host Messages - if data.Type == 'Launch' then - -- The client compares the game options with those of the host. They both look like the local 'gameInfo' as defined - -- above, but the host adds these fields upon launch (see: CheckForLaunch) so that we can display them on the scoreboard. - -- A client won't have this information attached, and therefore we remove it manually here - local hostOptions = table.copy(data.GameInfo.GameOptions) - hostOptions['Ratings'] = nil - hostOptions['ScenarioFile'] = nil - hostOptions['Divisions'] = nil - - -- This is a sanity check so we don't accidentally launch games - -- with the wrong game settings because the host is using a - -- client that doesn't support game options for matchmaker. - if not table.equal(gameInfo.GameOptions, hostOptions) then - WARN("Game options missmatch!") - - LOG("Client settings: ") - reprsl(gameInfo.GameOptions) - - LOG("Host settings: ") - reprsl(hostOptions) - - SetDialog(parent, Strings.LaunchRejected, "", CleanupAndExit) - - self:BroadcastData({ Type = 'LaunchStatus', Status = 'Rejected' }) - -- To distinguish this from regular failed connections - GpgNetSend('LaunchStatus', 'Rejected') - else - self:BroadcastData({ Type = 'LaunchStatus', Status = 'Accepted' }) - self:LaunchGame(data.GameInfo) - end - end + -- verify that we can accept it + if not messageType.Accept(self, data) then + self:DebugWarn("Message rejected: ", data.Type) + return end - end - lobbyComm.SystemMessage = function(self, text) - LOG("System: ",text) - end - - lobbyComm.GameLaunched = function(self) - GpgNetSend('GameState', 'Launching') - parent:Destroy() - parent = false - MenuCommon.MenuCleanup() - lobbyComm:Destroy() - lobbyComm = false - end + -- handle the message + messageType.Handler(self, data) + end, + + --- Called by the engine when the game configuration is requested by the discovery service. + ---@param self UIAutolobbyCommunications + GameConfigRequested = function(self) + self:DebugSpew("GameConfigRequested") + end, + + --- Called by the engine when a peer disconnects. + ---@param self UIAutolobbyCommunications + ---@param peerName string + ---@param otherId string + PeerDisconnected = function(self, peerName, otherId) + self:DebugSpew("PeerDisconnected", peerName, otherId) + end, + + --- Called by the engine when the game is launched. + ---@param self UIAutolobbyCommunications + GameLaunched = function(self) + self:DebugSpew("GameLaunched") + end, + + --- Called by the engine when the launch failed. + ---@param self UIAutolobbyCommunications + ---@param reasonKey string + LaunchFailed = function(self, reasonKey) + self:DebugSpew("LaunchFailed", reasonKey) + end, + + --#endregion + + --#region Debugging + + ---@param self UIAutolobbyCommunications + ---@param ... any + DebugSpew = function(self, ...) + if not self.EnabledSpewing then + return + end - lobbyComm.Hosting = function(self) - localPlayerID = self:GetLocalPlayerID() - hostID = localPlayerID + SPEW("Autolobby communications", unpack(arg)) + end, - -- Give myself the first slot - HostAddPlayer(hostID, MakeLocalPlayerInfo(localPlayerName)) - -- Fill in the desired scenario. - gameInfo.GameOptions.ScenarioFile = self.desiredScenario - end - - lobbyComm.EstablishedPeers = function(self, uid, peers) - if not wasConnected(uid) then - table.insert(connectedTo, uid) + ---@param self UIAutolobbyCommunications + ---@param ... any + DebugLog = function(self, ...) + if not self.EnabledLogging then + return end - -- update ui to inform players - connectionStatusGUI:SetPlayersConnectedCount(table.getn(connectedTo)) + LOG("Autolobby communications", unpack(arg)) + end, - if self:IsHost() then - CheckForLaunch() + ---@param self UIAutolobbyCommunications + ---@param ... any + DebugWarn = function(self, ...) + if not self.EnabledWarnings then + return end - end - lobbyComm.PeerDisconnected = function(self, peerName, peerID) - LOG('>DEBUG> PeerDisconnected : peerName='..peerName..' peerID='..peerID) - if IsPlayer(peerID) then - local slot = FindSlotForID(peerID) - if slot and self:IsHost() then - gameInfo.PlayerOptions[slot] = nil - end + WARN("Autolobby communications", unpack(arg)) + end, + + ---@param self UIAutolobbyCommunications + ---@param ... any + DebugError = function(self, ...) + if not self.EnabledErrors then + return end - end -end + error("Autolobby communications", unpack(arg)) + end, + --#endregion +} --- Create a new unconnected lobby. +--- Creates the lobby communications, called (indirectly) by the engine. +---@param protocol any +---@param localPort any +---@param desiredPlayerName any +---@param localPlayerUID any +---@param natTraversalProvider any function CreateLobby(protocol, localPort, desiredPlayerName, localPlayerUID, natTraversalProvider) - if not parent then parent = UIUtil.CreateScreenGroup(GetFrame(0), "CreateLobby ScreenGroup") end - -- don't parent background to screen group so it doesn't get destroyed until we leave the menus - local background = MenuCommon.SetupBackground(GetFrame(0)) + LOG("CreateLobby", protocol, localPort, desiredPlayerName, localPlayerUID, natTraversalProvider) - -- construct the initial dialog - SetDialog(parent, Strings.TryingToConnect) + local maxConnections = 16 + AutolobbyCommunicationsInstance = InternalCreateLobby( + AutolobbyCommunications, + protocol, localPort, maxConnections, desiredPlayerName, + localPlayerUID, natTraversalProvider + ) - InitLobbyComm(protocol, localPort, desiredPlayerName, localPlayerUID, natTraversalProvider) - - localPlayerName = lobbyComm:GetLocalPlayerName() + -- create the singleton for the interface + import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() end - --- create the lobby as a host +--- Instantiates a lobby instance by hosting one. +--- +--- Assumes that the lobby communications to be initialized by calling `CreateLobby`. +---@param gameName any +---@param scenarioFileName any +---@param singlePlayer any function HostGame(gameName, scenarioFileName, singlePlayer) - CreateUI() + LOG("HostGame", gameName, scenarioFileName, singlePlayer) - requiredPlayers = 2 - local args = GetCommandLineArg("/players", 1) - if args then - requiredPlayers = tonumber(args[1]) - LOG("requiredPlayers was set to: "..requiredPlayers) + if AutolobbyCommunicationsInstance then + AutolobbyCommunicationsInstance.GameOptions.ScenarioFile = string.gsub(scenarioFileName, + ".v%d%d%d%d_scenario.lua", + "_scenario.lua") + AutolobbyCommunicationsInstance:HostGame() end - SetGameOptionsFromCommandLine() - - -- update the connection status GUI - connectionStatusGUI:SetTotalPlayersCount(requiredPlayers) - - -- The guys at GPG were unable to make a standard for map. We dirty-solve it. - lobbyComm.desiredScenario = string.gsub(scenarioFileName, ".v%d%d%d%d_scenario.lua", "_scenario.lua") - - lobbyComm:HostGame() + -- start with a loading dialog + import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() + :CreateLoadingDialog() end --- join an already existing lobby +--- Joins an instantiated lobby instance. +--- +--- Assumes that the lobby communications to be initialized by calling `CreateLobby`. +---@param address any +---@param asObserver any +---@param playerName any +---@param uid any function JoinGame(address, asObserver, playerName, uid) - LOG("Joingame (name=" .. tostring(playerName) .. ", uid=" .. tostring(uid) .. ", address=" .. tostring(address) ..")") - CreateUI() - - -- TODO: I'm not sure if this argument is passed along when you are joining a lobby - requiredPlayers = 2 - local args = GetCommandLineArg("/players", 1) - if args then - requiredPlayers = tonumber(args[1]) - LOG("requiredPlayers was set to: "..requiredPlayers) - end - - SetGameOptionsFromCommandLine() - - -- update the connection status GUI - connectionStatusGUI:SetTotalPlayersCount(requiredPlayers) - - lobbyComm:JoinGame(address, playerName, uid) -end + LOG("JoinGame", address, asObserver, playerName, uid) -function ConnectToPeer(addressAndPort,name,uid) - if not string.find(addressAndPort, '127.0.0.1') then - LOG("ConnectToPeer (name=" .. name .. ", uid=" .. uid .. ", address=" .. addressAndPort ..")") - else - DisconnectFromPeer(uid, true) - LOG("ConnectToPeer (name=" .. name .. ", uid=" .. uid .. ", address=" .. addressAndPort ..", USE PROXY)") + if AutolobbyCommunicationsInstance then + AutolobbyCommunicationsInstance:JoinGame(address, playerName, uid) end - -- update ui to inform players - connectionStatusGUI:AddConnectedPlayer() - - lobbyComm:ConnectToPeer(addressAndPort,name,uid) + -- start with a loading dialog + import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() + :CreateLoadingDialog() end -function DisconnectFromPeer(uid, doNotUpdateView) - LOG("DisconnectFromPeer (uid=" .. uid ..")") - if wasConnected(uid) then - table.remove(connectedTo, uid) - end - GpgNetSend('Disconnected', string.format("%d", uid)) +--- Called by the engine. +---@param addressAndPort any +---@param name any +---@param uid any +function ConnectToPeer(addressAndPort, name, uid) + LOG("ConnectToPeer", addressAndPort, name, uid) - -- sometimes we disconnect immediately, but secretly connect through a proxy - if not doNotUpdateView then - connectionStatusGUI:RemoveConnectedPlayer() + if AutolobbyCommunicationsInstance then + AutolobbyCommunicationsInstance:ConnectToPeer(addressAndPort, name, uid) end - - lobbyComm:DisconnectFromPeer(uid) end +--- Called by the engine. +---@param uid any +---@param doNotUpdateView any +function DisconnectFromPeer(uid, doNotUpdateView) + LOG("DisconnectFromPeer", uid, doNotUpdateView) -function SetGameOptionsFromCommandLine() - for name, value in utils.GetCommandLineArgTable("/gameoptions") do - if name and value then - gameInfo.GameOptions[name] = value - else - LOG("Malformed gameoption. ignoring name: " .. repr(name) .. " and value: " .. repr(value)) - end + if AutolobbyCommunicationsInstance then + AutolobbyCommunicationsInstance:DisconnectFromPeer(uid) end end diff --git a/lua/ui/lobby/autolobby/AutolobbyInterface.lua b/lua/ui/lobby/autolobby/AutolobbyInterface.lua new file mode 100644 index 0000000000..5273990c32 --- /dev/null +++ b/lua/ui/lobby/autolobby/AutolobbyInterface.lua @@ -0,0 +1,167 @@ +--****************************************************************************************************** +--** Copyright (c) 2024 Willem 'Jip' Wijnia +--** +--** Permission is hereby granted, free of charge, to any person obtaining a copy +--** of this software and associated documentation files (the "Software"), to deal +--** in the Software without restriction, including without limitation the rights +--** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +--** copies of the Software, and to permit persons to whom the Software is +--** furnished to do so, subject to the following conditions: +--** +--** The above copyright notice and this permission notice shall be included in all +--** copies or substantial portions of the Software. +--** +--** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +--** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +--** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +--** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +--** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +--** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +--** SOFTWARE. +--****************************************************************************************************** + +-- This module is designed to support a form of 'hot reload' that is seen in modern programming +-- languages. To make this possible there can be only one instance of the class that this module +-- represents. And no direct references of the module and/or of the instance should be kept. In +-- short: +-- +-- - (1) Always import the module whenever you need to interact with it. +-- - (2) Always use the `GetSingleton` helper function to obtain a reference to the instance. + +local UIUtil = import("/lua/ui/uiutil.lua") +local LayoutHelpers = import("/lua/maui/layouthelpers.lua") + +local Group = import("/lua/maui/group.lua").Group +local AutolobbyMapPreview = import("/lua/ui/lobby/autolobby/AutolobbyMapPreview.lua") + +---@class UIAutolobbyInterfaceState +---@field PlayerOptions? table +---@field GameOptions? UILobbyLaunchGameOptionsConfiguration + +---@class UIAutolobbyInterface : Group +---@field State UIAutolobbyInterfaceState +---@field BackgroundTextures string[] +---@field Background Bitmap +---@field Preview UIAutolobbyMapPreview +local AutolobbyInterface = Class(Group) { + + BackgroundTextures = { + "/menus02/background-paint01_bmp.dds", + "/menus02/background-paint02_bmp.dds", + "/menus02/background-paint03_bmp.dds", + "/menus02/background-paint04_bmp.dds", + "/menus02/background-paint05_bmp.dds", + }, + + ---@param self UIAutolobbyInterface + ---@param parent Control + __init = function(self, parent) + Group.__init(self, parent, "AutolobbyInterface") + + -- initial, empty state + self.State = {} + + self.Background = UIUtil.CreateBitmap(self, self.BackgroundTextures[math.random(1, 5)]) + self.Preview = AutolobbyMapPreview.GetInstance(self) + end, + + ---@param self UIAutolobbyInterface + ---@param parent Control + __post_init = function(self, parent) + LayoutHelpers.LayoutFor(self) + :Fill(parent) + :End() + + LayoutHelpers.LayoutFor(self.Background) + :Fill(self) + :End() + + LayoutHelpers.LayoutFor(self.Preview) + :AtCenterIn(self) + :Width(400) + :Height(400) + :Hide() + :End() + end, + + ---@param self UIAutolobbyInterface + ---@param playerOptions table + UpdatePlayerOptions = function(self, playerOptions) + self.State.PlayerOptions = playerOptions + end, + + ---@param self UIAutolobbyInterface + ---@param gameOptions UILobbyLaunchGameOptionsConfiguration + UpdateGameOptions = function(self, gameOptions) + self.State.GameOptions = gameOptions + + local scenarioFile = self.State.GameOptions.ScenarioFile + if scenarioFile then + self.Preview:Show() + self.Preview:UpdateScenario(scenarioFile) + else + self.Preview:Hide() + end + end, + + --#region Debugging + + ---@param self UIAutolobbyInterface + ---@param state UIAutolobbyInterfaceState + RestoreState = function(self, state) + if state.PlayerOptions then + self:UpdatePlayerOptions(state.PlayerOptions) + end + + if state.GameOptions then + self:UpdateGameOptions(state.GameOptions) + end + end, + + --#endregion + +} + +--- A trashbag that should be destroyed upon reload. +local ModuleTrash = TrashBag() + +---@type UIAutolobbyInterface | false +local AutolobbyInterfaceInstance = false + +---@return UIAutolobbyInterface +GetSingleton = function() + if AutolobbyInterfaceInstance then + return AutolobbyInterfaceInstance + end + + AutolobbyInterfaceInstance = AutolobbyInterface(GetFrame(0)) + ModuleTrash:Add(AutolobbyInterfaceInstance) + return AutolobbyInterfaceInstance +end + +------------------------------------------------------------------------------- +--#region Debugging + +--- Called by the module manager when this module is reloaded +---@param newModule any +function __moduleinfo.OnReload(newModule) + if AutolobbyInterfaceInstance then + local handle = newModule.GetSingleton(GetFrame(0)) + handle:RestoreState(AutolobbyInterfaceInstance.State) + end +end + +--- Called by the module manager when this module becomes dirty +function __moduleinfo.OnDirty() + ModuleTrash:Destroy() + + -- trigger a reload + ForkThread( + function() + WaitSeconds(1.0) + import(__moduleinfo.name) + end + ) +end + +--#endregionGetSingleton diff --git a/lua/ui/lobby/autolobby/AutolobbyMapPreview.lua b/lua/ui/lobby/autolobby/AutolobbyMapPreview.lua index c1b5f04678..eaca5e264a 100644 --- a/lua/ui/lobby/autolobby/AutolobbyMapPreview.lua +++ b/lua/ui/lobby/autolobby/AutolobbyMapPreview.lua @@ -21,15 +21,11 @@ --****************************************************************************************************** local UIUtil = import("/lua/ui/uiutil.lua") -local Group = import("/lua/maui/group.lua").Group -local MapPreview = import("/lua/ui/controls/mappreview.lua").MapPreview -local LayoutHelpers = import("/lua/maui/layouthelpers.lua") local MapUtil = import("/lua/ui/maputil.lua") -local TexturePool = import("/lua/ui/texturepool.lua").TexturePool -local ACUButton = import("/lua/ui/controls/acubutton.lua").ACUButton -local gameColors = import("/lua/gamecolors.lua").GameColors +local LayoutHelpers = import("/lua/maui/layouthelpers.lua") -local MapUtil = import("/lua/ui/maputil.lua") +local Group = import("/lua/maui/group.lua").Group +local MapPreview = import("/lua/ui/controls/mappreview.lua").MapPreview ---@class UIAutolobbyMapPreview : Group ---@field Preview MapPreview @@ -43,7 +39,7 @@ local MapUtil = import("/lua/ui/maputil.lua") ---@field MassIcons Bitmap[] ---@field EnergyIcons Bitmap[] ---@field WreckageIcons Bitmap[] -AutolobbyMapPreview = ClassUI(Group) { +local AutolobbyMapPreview = ClassUI(Group) { ---@param self UIAutolobbyMapPreview ---@param parent Control @@ -95,7 +91,6 @@ AutolobbyMapPreview = ClassUI(Group) { ---@return Bitmap _CreateIcon = function(self, scenarioWidth, scenarioHeight, px, pz, source) local size = self.Width() - local xOffset = 0 local xFactor = 1 local yOffset = 0 @@ -215,4 +210,25 @@ AutolobbyMapPreview = ClassUI(Group) { self:_UpdateSpawnLocations(self.ScenarioInfo) end end, + + --------------------------------------------------------------------------- + --#region Engine hooks + + ---@param self UIAutolobbyMapPreview + Show = function(self) + Group.Show(self) + + -- do not show the pooled icons + self.EnergyIcon:Hide() + self.MassIcon:Hide() + self.WreckageIcon:Hide() + end, + + --#endregion } + +---@param parent Control +---@return UIAutolobbyMapPreview +GetInstance = function(parent) + return AutolobbyMapPreview(parent) +end \ No newline at end of file diff --git a/lua/ui/lobby/autolobby/AutolobbyMessageHandlers.lua b/lua/ui/lobby/autolobby/AutolobbyMessageHandlers.lua new file mode 100644 index 0000000000..3cb7238a83 --- /dev/null +++ b/lua/ui/lobby/autolobby/AutolobbyMessageHandlers.lua @@ -0,0 +1,162 @@ +--****************************************************************************************************** +--** Copyright (c) 2024 Willem 'Jip' Wijnia +--** +--** Permission is hereby granted, free of charge, to any person obtaining a copy +--** of this software and associated documentation files (the "Software"), to deal +--** in the Software without restriction, including without limitation the rights +--** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +--** copies of the Software, and to permit persons to whom the Software is +--** furnished to do so, subject to the following conditions: +--** +--** The above copyright notice and this permission notice shall be included in all +--** copies or substantial portions of the Software. +--** +--** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +--** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +--** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +--** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +--** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +--** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +--** SOFTWARE. +--****************************************************************************************************** + +---@class UIAutolobbyMessageHandler +---@field Accept fun(lobby: UIAutolobbyCommunications, data: UILobbyReceivedMessage): boolean # Responsible for filtering out non-sense +---@field Handler fun(lobby: UIAutolobbyCommunications, data: UILobbyReceivedMessage) # Responsible for handling the message + +---@param lobby UIAutolobbyCommunications +---@param data UILobbyReceivedMessage +local function IsFromHost(lobby, data) + return data.SenderID == lobby.HostID +end + +---@param lobby UIAutolobbyCommunications +---@param data UILobbyReceivedMessage +local function IsHost(lobby, data) + return lobby:IsHost() +end + +--- Represents all valid message types that can be sent between peers. +---@type table +AutolobbyMessageHandlers = { + IsAlive = { + ---@param lobby UIAutolobbyCommunications + ---@param data UILobbyReceivedMessage + ---@return boolean + Accept = function(lobby, data) + return true + end, + + ---@param lobby UIAutolobbyCommunications + ---@param data UILobbyReceivedMessage + Handler = function(lobby, data) + lobby:DebugSpew("IsAlive handler") + + -- TODO: process the alive tick + end + }, + + AddPlayer = { + ---@param lobby UIAutolobbyCommunications + ---@param data UILobbyReceivedMessage + ---@return boolean + Accept = function(lobby, data) + if not IsHost(lobby, data) then + lobby:DebugWarn("Received message for the host peer of type ", data.Type) + return false + end + + -- verify integrity of the message + ---@type UIAutolobbyPlayer + local playerOptions = data.PlayerOptions + if not playerOptions then + lobby:DebugWarn("Received malformed message of type ", data.Type) + return false + end + + -- verify that the player is not already in the lobby + for _, otherPlayerOptions in lobby.PlayerOptions do + if otherPlayerOptions.OwnerID == data.SenderID then + lobby:DebugWarn("Received duplicate message of type ", data.Type) + return false + end + end + + return true + end, + + ---@param lobby UIAutolobbyCommunications + ---@param data UILobbyReceivedMessage + Handler = function(lobby, data) + + ---@type UIAutolobbyPlayer + local playerOptions = data.PlayerOptions + + -- override some data + playerOptions.OwnerID = data.SenderID + playerOptions.PlayerName = lobby:MakeValidPlayerName(playerOptions.OwnerID, playerOptions.PlayerName) + + -- TODO: verify that the StartSpot is not occupied + -- put the player where it belongs + lobby.PlayerOptions[playerOptions.StartSpot] = playerOptions + + -- sync game options with the connected peer + lobby:SendData(data.SenderID, { Type = "UpdateGameOptions", GameOptions = lobby.GameOptions }) + + -- sync player options to all connected peers + lobby:BroadcastData({ Type = "UpdatePlayerOptions", GameOptions = lobby.PlayerOptions }) + end + }, + + UpdatePlayerOptions = { + ---@param lobby UIAutolobbyCommunications + ---@param data UILobbyReceivedMessage + ---@return boolean + Accept = function(lobby, data) + if not IsFromHost(lobby, data) then + lobby:DebugWarn("Received message from non-host peer of type ", data.Type) + return false + end + + -- TODO: verify integrity of the message + + return true + end, + + ---@param lobby UIAutolobbyCommunications + ---@param data UILobbyReceivedMessage + Handler = function(lobby, data) + lobby.PlayerOptions = data.PlayerOptions + + -- update UI for player options + import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() + :UpdatePlayerOptions(lobby.PlayerOptions) + end + }, + + UpdateGameOptions = { + ---@param lobby UIAutolobbyCommunications + ---@param data UILobbyReceivedMessage + ---@return boolean + Accept = function(lobby, data) + if not IsFromHost(lobby, data) then + lobby:DebugWarn("Received message from non-host peer of type ", data.Type) + return false + end + + -- TODO: verify integrity of the message + + return true + end, + + ---@param lobby UIAutolobbyCommunications + ---@param data UILobbyReceivedMessage + Handler = function(lobby, data) + lobby.GameOptions = data.GameOptions + + -- update UI for game options + import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() + :UpdateGameOptions(lobby.GameOptions) + end + } +} From 5fb02fcf3431fc6eaad0c2cfe40be5df21af180e Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Thu, 17 Oct 2024 21:54:21 +0200 Subject: [PATCH 04/83] First working setup that actually launches into the game --- lua/ui/lobby/autolobby.lua | 71 ++++++++++++++++--- lua/ui/lobby/autolobby/AutolobbyInterface.lua | 1 - .../autolobby/AutolobbyMessageHandlers.lua | 24 +++++++ scripts/LaunchFAInstances.ps1 | 2 +- 4 files changed, 86 insertions(+), 12 deletions(-) diff --git a/lua/ui/lobby/autolobby.lua b/lua/ui/lobby/autolobby.lua index 301cbcaea4..1de2eee4c5 100644 --- a/lua/ui/lobby/autolobby.lua +++ b/lua/ui/lobby/autolobby.lua @@ -85,6 +85,7 @@ local AutolobbyCommunicationsInstance = false ---@field LocalConnectedTo table # list of other player identifiers that we're connected to ---@field OthersConnectedTo table> # list of list ofother player identifiers that other players are connected to ---@field HostID UILobbyPlayerId +---@field PlayerCount number ---@field GameOptions UILobbyLaunchGameOptionsConfiguration # Is synced from the host to the others. ---@field PlayerOptions UIAutolobbyPlayer[] # Is synced from the host to the others. AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { @@ -104,7 +105,8 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { self.LocalID = "-1" self.LocalPlayerName = "Charlie" - self.ConnectedTo = {} + self.PlayerCount = tonumber(GetCommandLineArg("/players", 1)[1]) or 2 + self.LocalConnectedTo = {} self.OthersConnectedTo = {} self.HostID = "-1" @@ -207,6 +209,36 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { end end, + ---@param self UIAutolobbyCommunications + CheckForLaunchThread = function(self) + while not IsDestroyed(self) do + + -- true iff we are connected to all peers + local allLocalConnected = table.getsize(self:GetPeers()) == self.PlayerCount - 1 + + -- true iff all peers are connected to every of their peers + local allPeersConnected = true + for peerId, otherConnectedTo in self.OthersConnectedTo do + allPeersConnected = allPeersConnected and table.getsize(otherConnectedTo) == self.PlayerCount - 1 + end + + if allLocalConnected and allPeersConnected then + ---@type UILobbyLaunchConfiguration + local gameConfiguration = { + GameMods = {}, + GameOptions = self.GameOptions, + PlayerOptions = self.PlayerOptions, + Observers = {}, + } + + self:BroadcastData({ Type = "Launch", GameConfig = gameConfiguration }) + self:LaunchGame(gameConfiguration) + end + + WaitSeconds(1.0) + end + end, + --------------------------------------------------------------------------- --#region Engine interface @@ -307,8 +339,12 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { return MohoLobbyMethods.JoinGame(self, address, remotePlayerName, remotePlayerUID) end, + ---@param self UIAutolobbyCommunications + ---@param gameConfig UILobbyLaunchConfiguration + ---@return nil LaunchGame = function(self, gameConfig) - self:DebugSpew("LaunchGame", gameConfig) + self:DebugSpew("LaunchGame") + self:DebugSpew(reprs(gameConfig, { depth = 10 })) return MohoLobbyMethods.LaunchGame(self, gameConfig) end, @@ -350,8 +386,18 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { self.LocalPlayerName = self:GetLocalPlayerName() self.HostID = self:GetLocalPlayerID() + -- give ourself a seat at the table + local hostPlayerOptions = self:CreateLocalPlayer() + hostPlayerOptions.OwnerID = self.LocalID + hostPlayerOptions.PlayerName = self:MakeValidPlayerName(self.LocalID, self.LocalPlayerName) + self.PlayerOptions[hostPlayerOptions.StartSpot] = hostPlayerOptions + -- occasionally send data over the network to create pings on screen self.Trash:Add(ForkThread(self.IsAliveThread, self)) + self.Trash:Add(ForkThread(self.CheckForLaunchThread, self)) + + -- start prefetching the scenario + PrefetchSession(self.GameOptions.ScenarioFile, {}, true) -- update UI for game options import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() @@ -392,6 +438,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { ---@param playerConnectedTo string[] # all established conenctions for the given player EstablishedPeers = function(self, playerId, playerConnectedTo) self:DebugSpew("EstablishedPeers", playerId, reprs(playerConnectedTo)) + self.OthersConnectedTo[playerId] = playerConnectedTo end, --#endregion @@ -458,13 +505,16 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { ---@param self UIAutolobbyCommunications GameLaunched = function(self) self:DebugSpew("GameLaunched") + + -- GpgNetSend('GameState', 'Launching') + self:Destroy() end, --- Called by the engine when the launch failed. ---@param self UIAutolobbyCommunications ---@param reasonKey string LaunchFailed = function(self, reasonKey) - self:DebugSpew("LaunchFailed", reasonKey) + self:DebugSpew("LaunchFailed", LOC(reasonKey)) end, --#endregion @@ -532,7 +582,8 @@ function CreateLobby(protocol, localPort, desiredPlayerName, localPlayerUID, nat ) -- create the singleton for the interface - import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() + local interface = import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() + AutolobbyCommunicationsInstance.Trash:Add(interface) end --- Instantiates a lobby instance by hosting one. @@ -551,9 +602,9 @@ function HostGame(gameName, scenarioFileName, singlePlayer) AutolobbyCommunicationsInstance:HostGame() end - -- start with a loading dialog - import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() - :CreateLoadingDialog() + -- -- start with a loading dialog + -- import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() + -- :CreateLoadingDialog() end --- Joins an instantiated lobby instance. @@ -570,9 +621,9 @@ function JoinGame(address, asObserver, playerName, uid) AutolobbyCommunicationsInstance:JoinGame(address, playerName, uid) end - -- start with a loading dialog - import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() - :CreateLoadingDialog() + -- -- start with a loading dialog + -- import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() + -- :CreateLoadingDialog() end --- Called by the engine. diff --git a/lua/ui/lobby/autolobby/AutolobbyInterface.lua b/lua/ui/lobby/autolobby/AutolobbyInterface.lua index 5273990c32..1894940918 100644 --- a/lua/ui/lobby/autolobby/AutolobbyInterface.lua +++ b/lua/ui/lobby/autolobby/AutolobbyInterface.lua @@ -119,7 +119,6 @@ local AutolobbyInterface = Class(Group) { end, --#endregion - } --- A trashbag that should be destroyed upon reload. diff --git a/lua/ui/lobby/autolobby/AutolobbyMessageHandlers.lua b/lua/ui/lobby/autolobby/AutolobbyMessageHandlers.lua index 3cb7238a83..652b36ff38 100644 --- a/lua/ui/lobby/autolobby/AutolobbyMessageHandlers.lua +++ b/lua/ui/lobby/autolobby/AutolobbyMessageHandlers.lua @@ -154,9 +154,33 @@ AutolobbyMessageHandlers = { Handler = function(lobby, data) lobby.GameOptions = data.GameOptions + PrefetchSession(lobby.GameOptions.ScenarioFile, {}, true) + -- update UI for game options import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() :UpdateGameOptions(lobby.GameOptions) end + }, + + Launch = { + ---@param lobby UIAutolobbyCommunications + ---@param data UILobbyReceivedMessage + ---@return boolean + Accept = function(lobby, data) + if not IsFromHost(lobby, data) then + lobby:DebugWarn("Received message from non-host peer of type ", data.Type) + return false + end + + -- TODO: verify integrity of the message + + return true + end, + + ---@param lobby UIAutolobbyCommunications + ---@param data UILobbyReceivedMessage + Handler = function(lobby, data) + lobby:LaunchGame(data.GameConfig) + end } } diff --git a/scripts/LaunchFAInstances.ps1 b/scripts/LaunchFAInstances.ps1 index 8a22fca4a6..648fc8f111 100644 --- a/scripts/LaunchFAInstances.ps1 +++ b/scripts/LaunchFAInstances.ps1 @@ -1,5 +1,5 @@ param ( - [int]$players = 2, # Default to 2 instances (1 host, 1 client) + [int]$players = 4, # Default to 2 instances (1 host, 1 client) [string]$map = "/maps/scmp_009/SCMP_009_scenario.lua", # Default map: Seton's Clutch [int]$port = 15000, # Default port for hosting the game [int]$teams = 2 # Default to two teams, 0 for FFA From beedc76ddae8fc1cd02a1109c9cd7d0b9bbf2985 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Thu, 17 Oct 2024 22:01:13 +0200 Subject: [PATCH 05/83] Separate file for the controller --- lua/ui/lobby/autolobby.lua | 530 +---------------- .../lobby/autolobby/AutolobbyController.lua | 550 ++++++++++++++++++ 2 files changed, 551 insertions(+), 529 deletions(-) create mode 100644 lua/ui/lobby/autolobby/AutolobbyController.lua diff --git a/lua/ui/lobby/autolobby.lua b/lua/ui/lobby/autolobby.lua index 1de2eee4c5..5f59038174 100644 --- a/lua/ui/lobby/autolobby.lua +++ b/lua/ui/lobby/autolobby.lua @@ -34,537 +34,9 @@ --** SOFTWARE. --****************************************************************************************************** -local Utils = import("/lua/system/utils.lua") - -local MohoLobbyMethods = moho.lobby_methods -local DebugComponent = import("/lua/shared/components/DebugComponent.lua").DebugComponent - -local AutolobbyMessageHandlers = import("/lua/ui/lobby/autolobby/AutolobbyMessageHandlers.lua").AutolobbyMessageHandlers - -local AutolobbyEngineStrings = { - -- General info strings - ['Connecting'] = "Connecting to Game", - ['AbortConnect'] = "Abort Connect", - ['TryingToConnect'] = "Connecting...", - ['TimedOut'] = "%s timed out.", - ['TimedOutToHost'] = "Timed out to host.", - ['Ejected'] = "You have been ejected: %s", - ['ConnectionFailed'] = "Connection failed: %s", - ['LaunchFailed'] = "Launch failed: %s", - ['LobbyFull'] = "The game lobby is full.", - - -- Error reasons - ['StartSpots'] = "The map does not support this number of players.", - ['NoConfig'] = "No valid game configurations found.", - ['NoObservers'] = "Observers not allowed.", - ['KickedByHost'] = "Kicked by host.", - ['GameLaunched'] = "Game was launched.", - ['NoLaunchLimbo'] = "No clients allowed in limbo at launch", - ['HostLeft'] = "Host abandoned lobby", - ['LaunchRejected'] = "Some players are using an incompatible client version.", -} - ----@class UIAutolobbyPlayer: UILobbyLaunchPlayerConfiguration ----@field StartSpot number ----@field DEV number # Related to rating/divisions ----@field MEAN number # Related to rating/divisions ----@field NG number # Related to rating/divisions ----@field DIV string # Related to rating/divisions ----@field SUBDIV string # Related to rating/divisions ----@field PL number # Related to rating/divisions - ---@type UIAutolobbyCommunications | false local AutolobbyCommunicationsInstance = false ---- Responsible for the behavior of the automated lobby. ----@class UIAutolobbyCommunications : moho.lobby_methods, DebugComponent ----@field Trash TrashBag ----@field InterfaceTrash TrashBag ----@field LocalID UILobbyPlayerId # a number that is stringified ----@field LocalPlayerName string # nickname ----@field LocalConnectedTo table # list of other player identifiers that we're connected to ----@field OthersConnectedTo table> # list of list ofother player identifiers that other players are connected to ----@field HostID UILobbyPlayerId ----@field PlayerCount number ----@field GameOptions UILobbyLaunchGameOptionsConfiguration # Is synced from the host to the others. ----@field PlayerOptions UIAutolobbyPlayer[] # Is synced from the host to the others. -AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { - - BackgroundTextures = { - "/menus02/background-paint01_bmp.dds", - "/menus02/background-paint02_bmp.dds", - "/menus02/background-paint03_bmp.dds", - "/menus02/background-paint04_bmp.dds", - "/menus02/background-paint05_bmp.dds", - }, - - ---@param self UIAutolobbyCommunications - __init = function(self) - self.Trash = TrashBag() - self.InterfaceTrash = self.Trash:Add(TrashBag()) - - self.LocalID = "-1" - self.LocalPlayerName = "Charlie" - self.PlayerCount = tonumber(GetCommandLineArg("/players", 1)[1]) or 2 - self.LocalConnectedTo = {} - self.OthersConnectedTo = {} - self.HostID = "-1" - - self.GameOptions = self:CreateLocalGameOptions() - self.PlayerOptions = {} - end, - - ---@param self UIAutolobbyCommunications - __init_post = function(self) - - end, - - --- Creates a table that represents the local player settings. This represents the initial player. It can be edited by the host accordingly. - ---@param self UIAutolobbyCommunications - ---@return UIAutolobbyPlayer - CreateLocalPlayer = function(self) - ---@type UIAutolobbyPlayer - local info = {} - - info.Team = 1 - info.PlayerColor = 1 - info.ArmyColor = 1 - info.Human = true - info.Civilian = false - - -- determine player name - info.PlayerName = self.LocalPlayerName or self:GetLocalPlayerName() or "player" - - -- retrieve faction - info.Faction = 1 - local factionData = import("/lua/factions.lua") - for index, tbl in factionData.Factions do - if HasCommandLineArg("/" .. tbl.Key) then - info.Faction = index - break - end - end - - -- retrieve team and start spot - info.Team = tonumber(GetCommandLineArg("/team", 1)[1]) - info.StartSpot = tonumber(GetCommandLineArg("/startspot", 1)[1]) or false - - -- retrieve rating - info.DEV = tonumber(GetCommandLineArg("/deviation", 1)[1]) or 500 - info.MEAN = tonumber(GetCommandLineArg("/mean", 1)[1]) or 1500 - info.NG = tonumber(GetCommandLineArg("/numgames", 1)[1]) or 0 - info.DIV = (GetCommandLineArg("/division", 1)[1]) or "" - info.SUBDIV = (GetCommandLineArg("/subdivision", 1)[1]) or "" - info.PL = math.floor(info.MEAN - 3 * info.DEV) - - return info - end, - - --- Creates a table that represents the local game options. - ---@param self UIAutolobbyCommunications - ---@return UILobbyLaunchGameOptionsConfiguration - CreateLocalGameOptions = function(self) - ---@type UILobbyLaunchGameOptionsConfiguration - local options = { - Score = 'no', - TeamSpawn = 'fixed', - TeamLock = 'locked', - Victory = 'demoralization', - Timeouts = '3', - CheatsEnabled = 'false', - CivilianAlliance = 'enemy', - RevealCivilians = 'Yes', - GameSpeed = 'normal', - FogOfWar = 'explored', - UnitCap = '1500', - PrebuiltUnits = 'Off', - Share = 'FullShare', - ShareUnitCap = 'allies', - DisconnectionDelay02 = '90', - - -- yep, great - Ranked = true, - Unranked = 'No', - } - - -- process game options from the command line - for name, value in Utils.GetCommandLineArgTable("/gameoptions") do - if name and value then - options[name] = value - else - LOG("Malformed gameoption. ignoring name: " .. repr(name) .. " and value: " .. repr(value)) - end - end - - return options - end, - - --- A thread to indicate that we're still around. Various properties such as ping are not updated - --- until a message is received. This thread introduces occasional traffic between players. - ---@param self UIAutolobbyCommunications - IsAliveThread = function(self) - while not IsDestroyed(self) do - self:BroadcastData({ Type = "IsAlive" }) - WaitSeconds(1.0) - end - end, - - ---@param self UIAutolobbyCommunications - CheckForLaunchThread = function(self) - while not IsDestroyed(self) do - - -- true iff we are connected to all peers - local allLocalConnected = table.getsize(self:GetPeers()) == self.PlayerCount - 1 - - -- true iff all peers are connected to every of their peers - local allPeersConnected = true - for peerId, otherConnectedTo in self.OthersConnectedTo do - allPeersConnected = allPeersConnected and table.getsize(otherConnectedTo) == self.PlayerCount - 1 - end - - if allLocalConnected and allPeersConnected then - ---@type UILobbyLaunchConfiguration - local gameConfiguration = { - GameMods = {}, - GameOptions = self.GameOptions, - PlayerOptions = self.PlayerOptions, - Observers = {}, - } - - self:BroadcastData({ Type = "Launch", GameConfig = gameConfiguration }) - self:LaunchGame(gameConfiguration) - end - - WaitSeconds(1.0) - end - end, - - --------------------------------------------------------------------------- - --#region Engine interface - - --- Broadcasts data to all (connected) peers. - ---@param self UIAutolobbyCommunications - ---@param data UILobbyData - BroadcastData = function(self, data) - self:DebugSpew("BroadcastData", data.Type) - if not AutolobbyMessageHandlers[data.Type] then - self:DebugWarn("Broadcasting unknown message type", data.Type) - end - - return MohoLobbyMethods.BroadcastData(self, data) - end, - - --- (Re)Connects to a peer. - ---@param self any - ---@param address any - ---@param name any - ---@param uid any - ---@return nil - ConnectToPeer = function(self, address, name, uid) - self:DebugSpew("ConnectToPeer", address, name, uid) - return MohoLobbyMethods.ConnectToPeer(self, address, name, uid) - end, - - --- ??? - ---@param self UIAutolobbyCommunications - ---@return nil - DebugDump = function(self) - self:DebugSpew("DebugDump") - return MohoLobbyMethods.DebugDump(self) - end, - - --- Destroys the C-object and all the (UI) entities in the trash bag. - ---@param self UIAutolobbyCommunications - ---@return nil - Destroy = function(self) - self:DebugSpew("Destroy") - - self.Trash:Destroy() - return MohoLobbyMethods.Destroy(self) - end, - - --- Disconnects from a peer. - --- See also `ConnectToPeer` to connect - ---@param self UIAutolobbyCommunications - ---@param uid any - ---@return nil - DisconnectFromPeer = function(self, uid) - self:DebugSpew("DisconnectFromPeer", uid) - return MohoLobbyMethods.DisconnectFromPeer(self, uid) - end, - - - EjectPeer = function(self, uid, reason) - self:DebugSpew("EjectPeer", uid, reason) - return MohoLobbyMethods.EjectPeer(self, uid, reason) - end, - - GetLocalPlayerID = function(self) - self:DebugSpew("GetLocalPlayerID") - return MohoLobbyMethods.GetLocalPlayerID(self) - end, - - GetLocalPlayerName = function(self) - self:DebugSpew("GetLocalPlayerName") - return MohoLobbyMethods.GetLocalPlayerName(self) - end, - - GetLocalPort = function(self) - self:DebugSpew("GetLocalPort") - return MohoLobbyMethods.GetLocalPort(self) - end, - - GetPeer = function(self, uid) - self:DebugSpew("GetPeer", uid) - return MohoLobbyMethods.GetPeer(self, uid) - end, - - GetPeers = function(self) - self:DebugSpew("GetPeers") - return MohoLobbyMethods.GetPeers(self) - end, - - HostGame = function(self) - self:DebugSpew("HostGame") - return MohoLobbyMethods.HostGame(self) - end, - - IsHost = function(self) - self:DebugSpew("IsHost") - return MohoLobbyMethods.IsHost(self) - end, - - JoinGame = function(self, address, remotePlayerName, remotePlayerUID) - self:DebugSpew("JoinGame", address, remotePlayerName, remotePlayerUID) - return MohoLobbyMethods.JoinGame(self, address, remotePlayerName, remotePlayerUID) - end, - - ---@param self UIAutolobbyCommunications - ---@param gameConfig UILobbyLaunchConfiguration - ---@return nil - LaunchGame = function(self, gameConfig) - self:DebugSpew("LaunchGame") - self:DebugSpew(reprs(gameConfig, { depth = 10 })) - return MohoLobbyMethods.LaunchGame(self, gameConfig) - end, - - MakeValidGameName = function(self, name) - - self:DebugSpew("MakeValidGameName", name) - return MohoLobbyMethods.MakeValidGameName(self, name) - end, - - MakeValidPlayerName = function(self, uid, name) - self:DebugSpew("MakeValidPlayerName", uid, name) - return MohoLobbyMethods.MakeValidPlayerName(self, uid, name) - end, - - ---@param self UIAutolobbyCommunications - ---@param uid UILobbyPlayerId - ---@param data UILobbyData - ---@return nil - SendData = function(self, uid, data) - self:DebugSpew("SendData", uid, data.Type) - if not AutolobbyMessageHandlers[data.Type] then - self:DebugWarn("Sending unknown message type", data.Type, "to", uid) - end - - return MohoLobbyMethods.SendData(self, uid, data) - end, - - --#endregion - - --------------------------------------------------------------------------- - --#region Connection events - - --- Called by the engine as we're trying to host a lobby. - ---@param self UIAutolobbyCommunications - Hosting = function(self) - self:DebugSpew("Hosting") - - self.LocalID = self:GetLocalPlayerID() - self.LocalPlayerName = self:GetLocalPlayerName() - self.HostID = self:GetLocalPlayerID() - - -- give ourself a seat at the table - local hostPlayerOptions = self:CreateLocalPlayer() - hostPlayerOptions.OwnerID = self.LocalID - hostPlayerOptions.PlayerName = self:MakeValidPlayerName(self.LocalID, self.LocalPlayerName) - self.PlayerOptions[hostPlayerOptions.StartSpot] = hostPlayerOptions - - -- occasionally send data over the network to create pings on screen - self.Trash:Add(ForkThread(self.IsAliveThread, self)) - self.Trash:Add(ForkThread(self.CheckForLaunchThread, self)) - - -- start prefetching the scenario - PrefetchSession(self.GameOptions.ScenarioFile, {}, true) - - -- update UI for game options - import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() - :UpdateGameOptions(self.GameOptions) - end, - - --- Called by the engine as we're trying to join a lobby. - ---@param self UIAutolobbyCommunications - Connecting = function(self) - self:DebugSpew("Connecting") - end, - - --- Called by the engine when the connection fails. - ---@param self UIAutolobbyCommunications - ---@param reason string # reason for connection failure, populated by the engine - ConnectionFailed = function(self, reason) - self:DebugSpew("ConnectionFailed", reason) - end, - - --- Called by the engine when the connection succeeds with the host. - ---@param self UIAutolobbyCommunications - ---@param localId string - ---@param hostId string - ConnectionToHostEstablished = function(self, localId, newLocalName, hostId) - self:DebugSpew("ConnectionToHostEstablished", localId, newLocalName, hostId) - self.LocalPlayerName = newLocalName - self.LocalID = localId - self.HostID = hostId - - -- occasionally send data over the network to create pings on screen - self.Trash:Add(ForkThread(self.IsAliveThread, self)) - self:SendData(self.HostID, { Type = "AddPlayer", PlayerOptions = self:CreateLocalPlayer() }) - end, - - --- Called by the engine when a peer establishes a connection. - ---@param self UIAutolobbyCommunications - ---@param playerId string - ---@param playerConnectedTo string[] # all established conenctions for the given player - EstablishedPeers = function(self, playerId, playerConnectedTo) - self:DebugSpew("EstablishedPeers", playerId, reprs(playerConnectedTo)) - self.OthersConnectedTo[playerId] = playerConnectedTo - end, - - --#endregion - - --------------------------------------------------------------------------- - --#region Lobby events - - --- Called by the engine when you are ejected from a lobby. - ---@param self UIAutolobbyCommunications - ---@param reason string # reason for disconnection, populated by the host - Ejected = function(self, reason) - self:DebugSpew("Ejected", reason) - end, - - --- ??? - ---@param self UIAutolobbyCommunications - ---@param text string - SystemMessage = function(self, text) - self:DebugSpew("SystemMessage", text) - end, - - --- Called by the engine when we receive data from other players. There is no checking to see if the data is legitimate, these need to be done in Lua. - --- - --- Data can be send via `BroadcastData` and/or `SendData`. - ---@param self UIAutolobbyCommunications - ---@param data UILobbyReceivedMessage - DataReceived = function(self, data) - self:DebugSpew("DataReceived", data.Type, data.SenderID, data.SenderName) - - ---@type UIAutolobbyMessageHandler? - local messageType = AutolobbyMessageHandlers[data.Type] - - -- verify that the message type exists - if not messageType then - self:DebugError('Unknown message received: ', data.Type) - return - end - - -- verify that we can accept it - if not messageType.Accept(self, data) then - self:DebugWarn("Message rejected: ", data.Type) - return - end - - -- handle the message - messageType.Handler(self, data) - end, - - --- Called by the engine when the game configuration is requested by the discovery service. - ---@param self UIAutolobbyCommunications - GameConfigRequested = function(self) - self:DebugSpew("GameConfigRequested") - end, - - --- Called by the engine when a peer disconnects. - ---@param self UIAutolobbyCommunications - ---@param peerName string - ---@param otherId string - PeerDisconnected = function(self, peerName, otherId) - self:DebugSpew("PeerDisconnected", peerName, otherId) - end, - - --- Called by the engine when the game is launched. - ---@param self UIAutolobbyCommunications - GameLaunched = function(self) - self:DebugSpew("GameLaunched") - - -- GpgNetSend('GameState', 'Launching') - self:Destroy() - end, - - --- Called by the engine when the launch failed. - ---@param self UIAutolobbyCommunications - ---@param reasonKey string - LaunchFailed = function(self, reasonKey) - self:DebugSpew("LaunchFailed", LOC(reasonKey)) - end, - - --#endregion - - --#region Debugging - - ---@param self UIAutolobbyCommunications - ---@param ... any - DebugSpew = function(self, ...) - if not self.EnabledSpewing then - return - end - - SPEW("Autolobby communications", unpack(arg)) - end, - - - ---@param self UIAutolobbyCommunications - ---@param ... any - DebugLog = function(self, ...) - if not self.EnabledLogging then - return - end - - LOG("Autolobby communications", unpack(arg)) - end, - - ---@param self UIAutolobbyCommunications - ---@param ... any - DebugWarn = function(self, ...) - if not self.EnabledWarnings then - return - end - - WARN("Autolobby communications", unpack(arg)) - end, - - ---@param self UIAutolobbyCommunications - ---@param ... any - DebugError = function(self, ...) - if not self.EnabledErrors then - return - end - - error("Autolobby communications", unpack(arg)) - end, - - --#endregion -} - --- Creates the lobby communications, called (indirectly) by the engine. ---@param protocol any ---@param localPort any @@ -576,7 +48,7 @@ function CreateLobby(protocol, localPort, desiredPlayerName, localPlayerUID, nat local maxConnections = 16 AutolobbyCommunicationsInstance = InternalCreateLobby( - AutolobbyCommunications, + import("/lua/ui/lobby/autolobby/AutolobbyController.lua").AutolobbyCommunications, protocol, localPort, maxConnections, desiredPlayerName, localPlayerUID, natTraversalProvider ) diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua new file mode 100644 index 0000000000..354f5c16d6 --- /dev/null +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -0,0 +1,550 @@ +--****************************************************************************************************** +--** Copyright (c) 2024 Willem 'Jip' Wijnia +--** +--** Permission is hereby granted, free of charge, to any person obtaining a copy +--** of this software and associated documentation files (the "Software"), to deal +--** in the Software without restriction, including without limitation the rights +--** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +--** copies of the Software, and to permit persons to whom the Software is +--** furnished to do so, subject to the following conditions: +--** +--** The above copyright notice and this permission notice shall be included in all +--** copies or substantial portions of the Software. +--** +--** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +--** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +--** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +--** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +--** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +--** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +--** SOFTWARE. +--****************************************************************************************************** + + +local Utils = import("/lua/system/utils.lua") + +local MohoLobbyMethods = moho.lobby_methods +local DebugComponent = import("/lua/shared/components/DebugComponent.lua").DebugComponent + +local AutolobbyMessageHandlers = import("/lua/ui/lobby/autolobby/AutolobbyMessageHandlers.lua").AutolobbyMessageHandlers + +local AutolobbyEngineStrings = { + -- General info strings + ['Connecting'] = "Connecting to Game", + ['AbortConnect'] = "Abort Connect", + ['TryingToConnect'] = "Connecting...", + ['TimedOut'] = "%s timed out.", + ['TimedOutToHost'] = "Timed out to host.", + ['Ejected'] = "You have been ejected: %s", + ['ConnectionFailed'] = "Connection failed: %s", + ['LaunchFailed'] = "Launch failed: %s", + ['LobbyFull'] = "The game lobby is full.", + + -- Error reasons + ['StartSpots'] = "The map does not support this number of players.", + ['NoConfig'] = "No valid game configurations found.", + ['NoObservers'] = "Observers not allowed.", + ['KickedByHost'] = "Kicked by host.", + ['GameLaunched'] = "Game was launched.", + ['NoLaunchLimbo'] = "No clients allowed in limbo at launch", + ['HostLeft'] = "Host abandoned lobby", + ['LaunchRejected'] = "Some players are using an incompatible client version.", +} + +---@class UIAutolobbyPlayer: UILobbyLaunchPlayerConfiguration +---@field StartSpot number +---@field DEV number # Related to rating/divisions +---@field MEAN number # Related to rating/divisions +---@field NG number # Related to rating/divisions +---@field DIV string # Related to rating/divisions +---@field SUBDIV string # Related to rating/divisions +---@field PL number # Related to rating/divisions + +--- Responsible for the behavior of the automated lobby. +---@class UIAutolobbyCommunications : moho.lobby_methods, DebugComponent +---@field Trash TrashBag +---@field InterfaceTrash TrashBag +---@field LocalID UILobbyPlayerId # a number that is stringified +---@field LocalPlayerName string # nickname +---@field LocalConnectedTo table # list of other player identifiers that we're connected to +---@field OthersConnectedTo table> # list of list ofother player identifiers that other players are connected to +---@field HostID UILobbyPlayerId +---@field PlayerCount number +---@field GameOptions UILobbyLaunchGameOptionsConfiguration # Is synced from the host to the others. +---@field PlayerOptions UIAutolobbyPlayer[] # Is synced from the host to the others. +AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { + + BackgroundTextures = { + "/menus02/background-paint01_bmp.dds", + "/menus02/background-paint02_bmp.dds", + "/menus02/background-paint03_bmp.dds", + "/menus02/background-paint04_bmp.dds", + "/menus02/background-paint05_bmp.dds", + }, + + ---@param self UIAutolobbyCommunications + __init = function(self) + self.Trash = TrashBag() + self.InterfaceTrash = self.Trash:Add(TrashBag()) + + self.LocalID = "-1" + self.LocalPlayerName = "Charlie" + self.PlayerCount = tonumber(GetCommandLineArg("/players", 1)[1]) or 2 + self.LocalConnectedTo = {} + self.OthersConnectedTo = {} + self.HostID = "-1" + + self.GameOptions = self:CreateLocalGameOptions() + self.PlayerOptions = {} + end, + + ---@param self UIAutolobbyCommunications + __init_post = function(self) + + end, + + --- Creates a table that represents the local player settings. This represents the initial player. It can be edited by the host accordingly. + ---@param self UIAutolobbyCommunications + ---@return UIAutolobbyPlayer + CreateLocalPlayer = function(self) + ---@type UIAutolobbyPlayer + local info = {} + + info.Team = 1 + info.PlayerColor = 1 + info.ArmyColor = 1 + info.Human = true + info.Civilian = false + + -- determine player name + info.PlayerName = self.LocalPlayerName or self:GetLocalPlayerName() or "player" + + -- retrieve faction + info.Faction = 1 + local factionData = import("/lua/factions.lua") + for index, tbl in factionData.Factions do + if HasCommandLineArg("/" .. tbl.Key) then + info.Faction = index + break + end + end + + -- retrieve team and start spot + info.Team = tonumber(GetCommandLineArg("/team", 1)[1]) + info.StartSpot = tonumber(GetCommandLineArg("/startspot", 1)[1]) or false + + -- retrieve rating + info.DEV = tonumber(GetCommandLineArg("/deviation", 1)[1]) or 500 + info.MEAN = tonumber(GetCommandLineArg("/mean", 1)[1]) or 1500 + info.NG = tonumber(GetCommandLineArg("/numgames", 1)[1]) or 0 + info.DIV = (GetCommandLineArg("/division", 1)[1]) or "" + info.SUBDIV = (GetCommandLineArg("/subdivision", 1)[1]) or "" + info.PL = math.floor(info.MEAN - 3 * info.DEV) + + return info + end, + + --- Creates a table that represents the local game options. + ---@param self UIAutolobbyCommunications + ---@return UILobbyLaunchGameOptionsConfiguration + CreateLocalGameOptions = function(self) + ---@type UILobbyLaunchGameOptionsConfiguration + local options = { + Score = 'no', + TeamSpawn = 'fixed', + TeamLock = 'locked', + Victory = 'demoralization', + Timeouts = '3', + CheatsEnabled = 'false', + CivilianAlliance = 'enemy', + RevealCivilians = 'Yes', + GameSpeed = 'normal', + FogOfWar = 'explored', + UnitCap = '1500', + PrebuiltUnits = 'Off', + Share = 'FullShare', + ShareUnitCap = 'allies', + DisconnectionDelay02 = '90', + + -- yep, great + Ranked = true, + Unranked = 'No', + } + + -- process game options from the command line + for name, value in Utils.GetCommandLineArgTable("/gameoptions") do + if name and value then + options[name] = value + else + LOG("Malformed gameoption. ignoring name: " .. repr(name) .. " and value: " .. repr(value)) + end + end + + return options + end, + + --- A thread to indicate that we're still around. Various properties such as ping are not updated + --- until a message is received. This thread introduces occasional traffic between players. + ---@param self UIAutolobbyCommunications + IsAliveThread = function(self) + while not IsDestroyed(self) do + self:BroadcastData({ Type = "IsAlive" }) + WaitSeconds(1.0) + end + end, + + ---@param self UIAutolobbyCommunications + CheckForLaunchThread = function(self) + while not IsDestroyed(self) do + + -- true iff we are connected to all peers + local allLocalConnected = table.getsize(self:GetPeers()) == self.PlayerCount - 1 + + -- true iff all peers are connected to every of their peers + local allPeersConnected = true + for peerId, otherConnectedTo in self.OthersConnectedTo do + allPeersConnected = allPeersConnected and table.getsize(otherConnectedTo) == self.PlayerCount - 1 + end + + if allLocalConnected and allPeersConnected then + ---@type UILobbyLaunchConfiguration + local gameConfiguration = { + GameMods = {}, + GameOptions = self.GameOptions, + PlayerOptions = self.PlayerOptions, + Observers = {}, + } + + self:BroadcastData({ Type = "Launch", GameConfig = gameConfiguration }) + self:LaunchGame(gameConfiguration) + end + + WaitSeconds(1.0) + end + end, + + --------------------------------------------------------------------------- + --#region Engine interface + + --- Broadcasts data to all (connected) peers. + ---@param self UIAutolobbyCommunications + ---@param data UILobbyData + BroadcastData = function(self, data) + self:DebugSpew("BroadcastData", data.Type) + if not AutolobbyMessageHandlers[data.Type] then + self:DebugWarn("Broadcasting unknown message type", data.Type) + end + + return MohoLobbyMethods.BroadcastData(self, data) + end, + + --- (Re)Connects to a peer. + ---@param self any + ---@param address any + ---@param name any + ---@param uid any + ---@return nil + ConnectToPeer = function(self, address, name, uid) + self:DebugSpew("ConnectToPeer", address, name, uid) + return MohoLobbyMethods.ConnectToPeer(self, address, name, uid) + end, + + --- ??? + ---@param self UIAutolobbyCommunications + ---@return nil + DebugDump = function(self) + self:DebugSpew("DebugDump") + return MohoLobbyMethods.DebugDump(self) + end, + + --- Destroys the C-object and all the (UI) entities in the trash bag. + ---@param self UIAutolobbyCommunications + ---@return nil + Destroy = function(self) + self:DebugSpew("Destroy") + + self.Trash:Destroy() + return MohoLobbyMethods.Destroy(self) + end, + + --- Disconnects from a peer. + --- See also `ConnectToPeer` to connect + ---@param self UIAutolobbyCommunications + ---@param uid any + ---@return nil + DisconnectFromPeer = function(self, uid) + self:DebugSpew("DisconnectFromPeer", uid) + return MohoLobbyMethods.DisconnectFromPeer(self, uid) + end, + + + EjectPeer = function(self, uid, reason) + self:DebugSpew("EjectPeer", uid, reason) + return MohoLobbyMethods.EjectPeer(self, uid, reason) + end, + + GetLocalPlayerID = function(self) + self:DebugSpew("GetLocalPlayerID") + return MohoLobbyMethods.GetLocalPlayerID(self) + end, + + GetLocalPlayerName = function(self) + self:DebugSpew("GetLocalPlayerName") + return MohoLobbyMethods.GetLocalPlayerName(self) + end, + + GetLocalPort = function(self) + self:DebugSpew("GetLocalPort") + return MohoLobbyMethods.GetLocalPort(self) + end, + + GetPeer = function(self, uid) + self:DebugSpew("GetPeer", uid) + return MohoLobbyMethods.GetPeer(self, uid) + end, + + GetPeers = function(self) + self:DebugSpew("GetPeers") + return MohoLobbyMethods.GetPeers(self) + end, + + HostGame = function(self) + self:DebugSpew("HostGame") + return MohoLobbyMethods.HostGame(self) + end, + + IsHost = function(self) + self:DebugSpew("IsHost") + return MohoLobbyMethods.IsHost(self) + end, + + JoinGame = function(self, address, remotePlayerName, remotePlayerUID) + self:DebugSpew("JoinGame", address, remotePlayerName, remotePlayerUID) + return MohoLobbyMethods.JoinGame(self, address, remotePlayerName, remotePlayerUID) + end, + + ---@param self UIAutolobbyCommunications + ---@param gameConfig UILobbyLaunchConfiguration + ---@return nil + LaunchGame = function(self, gameConfig) + self:DebugSpew("LaunchGame") + self:DebugSpew(reprs(gameConfig, { depth = 10 })) + return MohoLobbyMethods.LaunchGame(self, gameConfig) + end, + + MakeValidGameName = function(self, name) + + self:DebugSpew("MakeValidGameName", name) + return MohoLobbyMethods.MakeValidGameName(self, name) + end, + + MakeValidPlayerName = function(self, uid, name) + self:DebugSpew("MakeValidPlayerName", uid, name) + return MohoLobbyMethods.MakeValidPlayerName(self, uid, name) + end, + + ---@param self UIAutolobbyCommunications + ---@param uid UILobbyPlayerId + ---@param data UILobbyData + ---@return nil + SendData = function(self, uid, data) + self:DebugSpew("SendData", uid, data.Type) + if not AutolobbyMessageHandlers[data.Type] then + self:DebugWarn("Sending unknown message type", data.Type, "to", uid) + end + + return MohoLobbyMethods.SendData(self, uid, data) + end, + + --#endregion + + --------------------------------------------------------------------------- + --#region Connection events + + --- Called by the engine as we're trying to host a lobby. + ---@param self UIAutolobbyCommunications + Hosting = function(self) + self:DebugSpew("Hosting") + + self.LocalID = self:GetLocalPlayerID() + self.LocalPlayerName = self:GetLocalPlayerName() + self.HostID = self:GetLocalPlayerID() + + -- give ourself a seat at the table + local hostPlayerOptions = self:CreateLocalPlayer() + hostPlayerOptions.OwnerID = self.LocalID + hostPlayerOptions.PlayerName = self:MakeValidPlayerName(self.LocalID, self.LocalPlayerName) + self.PlayerOptions[hostPlayerOptions.StartSpot] = hostPlayerOptions + + -- occasionally send data over the network to create pings on screen + self.Trash:Add(ForkThread(self.IsAliveThread, self)) + self.Trash:Add(ForkThread(self.CheckForLaunchThread, self)) + + -- start prefetching the scenario + PrefetchSession(self.GameOptions.ScenarioFile, {}, true) + + -- update UI for game options + import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() + :UpdateGameOptions(self.GameOptions) + end, + + --- Called by the engine as we're trying to join a lobby. + ---@param self UIAutolobbyCommunications + Connecting = function(self) + self:DebugSpew("Connecting") + end, + + --- Called by the engine when the connection fails. + ---@param self UIAutolobbyCommunications + ---@param reason string # reason for connection failure, populated by the engine + ConnectionFailed = function(self, reason) + self:DebugSpew("ConnectionFailed", reason) + end, + + --- Called by the engine when the connection succeeds with the host. + ---@param self UIAutolobbyCommunications + ---@param localId string + ---@param hostId string + ConnectionToHostEstablished = function(self, localId, newLocalName, hostId) + self:DebugSpew("ConnectionToHostEstablished", localId, newLocalName, hostId) + self.LocalPlayerName = newLocalName + self.LocalID = localId + self.HostID = hostId + + -- occasionally send data over the network to create pings on screen + self.Trash:Add(ForkThread(self.IsAliveThread, self)) + self:SendData(self.HostID, { Type = "AddPlayer", PlayerOptions = self:CreateLocalPlayer() }) + end, + + --- Called by the engine when a peer establishes a connection. + ---@param self UIAutolobbyCommunications + ---@param playerId string + ---@param playerConnectedTo string[] # all established conenctions for the given player + EstablishedPeers = function(self, playerId, playerConnectedTo) + self:DebugSpew("EstablishedPeers", playerId, reprs(playerConnectedTo)) + self.OthersConnectedTo[playerId] = playerConnectedTo + end, + + --#endregion + + --------------------------------------------------------------------------- + --#region Lobby events + + --- Called by the engine when you are ejected from a lobby. + ---@param self UIAutolobbyCommunications + ---@param reason string # reason for disconnection, populated by the host + Ejected = function(self, reason) + self:DebugSpew("Ejected", reason) + end, + + --- ??? + ---@param self UIAutolobbyCommunications + ---@param text string + SystemMessage = function(self, text) + self:DebugSpew("SystemMessage", text) + end, + + --- Called by the engine when we receive data from other players. There is no checking to see if the data is legitimate, these need to be done in Lua. + --- + --- Data can be send via `BroadcastData` and/or `SendData`. + ---@param self UIAutolobbyCommunications + ---@param data UILobbyReceivedMessage + DataReceived = function(self, data) + self:DebugSpew("DataReceived", data.Type, data.SenderID, data.SenderName) + + ---@type UIAutolobbyMessageHandler? + local messageType = AutolobbyMessageHandlers[data.Type] + + -- verify that the message type exists + if not messageType then + self:DebugError('Unknown message received: ', data.Type) + return + end + + -- verify that we can accept it + if not messageType.Accept(self, data) then + self:DebugWarn("Message rejected: ", data.Type) + return + end + + -- handle the message + messageType.Handler(self, data) + end, + + --- Called by the engine when the game configuration is requested by the discovery service. + ---@param self UIAutolobbyCommunications + GameConfigRequested = function(self) + self:DebugSpew("GameConfigRequested") + end, + + --- Called by the engine when a peer disconnects. + ---@param self UIAutolobbyCommunications + ---@param peerName string + ---@param otherId string + PeerDisconnected = function(self, peerName, otherId) + self:DebugSpew("PeerDisconnected", peerName, otherId) + end, + + --- Called by the engine when the game is launched. + ---@param self UIAutolobbyCommunications + GameLaunched = function(self) + self:DebugSpew("GameLaunched") + + -- GpgNetSend('GameState', 'Launching') + self:Destroy() + end, + + --- Called by the engine when the launch failed. + ---@param self UIAutolobbyCommunications + ---@param reasonKey string + LaunchFailed = function(self, reasonKey) + self:DebugSpew("LaunchFailed", LOC(reasonKey)) + end, + + --#endregion + + --#region Debugging + + ---@param self UIAutolobbyCommunications + ---@param ... any + DebugSpew = function(self, ...) + if not self.EnabledSpewing then + return + end + + SPEW("Autolobby communications", unpack(arg)) + end, + + + ---@param self UIAutolobbyCommunications + ---@param ... any + DebugLog = function(self, ...) + if not self.EnabledLogging then + return + end + + LOG("Autolobby communications", unpack(arg)) + end, + + ---@param self UIAutolobbyCommunications + ---@param ... any + DebugWarn = function(self, ...) + if not self.EnabledWarnings then + return + end + + WARN("Autolobby communications", unpack(arg)) + end, + + ---@param self UIAutolobbyCommunications + ---@param ... any + DebugError = function(self, ...) + if not self.EnabledErrors then + return + end + + error("Autolobby communications", unpack(arg)) + end, + + --#endregion +} From 6924ecad1ee7d9ace61b2c956b22b7aa4d4c34a5 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Fri, 18 Oct 2024 07:30:12 +0200 Subject: [PATCH 06/83] Add documentation --- lua/ui/lobby/autolobby.lua | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/lua/ui/lobby/autolobby.lua b/lua/ui/lobby/autolobby.lua index 5f59038174..0b580dbb18 100644 --- a/lua/ui/lobby/autolobby.lua +++ b/lua/ui/lobby/autolobby.lua @@ -34,10 +34,14 @@ --** SOFTWARE. --****************************************************************************************************** +-- This module exists because the engine expects this particular file to exist with +-- the provided functionality. It now acts as a wrapper for the autolobby controller +-- that can be found at: lua\ui\lobby\autolobby\AutolobbyController.lua + ---@type UIAutolobbyCommunications | false local AutolobbyCommunicationsInstance = false ---- Creates the lobby communications, called (indirectly) by the engine. +--- Creates the lobby communications, called (indirectly) by the engine to setup the module state. ---@param protocol any ---@param localPort any ---@param desiredPlayerName any @@ -60,7 +64,7 @@ end --- Instantiates a lobby instance by hosting one. --- ---- Assumes that the lobby communications to be initialized by calling `CreateLobby`. +--- Assumes that the lobby communications is initialized by calling `CreateLobby`. ---@param gameName any ---@param scenarioFileName any ---@param singlePlayer any @@ -81,7 +85,7 @@ end --- Joins an instantiated lobby instance. --- ---- Assumes that the lobby communications to be initialized by calling `CreateLobby`. +--- Assumes that the lobby communications is initialized by calling `CreateLobby`. ---@param address any ---@param asObserver any ---@param playerName any @@ -120,3 +124,7 @@ function DisconnectFromPeer(uid, doNotUpdateView) AutolobbyCommunicationsInstance:DisconnectFromPeer(uid) end end + +function SendNatPacket (...) + reprsl(arg, {depth = 5}) +end From 2c662e04e05cc8d31016cccdaa5b88870a6217e9 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Fri, 18 Oct 2024 07:30:48 +0200 Subject: [PATCH 07/83] Remove test function --- lua/ui/lobby/autolobby.lua | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lua/ui/lobby/autolobby.lua b/lua/ui/lobby/autolobby.lua index 0b580dbb18..ddcb6bf1bf 100644 --- a/lua/ui/lobby/autolobby.lua +++ b/lua/ui/lobby/autolobby.lua @@ -34,7 +34,7 @@ --** SOFTWARE. --****************************************************************************************************** --- This module exists because the engine expects this particular file to exist with +-- This module exists because the engine expects this particular file to exist with -- the provided functionality. It now acts as a wrapper for the autolobby controller -- that can be found at: lua\ui\lobby\autolobby\AutolobbyController.lua @@ -124,7 +124,3 @@ function DisconnectFromPeer(uid, doNotUpdateView) AutolobbyCommunicationsInstance:DisconnectFromPeer(uid) end end - -function SendNatPacket (...) - reprsl(arg, {depth = 5}) -end From 417eb09ebd39b509a38b26a447a9eec63f15dd33 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Fri, 18 Oct 2024 07:31:17 +0200 Subject: [PATCH 08/83] Remove old copyright statement --- lua/ui/lobby/autolobby.lua | 7 ------- 1 file changed, 7 deletions(-) diff --git a/lua/ui/lobby/autolobby.lua b/lua/ui/lobby/autolobby.lua index ddcb6bf1bf..a4de6059a3 100644 --- a/lua/ui/lobby/autolobby.lua +++ b/lua/ui/lobby/autolobby.lua @@ -1,11 +1,4 @@ --***************************************************************************** ---* File: lua/modules/ui/lobby/autolobby.lua ---* Author: Sam Demulling ---* Summary: Autolaunching games from GPGNet. This is intentionally designed ---* to have no user options as GPGNet is setting them for the player. ---* ---* Copyright © 2006 Gas Powered Games, Inc. All rights reserved. ---***************************************************************************** --* FAF notes: --* Automatch games are configured by the lobby server by sending parameters --* to the FAF client which then relays that configuration to autolobby.lua From 320f9f419c66cfffbee235777317d351d9736534 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Fri, 18 Oct 2024 07:39:09 +0200 Subject: [PATCH 09/83] Add documentation --- .../lobby/autolobby/AutolobbyController.lua | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index 354f5c16d6..9123019558 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -277,52 +277,84 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { return MohoLobbyMethods.DisconnectFromPeer(self, uid) end, - + --- Ejects a peer from the lobby. + ---@param self UIAutolobbyCommunications + ---@param uid UILobbyPlayerId + ---@param reason string + ---@return nil EjectPeer = function(self, uid, reason) self:DebugSpew("EjectPeer", uid, reason) return MohoLobbyMethods.EjectPeer(self, uid, reason) end, + --- Retrieves the local identifier. + ---@param self UIAutolobbyCommunications + ---@return UILobbyPlayerId GetLocalPlayerID = function(self) self:DebugSpew("GetLocalPlayerID") return MohoLobbyMethods.GetLocalPlayerID(self) end, + --- Retrieves the local name. Note that this name can be overwritten by the host via `MakeValidPlayerName` + ---@param self UIAutolobbyCommunications + ---@return string GetLocalPlayerName = function(self) self:DebugSpew("GetLocalPlayerName") return MohoLobbyMethods.GetLocalPlayerName(self) end, + --- Retrieves the local port. + ---@param self any + ---@return number|nil GetLocalPort = function(self) self:DebugSpew("GetLocalPort") return MohoLobbyMethods.GetLocalPort(self) end, + --- Retrieves information about a peer. See `GetPeers` to get the same information for all connected peers. + ---@param self UIAutolobbyCommunications + ---@param uid UILobbyPlayerId + ---@return Peer GetPeer = function(self, uid) self:DebugSpew("GetPeer", uid) return MohoLobbyMethods.GetPeer(self, uid) end, + --- Retrieves information about all connected peers. See `GetPeer` to get information for a specific peer. + ---@param self UIAutolobbyCommunications GetPeers = function(self) self:DebugSpew("GetPeers") return MohoLobbyMethods.GetPeers(self) end, + --- Transforms the lobby to be discoveryable and joinable for other players. + ---@param self UIAutolobbyCommunications + ---@return nil HostGame = function(self) self:DebugSpew("HostGame") return MohoLobbyMethods.HostGame(self) end, + --- Retrieves whether the local client is the host. + ---@param self any + ---@return boolean IsHost = function(self) self:DebugSpew("IsHost") return MohoLobbyMethods.IsHost(self) end, + --- Join a lobby that is set to be a host. + ---@param self UIAutolobbyCommunications + ---@param address GPGNetAddress + ---@param remotePlayerName string + ---@param remotePlayerUID UILobbyPlayerId + ---@return nil JoinGame = function(self, address, remotePlayerName, remotePlayerUID) self:DebugSpew("JoinGame", address, remotePlayerName, remotePlayerUID) return MohoLobbyMethods.JoinGame(self, address, remotePlayerName, remotePlayerUID) end, + --- Launches the game for the local client. The game configuration that is passed in should originate from the host. ---@param self UIAutolobbyCommunications ---@param gameConfig UILobbyLaunchConfiguration ---@return nil @@ -332,12 +364,21 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { return MohoLobbyMethods.LaunchGame(self, gameConfig) end, + --- Returns a valid game name. + ---@param self UIAutolobbyCommunications + ---@param name string + ---@return string MakeValidGameName = function(self, name) self:DebugSpew("MakeValidGameName", name) return MohoLobbyMethods.MakeValidGameName(self, name) end, + --- Returns a valid player name. + ---@param self UIAutolobbyCommunications + ---@param uid UILobbyPlayerId + ---@param name string + ---@return string MakeValidPlayerName = function(self, uid, name) self:DebugSpew("MakeValidPlayerName", uid, name) return MohoLobbyMethods.MakeValidPlayerName(self, uid, name) From 5473f889a8d21cd7b7b477e45059699afc90c581 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Fri, 18 Oct 2024 10:54:03 +0200 Subject: [PATCH 10/83] Document behavior of GpgNetSend --- engine/User/CLobby.lua | 1 + lua/ui/globals/GpgNetSend.lua | 101 ++++++++++++++++++ .../lobby/autolobby/AutolobbyController.lua | 1 - lua/userInit.lua | 19 +--- 4 files changed, 103 insertions(+), 19 deletions(-) create mode 100644 lua/ui/globals/GpgNetSend.lua diff --git a/engine/User/CLobby.lua b/engine/User/CLobby.lua index 7f757a39b1..777c177da9 100644 --- a/engine/User/CLobby.lua +++ b/engine/User/CLobby.lua @@ -49,6 +49,7 @@ local CLobby = {} ---@field PlayerName string # Read by the engine, TODO ---@class UILobbyLaunchPlayerConfiguration +---@field StartSpot number # Read by Lua code to determine start locations ---@field ArmyName string # Read by the engine, TODO ---@field PlayerName string # Read by the engine, TODO ---@field Civilian boolean # Read by the engine, TODO diff --git a/lua/ui/globals/GpgNetSend.lua b/lua/ui/globals/GpgNetSend.lua new file mode 100644 index 0000000000..901400c658 --- /dev/null +++ b/lua/ui/globals/GpgNetSend.lua @@ -0,0 +1,101 @@ +---@declare-global + +--****************************************************************************************************** +--** Copyright (c) 2024 Willem 'Jip' Wijnia +--** +--** Permission is hereby granted, free of charge, to any person obtaining a copy +--** of this software and associated documentation files (the "Software"), to deal +--** in the Software without restriction, including without limitation the rights +--** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +--** copies of the Software, and to permit persons to whom the Software is +--** furnished to do so, subject to the following conditions: +--** +--** The above copyright notice and this permission notice shall be included in all +--** copies or substantial portions of the Software. +--** +--** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +--** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +--** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +--** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +--** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +--** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +--** SOFTWARE. +--****************************************************************************************************** + +--- Original function that we should not use directly +local oldGpgNetSend = GpgNetSend + +--- Adds a hook that generates sim callbacks for communication to the +--- server. Useful for moderation purposes. +---@param command any +---@param ... unknown +_G.GpgNetSend = function(command, ...) + + if SessionIsActive() and not SessionIsReplay() then + local stringifiedArgs = "" + for k = 1, table.getn(arg) do + stringifiedArgs = stringifiedArgs .. tostring(arg[k]) .. "," + end + + local currentFocusArmy = GetFocusArmy() + SimCallback( + { + Func = "ModeratorEvent", + Args = { + From = currentFocusArmy, + Message = string.format("GpgNetSend with command '%s' and data '%s'", tostring(command), + stringifiedArgs), + }, + } + ) + end + + oldGpgNetSend(command, unpack(arg)) +end + +------------------------------------------------------------------------------- +--#region Game <-> Server communications + +-- All the following logic is tightly coupled with functionality on either the +-- lobby server, the ice adapter, the java server and/or the client. For more +-- context you can search for the various keywords in the following repositories: +-- - Lobby server: https://github.com/FAForever/server +-- - Java server: https://github.com/FAForever/faf-java-server +-- - Java Ice adapter: https://github.com/FAForever/java-ice-adapter +-- - Kotlin Ice adapter: https://github.com/FAForever/kotlin-ice-adapter +-- +-- If we do not send this information then the client is unaware of changes made +-- to the lobby after hosting. These messages are usually only accepted from the +-- host of the lobby. + +--- Sends player options to the lobby server. For more context: +--- - https://github.com/search?q=org%3AFAForever+player_option&type=code +---@param peerId UILobbyPlayerId +---@param key 'Team' | 'Army' | 'StartSpot' | 'Faction' | 'Color' +---@param value string | number +GpgNetSendPlayerOption = function(peerId, key, value) + _G.GpgNetSend('PlayerOption', peerId, key, value) +end + +--- Sends AI options to the lobby server. For more context: +--- - https://github.com/search?q=org%3AFAForever+ai_option&type=code +---@param key string +---@param value string +GpgNetSendAIOption = function(aiName, key, value) + _G.GpgNetSend('AIOption', aiName, key, value) +end + +--- Sends game options to the lobby server. For more context: +--- - https://github.com/search?q=repo%3AFAForever%2Fserver+game_option&type=code +---@param key 'ScenarioFile' | 'Slots' | 'RestrictedCategories' +---@param value string | number +GpgNetSendGameOption = function(key, value) + _G.GpgNetSend('GameOption', key, value) +end + +--- Sends game status to the lobby server. For more context: +--- - https://github.com/search?q=repo%3AFAForever%2Fserver+GameState&type=code +---@param value 'None' | 'Idle' | 'Launching' | 'Ended' +GpgNetSendGameState = function(value) + _G.GpgNetSend('GameState', value) +end diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index 9123019558..4c76c4e9de 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -20,7 +20,6 @@ --** SOFTWARE. --****************************************************************************************************** - local Utils = import("/lua/system/utils.lua") local MohoLobbyMethods = moho.lobby_methods diff --git a/lua/userInit.lua b/lua/userInit.lua index 601548a706..96df2e9c3d 100644 --- a/lua/userInit.lua +++ b/lua/userInit.lua @@ -17,6 +17,7 @@ end -- # Global (and shared) init doscript '/lua/globalInit.lua' +doscript '/lua/ui/globals/GpgNetSend.lua' -- Do we have an custom language set inside user-options ? local selectedlanguage = import("/lua/user/prefs.lua").GetFromCurrentProfile('options').selectedlanguage @@ -290,24 +291,6 @@ do oldSimCallback(callback, addUnitSelection or false) end - - local oldGpgNetSend = GpgNetSend - _G.GpgNetSend = function(command, ...) - - if SessionIsActive() and not SessionIsReplay() then - local stringifiedArgs = "" - for k = 1, table.getn(arg) do - stringifiedArgs = stringifiedArgs .. tostring(arg[k]) .. "," - end - - -- try to inform moderators - ForkThread(SendModeratorEventThread, - string.format("GpgNetSend with command '%s' and data '%s'", tostring(command), - stringifiedArgs)) - end - - oldGpgNetSend(command, unpack(arg)) - end end do From 10a49a229a46b6e4c40dff50a105b45d2456eb2d Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Fri, 18 Oct 2024 16:21:28 +0200 Subject: [PATCH 11/83] Add remaining messages to the server --- lua/ui/globals/GpgNetSend.lua | 9 ++++++++- lua/ui/lobby/autolobby.lua | 19 ++++++++++++------- .../lobby/autolobby/AutolobbyController.lua | 5 +++++ 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/lua/ui/globals/GpgNetSend.lua b/lua/ui/globals/GpgNetSend.lua index 901400c658..0d269a196a 100644 --- a/lua/ui/globals/GpgNetSend.lua +++ b/lua/ui/globals/GpgNetSend.lua @@ -95,7 +95,14 @@ end --- Sends game status to the lobby server. For more context: --- - https://github.com/search?q=repo%3AFAForever%2Fserver+GameState&type=code ----@param value 'None' | 'Idle' | 'Launching' | 'Ended' +---@param value 'None' | 'Idle' | 'Lobby' | 'Launching' | 'Ended' GpgNetSendGameState = function(value) _G.GpgNetSend('GameState', value) end + +--- Sends game status to the lobby server. For more context: +--- - https://github.com/search?q=repo%3AFAForever%2Fserver+GameState&type=code +---@param peerId UILobbyPlayerId +GpgNetSendDisconnected = function(peerId) + GpgNetSend('Disconnected', peerId) +end diff --git a/lua/ui/lobby/autolobby.lua b/lua/ui/lobby/autolobby.lua index a4de6059a3..8f54443e60 100644 --- a/lua/ui/lobby/autolobby.lua +++ b/lua/ui/lobby/autolobby.lua @@ -50,6 +50,8 @@ function CreateLobby(protocol, localPort, desiredPlayerName, localPlayerUID, nat localPlayerUID, natTraversalProvider ) + GpgNetSendGameState('Idle') + -- create the singleton for the interface local interface = import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() AutolobbyCommunicationsInstance.Trash:Add(interface) @@ -79,10 +81,10 @@ end --- Joins an instantiated lobby instance. --- --- Assumes that the lobby communications is initialized by calling `CreateLobby`. ----@param address any ----@param asObserver any ----@param playerName any ----@param uid any +---@param address GPGNetAddress +---@param asObserver boolean +---@param playerName string +---@param uid UILobbyPlayerId function JoinGame(address, asObserver, playerName, uid) LOG("JoinGame", address, asObserver, playerName, uid) @@ -96,9 +98,9 @@ function JoinGame(address, asObserver, playerName, uid) end --- Called by the engine. ----@param addressAndPort any +---@param addressAndPort GPGNetAddress ---@param name any ----@param uid any +---@param uid UILobbyPlayerId function ConnectToPeer(addressAndPort, name, uid) LOG("ConnectToPeer", addressAndPort, name, uid) @@ -108,11 +110,14 @@ function ConnectToPeer(addressAndPort, name, uid) end --- Called by the engine. ----@param uid any +---@param uid UILobbyPlayerId ---@param doNotUpdateView any function DisconnectFromPeer(uid, doNotUpdateView) LOG("DisconnectFromPeer", uid, doNotUpdateView) + -- inform the server of the event + GpgNetSendDisconnected(uid) + if AutolobbyCommunicationsInstance then AutolobbyCommunicationsInstance:DisconnectFromPeer(uid) end diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index 4c76c4e9de..2e5f560ee4 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -360,6 +360,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { LaunchGame = function(self, gameConfig) self:DebugSpew("LaunchGame") self:DebugSpew(reprs(gameConfig, { depth = 10 })) + GpgNetSendGameState('Launching') return MohoLobbyMethods.LaunchGame(self, gameConfig) end, @@ -423,6 +424,8 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { -- start prefetching the scenario PrefetchSession(self.GameOptions.ScenarioFile, {}, true) + GpgNetSendGameState('Lobby') + -- update UI for game options import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() :UpdateGameOptions(self.GameOptions) @@ -451,6 +454,8 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { self.LocalID = localId self.HostID = hostId + GpgNetSendGameState('Lobby') + -- occasionally send data over the network to create pings on screen self.Trash:Add(ForkThread(self.IsAliveThread, self)) self:SendData(self.HostID, { Type = "AddPlayer", PlayerOptions = self:CreateLocalPlayer() }) From 6c3c65ae06081123fbfd7948429c52eea44145d1 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Fri, 18 Oct 2024 16:42:41 +0200 Subject: [PATCH 12/83] Remove the old autolobby classes module --- lua/ui/lobby/autolobby-classes.lua | 125 ----------------------------- 1 file changed, 125 deletions(-) delete mode 100644 lua/ui/lobby/autolobby-classes.lua diff --git a/lua/ui/lobby/autolobby-classes.lua b/lua/ui/lobby/autolobby-classes.lua deleted file mode 100644 index 5e58d07191..0000000000 --- a/lua/ui/lobby/autolobby-classes.lua +++ /dev/null @@ -1,125 +0,0 @@ - -local UIUtil = import("/lua/ui/uiutil.lua") -local LayoutHelpers = import("/lua/maui/layouthelpers.lua") -local Group = import("/lua/maui/group.lua").Group -local Bitmap = import("/lua/maui/bitmap.lua").Bitmap - --- upvalue for performance -local MathMax = math.max -local MathMin = math.min - ---- A small UI component created according to the Model / View / Controller (MVC) principle ----@class ConnectionStatus : Group -ConnectionStatus = ClassUI(Group) { - - -- Initialisation - - __init = function(self, parent) - Group.__init(self, parent) - - -- set our dimensions - LayoutHelpers.SetDimensions(self, 200, 110) - - -- put a border around ourselves - UIUtil.SurroundWithBorder(self, '/scx_menu/lan-game-lobby/frame/') - - -- give ourself a background to become more readable - self.Background = Bitmap(self) - self.Background:SetSolidColor("000000") - self.Background:SetAlpha(0.2) - LayoutHelpers.FillParent(self.Background, self, 0.01) - - -- generic header - self.HeaderText = UIUtil.CreateText( - self, - "", - 16, - UIUtil.titleFont - ) - LayoutHelpers.AtCenterIn(self.HeaderText, self, 0.18) - LayoutHelpers.AtTopIn(self.HeaderText, self, 6) - - -- connection status to other players - self.ConnectionsText = UIUtil.CreateText( - self, - "", - 16, - UIUtil.bodyFont - ) - LayoutHelpers.CenteredBelow(self.ConnectionsText, self.HeaderText, 20) - self.ConnectionsCheckbox = UIUtil.CreateCheckboxStd(self, '/dialogs/check-box_btn/radio') - -- self.ConnectionsCheckbox:Disable() - LayoutHelpers.LeftOf(self.ConnectionsCheckbox, self.ConnectionsText) - LayoutHelpers.AtVerticalCenterIn(self.ConnectionsCheckbox, self.ConnectionsText) - - -- hide for now - self.ConnectionsCheckbox:Hide() - - -- initial view update - self:UpdateView() - end, - - -- Model elements - - -- these start at 1 as we're always connected to ourself - TotalPlayersCount = 1, - IsTotalPlayersCountSet = false, - ConnectedPlayersCount = 1, - - - -- View elements - - --- Updates the view of the model / view / controller of this UI element - UpdateView = function(self) - local headerText = LOC('Connection status') - self.HeaderText:SetText(headerText) - - local connectionsText - if not self.IsTotalPlayersCountSet then - - if self.ConnectedPlayersCount == 1 then - connectionsText = LOCF('%s player is connected', tostring(self.ConnectedPlayersCount)) - else - connectionsText = LOCF('%s players are connected', tostring(self.ConnectedPlayersCount)) - end - else - if self.ConnectedPlayersCount == 1 then - connectionsText = LOCF('%s / %s is connected', tostring(self.ConnectedPlayersCount), tostring(self.TotalPlayersCount)) - else - connectionsText = LOCF('%s / %s are connected', tostring(self.ConnectedPlayersCount), tostring(self.TotalPlayersCount)) - end - end - - self.ConnectionsText:SetText(connectionsText) - self.ConnectionsCheckbox:SetCheck(self.ConnectedPlayersCount == self.TotalPlayersCount) - end, - - -- Controller elements - - --- Updates the internal state and the text - SetTotalPlayersCount = function(self, count) - self.TotalPlayersCount = count - self.IsTotalPlayersCountSet = true - self:UpdateView() - end, - - --- Updates the internal state and the text - SetPlayersConnectedCount = function(self, count) - self.ConnectedPlayersCount = MathMax(MathMin(count, self.TotalPlayersCount), 1) - self:UpdateView() - end, - - AddConnectedPlayer = function(self) - self.ConnectedPlayersCount = self.ConnectedPlayersCount + 1 - if self.IsTotalPlayersCountSet then - self.ConnectedPlayersCount = MathMin(self.ConnectedPlayersCount, self.TotalPlayersCount) - end - - self:UpdateView() - end, - - RemoveConnectedPlayer = function(self) - self.ConnectedPlayersCount = MathMax(self.ConnectedPlayersCount - 1, 1) - self:UpdateView() - end, -} \ No newline at end of file From 24a7aa4298bad47121c7134fa2b80a51fb1ec6fe Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sat, 19 Oct 2024 10:49:18 +0200 Subject: [PATCH 13/83] Add a connection matrix to visualize the status quo --- engine/User/CLobby.lua | 6 +- lua/shared/color.lua | 1 - lua/ui/lobby/autolobby.lua | 18 +++ .../autolobby/AutolobbyConnectionMatrix.lua | 120 ++++++++++++++++ .../AutolobbyConnectionMatrixDot.lua | 103 ++++++++++++++ .../lobby/autolobby/AutolobbyController.lua | 130 +++++++++++++++--- lua/ui/lobby/autolobby/AutolobbyInterface.lua | 62 ++++++++- .../autolobby/AutolobbyMessageHandlers.lua | 4 +- 8 files changed, 418 insertions(+), 26 deletions(-) create mode 100644 lua/ui/lobby/autolobby/AutolobbyConnectionMatrix.lua create mode 100644 lua/ui/lobby/autolobby/AutolobbyConnectionMatrixDot.lua diff --git a/engine/User/CLobby.lua b/engine/User/CLobby.lua index 777c177da9..7235bdd960 100644 --- a/engine/User/CLobby.lua +++ b/engine/User/CLobby.lua @@ -5,18 +5,20 @@ ---@class moho.lobby_methods : Destroyable, InternalObject local CLobby = {} ---- "0", "1", "2", etc. +--- "0", "1", "2", but also "32254" and the like. ---@alias UILobbyPlayerId string ---@alias GPGNetAddress string | number +---@alias UIPeerStatus 'None' | 'Pending' | 'Connecting' | 'Answering' | 'Established' | 'TimedOut' | 'Errored' + ---@class Peer ---@field establishedPeers string[] ---@field id string ---@field ping number ---@field name string ---@field quiet number ----@field status string +---@field status UIPeerStatus --- A piece of data that is one can send with `BroadcastData` or `SendData` to other player(s) in the lobby. ---@class UILobbyReceivedMessage : table diff --git a/lua/shared/color.lua b/lua/shared/color.lua index f892e85231..9af8c30462 100644 --- a/lua/shared/color.lua +++ b/lua/shared/color.lua @@ -581,7 +581,6 @@ end --- Map of named colors the Moho engine can recognize and their representation ---@see EnumColorNames() ----@type table EnumColors = { AliceBlue = "F7FBFF", AntiqueWhite = "FFEBD6", diff --git a/lua/ui/lobby/autolobby.lua b/lua/ui/lobby/autolobby.lua index 8f54443e60..8664769ccf 100644 --- a/lua/ui/lobby/autolobby.lua +++ b/lua/ui/lobby/autolobby.lua @@ -92,6 +92,24 @@ function JoinGame(address, asObserver, playerName, uid) AutolobbyCommunicationsInstance:JoinGame(address, playerName, uid) end + -- ForkThread( + -- function() + + -- local seconds = tonumber(GetCommandLineArg("/startspot", 1)[1]) or 1 + -- WaitSeconds(seconds) + -- if AutolobbyCommunicationsInstance then + -- AutolobbyCommunicationsInstance:JoinGame(address, playerName, uid) + + -- if seconds == 2 then + -- WaitSeconds(seconds) + -- DisconnectFromPeer(AutolobbyCommunicationsInstance:GetPeers()[2].id, false) + -- end + -- end + + + -- end + -- ) + -- -- start with a loading dialog -- import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() -- :CreateLoadingDialog() diff --git a/lua/ui/lobby/autolobby/AutolobbyConnectionMatrix.lua b/lua/ui/lobby/autolobby/AutolobbyConnectionMatrix.lua new file mode 100644 index 0000000000..04a158e6f4 --- /dev/null +++ b/lua/ui/lobby/autolobby/AutolobbyConnectionMatrix.lua @@ -0,0 +1,120 @@ +--****************************************************************************************************** +--** Copyright (c) 2024 Willem 'Jip' Wijnia +--** +--** Permission is hereby granted, free of charge, to any person obtaining a copy +--** of this software and associated documentation files (the "Software"), to deal +--** in the Software without restriction, including without limitation the rights +--** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +--** copies of the Software, and to permit persons to whom the Software is +--** furnished to do so, subject to the following conditions: +--** +--** The above copyright notice and this permission notice shall be included in all +--** copies or substantial portions of the Software. +--** +--** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +--** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +--** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +--** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +--** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +--** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +--** SOFTWARE. +--****************************************************************************************************** + +local UIUtil = import("/lua/ui/uiutil.lua") +local LayoutHelpers = import("/lua/maui/layouthelpers.lua") + +local Group = import("/lua/maui/group.lua").Group + +local AutolobbyConnectionMatrixDot = import("/lua/ui/lobby/autolobby/AutolobbyConnectionMatrixDot.lua") + +---@class UIAutolobbyConnectionMatrix : Group +---@field PlayerCount number +---@field Elements UIAutolobbyConnectionMatrixDot[][] +local AutolobbyConnectionMatrix = Class(Group) { + + ---@param self UIAutolobbyConnectionMatrix + ---@param parent Control + __init = function(self, parent, playerCount) + Group.__init(self, parent, "AutolobbyConnectionMatrix") + + self.PlayerCount = playerCount + + self.Border = UIUtil.SurroundWithBorder(self, '/scx_menu/lan-game-lobby/frame/') + self.Background = UIUtil.CreateBitmapColor(self, '99000000') + + -- create the matrix + self.Elements = {} + for y = 1, self.PlayerCount do + self.Elements[y] = {} + for x = 1, self.PlayerCount do + self.Elements[y][x] = AutolobbyConnectionMatrixDot.Create(self) + end + end + end, + + ---@param self UIAutolobbyConnectionMatrix + ---@param parent Control + __post_init = function(self, parent) + LayoutHelpers.LayoutFor(self) + :Width(self.PlayerCount * 24) + :Height(self.PlayerCount * 24) + :End() + + LayoutHelpers.LayoutFor(self.Background) + :Fill(self) + :End() + + -- layout the matrix + for y = 1, self.PlayerCount do + for x = 1, self.PlayerCount do + LayoutHelpers.LayoutFor(self.Elements[y][x]) + :Width(22) + :Height(22) + :AtLeftTopIn(self, 2 + 24 * (x - 1), 2 + 24 * (y - 1)) + end + end + end, + + ---@param self UIAutolobbyConnectionMatrix + ---@param connectionMatrix UIAutolobbyConnections + UpdateConnections = function(self, connectionMatrix) + for y, connectionRow in connectionMatrix do + for x, isConnected in connectionRow do + ---@type UIAutolobbyConnectionMatrixDot + local dot = self.Elements[y][x] + if dot and y ~= x then + dot:SetConnected(isConnected) + end + end + end + end, + + ---@param self UIAutolobbyConnectionMatrix + ---@param statuses UIAutolobbyStatus + UpdateStatuses = function(self, statuses) + for k, status in statuses do + ---@type UIAutolobbyConnectionMatrixDot + local dot = self.Elements[k][k] + if dot then + dot:SetStatus(status) + end + end + end, + + ---@param self UIAutolobbyConnectionMatrix + ---@param id number + UpdateIsAliveTimestamp = function(self, id) + ---@type UIAutolobbyConnectionMatrixDot + local dot = self.Elements[id][id] + if dot then + dot:SetIsAliveTimestamp(GetSystemTimeSeconds()) + end + end, +} + +---@param parent Control +---@param count number +---@return UIAutolobbyConnectionMatrix +Create = function(parent, count) + return AutolobbyConnectionMatrix(parent, count) +end diff --git a/lua/ui/lobby/autolobby/AutolobbyConnectionMatrixDot.lua b/lua/ui/lobby/autolobby/AutolobbyConnectionMatrixDot.lua new file mode 100644 index 0000000000..ce530f1efe --- /dev/null +++ b/lua/ui/lobby/autolobby/AutolobbyConnectionMatrixDot.lua @@ -0,0 +1,103 @@ +--****************************************************************************************************** +--** Copyright (c) 2024 Willem 'Jip' Wijnia +--** +--** Permission is hereby granted, free of charge, to any person obtaining a copy +--** of this software and associated documentation files (the "Software"), to deal +--** in the Software without restriction, including without limitation the rights +--** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +--** copies of the Software, and to permit persons to whom the Software is +--** furnished to do so, subject to the following conditions: +--** +--** The above copyright notice and this permission notice shall be included in all +--** copies or substantial portions of the Software. +--** +--** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +--** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +--** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +--** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +--** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +--** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +--** SOFTWARE. +--****************************************************************************************************** + +local UIUtil = import("/lua/ui/uiutil.lua") +local LayoutHelpers = import("/lua/maui/layouthelpers.lua") + +local Bitmap = import("/lua/maui/bitmap.lua").Bitmap + +local EnumColors = import("/lua/shared/color.lua").EnumColors + +--- A small dot that represents the connection status between players. +---@class UIAutolobbyConnectionMatrixDot : Bitmap +---@field IsAliveTimestamp number +local AutolobbyConnectionMatrixDot = Class(Bitmap) { + + ---@param self UIAutolobbyConnectionMatrixDot + ---@param parent Control + __init = function(self, parent) + Bitmap.__init(self, parent) + + self.IsAliveTimestamp = 0 + + -- initial state + self:SetConnected(false) + self:SetStatus('None') + end, + + ---@param self UIAutolobbyConnectionMatrixDot + ---@param parent Control + __post_init = function(self, parent) + + end, + + ---@param self UIAutolobbyConnectionMatrixDot + ---@param delta number + OnFrame = function(self, delta) + local time = GetSystemTimeSeconds() + local diff = time - self.IsAliveTimestamp + self:SetAlpha(math.max(0, 1 - (0.25 * diff))) + end, + + ---@param self UIAutolobbyConnectionMatrixDot + ---@param isConnected boolean + SetConnected = function(self, isConnected) + if isConnected then + self:SetAlpha(0.9) + else + self:SetAlpha(0.5) + end + end, + + ---@param self UIAutolobbyConnectionMatrixDot + ---@param status UIPeerStatus + SetStatus = function(self, status) + if status == 'None' then + self:SetSolidColor(EnumColors.AliceBlue) + elseif status == 'Pending' then + self:SetSolidColor(EnumColors.Yellow) + elseif status == 'Connecting' then + self:SetSolidColor(EnumColors.YellowGreen) + elseif status == 'Answering' then + self:SetSolidColor(EnumColors.YellowGreen) + elseif status == 'Established' then + self:SetSolidColor(EnumColors.Green) + elseif status == 'TimedOut' then + self:SetSolidColor(EnumColors.Red) + elseif status == 'Errored' then + self:SetSolidColor(EnumColors.Crimson) + end + end, + + ---@param self UIAutolobbyConnectionMatrixDot + ---@param timestamp number + SetIsAliveTimestamp = function(self, timestamp) + self:SetNeedsFrameUpdate(true) + self.IsAliveTimestamp = timestamp + end +} + +---@param parent Control +---@return UIAutolobbyConnectionMatrixDot +Create = function(parent) + return AutolobbyConnectionMatrixDot(parent) +end diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index 2e5f560ee4..9f71465958 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -59,14 +59,16 @@ local AutolobbyEngineStrings = { ---@field SUBDIV string # Related to rating/divisions ---@field PL number # Related to rating/divisions +---@alias UIAutolobbyConnections boolean[][] +---@alias UIAutolobbyStatus UIPeerStatus[] + + --- Responsible for the behavior of the automated lobby. ---@class UIAutolobbyCommunications : moho.lobby_methods, DebugComponent ---@field Trash TrashBag ---@field InterfaceTrash TrashBag ---@field LocalID UILobbyPlayerId # a number that is stringified ---@field LocalPlayerName string # nickname ----@field LocalConnectedTo table # list of other player identifiers that we're connected to ----@field OthersConnectedTo table> # list of list ofother player identifiers that other players are connected to ---@field HostID UILobbyPlayerId ---@field PlayerCount number ---@field GameOptions UILobbyLaunchGameOptionsConfiguration # Is synced from the host to the others. @@ -89,8 +91,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { self.LocalID = "-1" self.LocalPlayerName = "Charlie" self.PlayerCount = tonumber(GetCommandLineArg("/players", 1)[1]) or 2 - self.LocalConnectedTo = {} - self.OthersConnectedTo = {} + self.Connections = {} self.HostID = "-1" self.GameOptions = self:CreateLocalGameOptions() @@ -98,7 +99,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { end, ---@param self UIAutolobbyCommunications - __init_post = function(self) + __post_init = function(self) end, @@ -182,30 +183,94 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { return options end, + ---@param self UIAutolobbyCommunications + ---@param peers Peer[] + ---@return UIAutolobbyConnections + CreateConnectionsMatrix = function(self, peers) + ---@type UIAutolobbyConnections + local connections = {} + + -- initial setup + for y = 1, self.PlayerCount do + connections[y] = {} + for x = 1, self.PlayerCount do + connections[y][x] = false + end + end + + -- populate the matrix + for _, peer in peers do + local peerIdNumber = tonumber(peer.id) + 1 + for _, peerConnectedToId in peer.establishedPeers do + local peerConnectedToIdNumber = tonumber(peerConnectedToId) + 1 + + -- connection works both ways + connections[peerIdNumber][peerConnectedToIdNumber] = true + connections[peerConnectedToIdNumber][peerIdNumber] = true + end + end + + return connections + end, + + ---@param self UIAutolobbyCommunications + ---@param peers Peer[] + ---@return UIPeerStatus[] + CreateConnectionStatuses = function(self, peers) + local statuses = {} + for k = 1, self.PlayerCount do + statuses[k] = 'None' + end + + for _, peer in peers do + local peerIdNumber = tonumber(peer.id) + 1 + statuses[peerIdNumber] = peer.status + end + + return statuses + end, + --- A thread to indicate that we're still around. Various properties such as ping are not updated --- until a message is received. This thread introduces occasional traffic between players. ---@param self UIAutolobbyCommunications IsAliveThread = function(self) while not IsDestroyed(self) do self:BroadcastData({ Type = "IsAlive" }) - WaitSeconds(1.0) + WaitSeconds(0.5) + end + end, + + ---@param self any + ---@param peers any + ---@return boolean + CheckForLaunch = function(self, peers) + -- true iff we are connected to all peers + local peers = self:GetPeers() + local allLocalConnected = table.getsize(peers) == self.PlayerCount - 1 + + if not allLocalConnected then + return false end + + -- true iff all peers are connected to every of their peers + for _, peer in peers do + if not table.getsize(peer.establishedPeers) == self.PlayerCount - 1 then + return false + end + end + + return true end, ---@param self UIAutolobbyCommunications CheckForLaunchThread = function(self) - while not IsDestroyed(self) do - -- true iff we are connected to all peers - local allLocalConnected = table.getsize(self:GetPeers()) == self.PlayerCount - 1 + while not IsDestroyed(self) do - -- true iff all peers are connected to every of their peers - local allPeersConnected = true - for peerId, otherConnectedTo in self.OthersConnectedTo do - allPeersConnected = allPeersConnected and table.getsize(otherConnectedTo) == self.PlayerCount - 1 - end + local peers = self:GetPeers() + local canLaunch = self:CheckForLaunch(peers) - if allLocalConnected and allPeersConnected then + if canLaunch then ---@type UILobbyLaunchConfiguration local gameConfiguration = { GameMods = {}, @@ -214,11 +279,36 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { Observers = {}, } - self:BroadcastData({ Type = "Launch", GameConfig = gameConfiguration }) - self:LaunchGame(gameConfiguration) + -- delay slightly + WaitSeconds(5) + + -- check again and if still good, we launch + local peers = self:GetPeers() + if self:CheckForLaunch(peers) then + self:BroadcastData({ Type = "Launch", GameConfig = gameConfiguration }) + self:LaunchGame(gameConfiguration) + end end - WaitSeconds(1.0) + WaitSeconds(5.0) + end + end, + + ---@param self UIAutolobbyCommunications + ConnectionMatrixThread = function(self) + while not IsDestroyed(self) do + local peers = self:GetPeers() + + local connections = self:CreateConnectionsMatrix(peers) + local statuses = self:CreateConnectionStatuses(peers) + + import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() + :UpdateConnections(connections) + + import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() + :UpdateConnectionStatuses(statuses) + + WaitFrames(1) end end, @@ -419,6 +509,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { -- occasionally send data over the network to create pings on screen self.Trash:Add(ForkThread(self.IsAliveThread, self)) + self.Trash:Add(ForkThread(self.ConnectionMatrixThread, self)) self.Trash:Add(ForkThread(self.CheckForLaunchThread, self)) -- start prefetching the scenario @@ -458,6 +549,8 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { -- occasionally send data over the network to create pings on screen self.Trash:Add(ForkThread(self.IsAliveThread, self)) + self.Trash:Add(ForkThread(self.ConnectionMatrixThread, self)) + self:SendData(self.HostID, { Type = "AddPlayer", PlayerOptions = self:CreateLocalPlayer() }) end, @@ -467,7 +560,6 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { ---@param playerConnectedTo string[] # all established conenctions for the given player EstablishedPeers = function(self, playerId, playerConnectedTo) self:DebugSpew("EstablishedPeers", playerId, reprs(playerConnectedTo)) - self.OthersConnectedTo[playerId] = playerConnectedTo end, --#endregion diff --git a/lua/ui/lobby/autolobby/AutolobbyInterface.lua b/lua/ui/lobby/autolobby/AutolobbyInterface.lua index 1894940918..347e342966 100644 --- a/lua/ui/lobby/autolobby/AutolobbyInterface.lua +++ b/lua/ui/lobby/autolobby/AutolobbyInterface.lua @@ -33,16 +33,20 @@ local LayoutHelpers = import("/lua/maui/layouthelpers.lua") local Group = import("/lua/maui/group.lua").Group local AutolobbyMapPreview = import("/lua/ui/lobby/autolobby/AutolobbyMapPreview.lua") +local AutolobbyConnectionMatrix = import("/lua/ui/lobby/autolobby/AutolobbyConnectionMatrix.lua") ---@class UIAutolobbyInterfaceState ---@field PlayerOptions? table ---@field GameOptions? UILobbyLaunchGameOptionsConfiguration +---@field Connections? UIAutolobbyConnections +---@field Statuses? UIAutolobbyStatus ---@class UIAutolobbyInterface : Group ---@field State UIAutolobbyInterfaceState ---@field BackgroundTextures string[] ---@field Background Bitmap ---@field Preview UIAutolobbyMapPreview +---@field ConnectionMatrix UIAutolobbyConnectionMatrix local AutolobbyInterface = Class(Group) { BackgroundTextures = { @@ -63,6 +67,7 @@ local AutolobbyInterface = Class(Group) { self.Background = UIUtil.CreateBitmap(self, self.BackgroundTextures[math.random(1, 5)]) self.Preview = AutolobbyMapPreview.GetInstance(self) + self.ConnectionMatrix = AutolobbyConnectionMatrix.Create(self, 4) end, ---@param self UIAutolobbyInterface @@ -77,11 +82,34 @@ local AutolobbyInterface = Class(Group) { :End() LayoutHelpers.LayoutFor(self.Preview) - :AtCenterIn(self) + :AtCenterIn(self, -100, 0) :Width(400) :Height(400) :Hide() :End() + + LayoutHelpers.LayoutFor(self.ConnectionMatrix) + :CenteredBelow(self.Preview, 20) + :Hide() + :End() + end, + + ---@param self UIAutolobbyInterface + ---@param connections UIAutolobbyConnections + UpdateConnections = function(self, connections) + self.State.Connections = connections + + self.ConnectionMatrix:Show() + self.ConnectionMatrix:UpdateConnections(connections) + end, + + ---@param self UIAutolobbyInterface + ---@param statuses UIAutolobbyStatus + UpdateConnectionStatuses = function(self, statuses) + self.State.Statuses = statuses + + self.ConnectionMatrix:Show() + self.ConnectionMatrix:UpdateStatuses(statuses) end, ---@param self UIAutolobbyInterface @@ -104,17 +132,45 @@ local AutolobbyInterface = Class(Group) { end end, + ---@param self UIAutolobbyInterface + ---@param id number + UpdateIsAliveStamp = function(self, id) + self.ConnectionMatrix:UpdateIsAliveTimestamp(id) + end, + --#region Debugging ---@param self UIAutolobbyInterface ---@param state UIAutolobbyInterfaceState RestoreState = function(self, state) + self.State = state + if state.PlayerOptions then - self:UpdatePlayerOptions(state.PlayerOptions) + local ok, msg = pcall(self.UpdatePlayerOptions, self, state.PlayerOptions) + if not ok then + WARN(msg) + end end if state.GameOptions then - self:UpdateGameOptions(state.GameOptions) + local ok, msg = pcall(self.UpdateGameOptions, self, state.GameOptions) + if not ok then + WARN(msg) + end + end + + if state.Connections then + local ok, msg = pcall(self.UpdateConnections, self, state.Connections) + if not ok then + WARN(msg) + end + end + + if state.Statuses then + local ok, msg = pcall(self.UpdateConnectionStatuses, self, state.Statuses) + if not ok then + WARN(msg) + end end end, diff --git a/lua/ui/lobby/autolobby/AutolobbyMessageHandlers.lua b/lua/ui/lobby/autolobby/AutolobbyMessageHandlers.lua index 652b36ff38..f41f521a4d 100644 --- a/lua/ui/lobby/autolobby/AutolobbyMessageHandlers.lua +++ b/lua/ui/lobby/autolobby/AutolobbyMessageHandlers.lua @@ -52,7 +52,9 @@ AutolobbyMessageHandlers = { Handler = function(lobby, data) lobby:DebugSpew("IsAlive handler") - -- TODO: process the alive tick + -- update UI for player options + import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() + :UpdateIsAliveStamp(tonumber(data.SenderID) + 1) end }, From ce10a6b105a19a02d4dd2cc8964e8a2d649448b8 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sat, 19 Oct 2024 11:01:06 +0200 Subject: [PATCH 14/83] Check support for up to 8 players --- lua/ui/lobby/autolobby/AutolobbyController.lua | 15 +++++++++++---- lua/ui/lobby/autolobby/AutolobbyInterface.lua | 2 +- scripts/LaunchFAInstances.ps1 | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index 9f71465958..acde17322c 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -246,13 +246,20 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { CheckForLaunch = function(self, peers) -- true iff we are connected to all peers local peers = self:GetPeers() - local allLocalConnected = table.getsize(peers) == self.PlayerCount - 1 - if not allLocalConnected then + -- check number of peers + if not table.getsize(peers) == self.PlayerCount -1 then return false end - -- true iff all peers are connected to every of their peers + -- check connection status + for k, peer in peers do + if peer.status ~= "Established" then + return false + end + end + + -- check confirmed established connections of peers for _, peer in peers do if not table.getsize(peer.establishedPeers) == self.PlayerCount - 1 then return false @@ -308,7 +315,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() :UpdateConnectionStatuses(statuses) - WaitFrames(1) + WaitFrames(10) end end, diff --git a/lua/ui/lobby/autolobby/AutolobbyInterface.lua b/lua/ui/lobby/autolobby/AutolobbyInterface.lua index 347e342966..7de3176465 100644 --- a/lua/ui/lobby/autolobby/AutolobbyInterface.lua +++ b/lua/ui/lobby/autolobby/AutolobbyInterface.lua @@ -67,7 +67,7 @@ local AutolobbyInterface = Class(Group) { self.Background = UIUtil.CreateBitmap(self, self.BackgroundTextures[math.random(1, 5)]) self.Preview = AutolobbyMapPreview.GetInstance(self) - self.ConnectionMatrix = AutolobbyConnectionMatrix.Create(self, 4) + self.ConnectionMatrix = AutolobbyConnectionMatrix.Create(self, 8) -- TODO: determine this number dynamically end, ---@param self UIAutolobbyInterface diff --git a/scripts/LaunchFAInstances.ps1 b/scripts/LaunchFAInstances.ps1 index 648fc8f111..87e997cd9c 100644 --- a/scripts/LaunchFAInstances.ps1 +++ b/scripts/LaunchFAInstances.ps1 @@ -1,5 +1,5 @@ param ( - [int]$players = 4, # Default to 2 instances (1 host, 1 client) + [int]$players = 8, # Default to 2 instances (1 host, 1 client) [string]$map = "/maps/scmp_009/SCMP_009_scenario.lua", # Default map: Seton's Clutch [int]$port = 15000, # Default port for hosting the game [int]$teams = 2 # Default to two teams, 0 for FFA From 2abdc11a3524bcb3c21f0ac2113bc485c9e38311 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sat, 19 Oct 2024 11:04:16 +0200 Subject: [PATCH 15/83] Use `~=` instead of `not` --- lua/ui/lobby/autolobby/AutolobbyController.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index acde17322c..ffb261381a 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -248,7 +248,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { local peers = self:GetPeers() -- check number of peers - if not table.getsize(peers) == self.PlayerCount -1 then + if table.getsize(peers) ~= self.PlayerCount -1 then return false end @@ -261,7 +261,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { -- check confirmed established connections of peers for _, peer in peers do - if not table.getsize(peer.establishedPeers) == self.PlayerCount - 1 then + if table.getsize(peer.establishedPeers) ~= self.PlayerCount - 1 then return false end end From 9607e7e25b635889fe2d42c3a105a695d7b159a5 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sat, 19 Oct 2024 13:54:22 +0200 Subject: [PATCH 16/83] Remove the wait for hot reload --- lua/ui/lobby/autolobby/AutolobbyInterface.lua | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/lua/ui/lobby/autolobby/AutolobbyInterface.lua b/lua/ui/lobby/autolobby/AutolobbyInterface.lua index 7de3176465..cb6315bba0 100644 --- a/lua/ui/lobby/autolobby/AutolobbyInterface.lua +++ b/lua/ui/lobby/autolobby/AutolobbyInterface.lua @@ -209,14 +209,7 @@ end --- Called by the module manager when this module becomes dirty function __moduleinfo.OnDirty() ModuleTrash:Destroy() - - -- trigger a reload - ForkThread( - function() - WaitSeconds(1.0) - import(__moduleinfo.name) - end - ) + import(__moduleinfo.name) end --#endregionGetSingleton From e57b4e8ff29da7c06c5965dfa15088fa8b53b096 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sat, 19 Oct 2024 14:09:37 +0200 Subject: [PATCH 17/83] Make use of `ReusedLayoutFor` --- lua/ui/lobby/autolobby/AutolobbyConnectionMatrix.lua | 6 +++--- lua/ui/lobby/autolobby/AutolobbyController.lua | 2 ++ lua/ui/lobby/autolobby/AutolobbyInterface.lua | 10 +++++----- lua/ui/lobby/autolobby/AutolobbyMapPreview.lua | 10 +++++----- scripts/LaunchFAInstances.ps1 | 2 +- 5 files changed, 16 insertions(+), 14 deletions(-) diff --git a/lua/ui/lobby/autolobby/AutolobbyConnectionMatrix.lua b/lua/ui/lobby/autolobby/AutolobbyConnectionMatrix.lua index 04a158e6f4..2dd96ef6f0 100644 --- a/lua/ui/lobby/autolobby/AutolobbyConnectionMatrix.lua +++ b/lua/ui/lobby/autolobby/AutolobbyConnectionMatrix.lua @@ -55,19 +55,19 @@ local AutolobbyConnectionMatrix = Class(Group) { ---@param self UIAutolobbyConnectionMatrix ---@param parent Control __post_init = function(self, parent) - LayoutHelpers.LayoutFor(self) + LayoutHelpers.ReusedLayoutFor(self) :Width(self.PlayerCount * 24) :Height(self.PlayerCount * 24) :End() - LayoutHelpers.LayoutFor(self.Background) + LayoutHelpers.ReusedLayoutFor(self.Background) :Fill(self) :End() -- layout the matrix for y = 1, self.PlayerCount do for x = 1, self.PlayerCount do - LayoutHelpers.LayoutFor(self.Elements[y][x]) + LayoutHelpers.ReusedLayoutFor(self.Elements[y][x]) :Width(22) :Height(22) :AtLeftTopIn(self, 2 + 24 * (x - 1), 2 + 24 * (y - 1)) diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index ffb261381a..ed5bc1477c 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -244,6 +244,8 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { ---@param peers any ---@return boolean CheckForLaunch = function(self, peers) + + do return false end -- true iff we are connected to all peers local peers = self:GetPeers() diff --git a/lua/ui/lobby/autolobby/AutolobbyInterface.lua b/lua/ui/lobby/autolobby/AutolobbyInterface.lua index cb6315bba0..d2fa5aef65 100644 --- a/lua/ui/lobby/autolobby/AutolobbyInterface.lua +++ b/lua/ui/lobby/autolobby/AutolobbyInterface.lua @@ -67,28 +67,28 @@ local AutolobbyInterface = Class(Group) { self.Background = UIUtil.CreateBitmap(self, self.BackgroundTextures[math.random(1, 5)]) self.Preview = AutolobbyMapPreview.GetInstance(self) - self.ConnectionMatrix = AutolobbyConnectionMatrix.Create(self, 8) -- TODO: determine this number dynamically + self.ConnectionMatrix = AutolobbyConnectionMatrix.Create(self, 4) -- TODO: determine this number dynamically end, ---@param self UIAutolobbyInterface ---@param parent Control __post_init = function(self, parent) - LayoutHelpers.LayoutFor(self) + LayoutHelpers.ReusedLayoutFor(self) :Fill(parent) :End() - LayoutHelpers.LayoutFor(self.Background) + LayoutHelpers.ReusedLayoutFor(self.Background) :Fill(self) :End() - LayoutHelpers.LayoutFor(self.Preview) + LayoutHelpers.ReusedLayoutFor(self.Preview) :AtCenterIn(self, -100, 0) :Width(400) :Height(400) :Hide() :End() - LayoutHelpers.LayoutFor(self.ConnectionMatrix) + LayoutHelpers.ReusedLayoutFor(self.ConnectionMatrix) :CenteredBelow(self.Preview, 20) :Hide() :End() diff --git a/lua/ui/lobby/autolobby/AutolobbyMapPreview.lua b/lua/ui/lobby/autolobby/AutolobbyMapPreview.lua index eaca5e264a..1e6a549042 100644 --- a/lua/ui/lobby/autolobby/AutolobbyMapPreview.lua +++ b/lua/ui/lobby/autolobby/AutolobbyMapPreview.lua @@ -62,19 +62,19 @@ local AutolobbyMapPreview = ClassUI(Group) { ---@param self UIAutolobbyMapPreview ---@param parent Control __post_init = function(self, parent) - LayoutHelpers.LayoutFor(self.Preview) + LayoutHelpers.ReusedLayoutFor(self.Preview) :Fill(self) :End() - LayoutHelpers.LayoutFor(self.EnergyIcon) + LayoutHelpers.ReusedLayoutFor(self.EnergyIcon) :Hide() :End() - LayoutHelpers.LayoutFor(self.MassIcon) + LayoutHelpers.ReusedLayoutFor(self.MassIcon) :Hide() :End() - LayoutHelpers.LayoutFor(self.WreckageIcon) + LayoutHelpers.ReusedLayoutFor(self.WreckageIcon) :Hide() :End() end, @@ -115,7 +115,7 @@ local AutolobbyMapPreview = ClassUI(Group) { local z = yOffset + (pz / scenarioHeight) * (size - 2) * yFactor - 4 -- position it - LayoutHelpers.LayoutFor(icon) + LayoutHelpers.ReusedLayoutFor(icon) :Width(14) :Height(14) :AtLeftTopIn(self, x, z) diff --git a/scripts/LaunchFAInstances.ps1 b/scripts/LaunchFAInstances.ps1 index 87e997cd9c..648fc8f111 100644 --- a/scripts/LaunchFAInstances.ps1 +++ b/scripts/LaunchFAInstances.ps1 @@ -1,5 +1,5 @@ param ( - [int]$players = 8, # Default to 2 instances (1 host, 1 client) + [int]$players = 4, # Default to 2 instances (1 host, 1 client) [string]$map = "/maps/scmp_009/SCMP_009_scenario.lua", # Default map: Seton's Clutch [int]$port = 15000, # Default port for hosting the game [int]$teams = 2 # Default to two teams, 0 for FFA From a4f4c09f936b9fc586c37b7a79c4bc9097c16394 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sat, 19 Oct 2024 14:21:00 +0200 Subject: [PATCH 18/83] Clean up code --- lua/ui/lobby/autolobby/AutolobbyConnectionMatrixDot.lua | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lua/ui/lobby/autolobby/AutolobbyConnectionMatrixDot.lua b/lua/ui/lobby/autolobby/AutolobbyConnectionMatrixDot.lua index ce530f1efe..2ca2424f45 100644 --- a/lua/ui/lobby/autolobby/AutolobbyConnectionMatrixDot.lua +++ b/lua/ui/lobby/autolobby/AutolobbyConnectionMatrixDot.lua @@ -20,13 +20,10 @@ --** SOFTWARE. --****************************************************************************************************** -local UIUtil = import("/lua/ui/uiutil.lua") -local LayoutHelpers = import("/lua/maui/layouthelpers.lua") +local EnumColors = import("/lua/shared/color.lua").EnumColors local Bitmap = import("/lua/maui/bitmap.lua").Bitmap -local EnumColors = import("/lua/shared/color.lua").EnumColors - --- A small dot that represents the connection status between players. ---@class UIAutolobbyConnectionMatrixDot : Bitmap ---@field IsAliveTimestamp number From 7d2d56496478c0aafbaa65be5743109bcd734dbd Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sat, 19 Oct 2024 14:49:01 +0200 Subject: [PATCH 19/83] Rework messages `AutolobbyMessages.lua` now document what messages exist and how/when they are accepted. To prevent too much coupling with the controller we simplified the handler to just be a wrapper to a function inside the lobby. --- .../lobby/autolobby/AutolobbyController.lua | 88 +++++++++++++++++-- ...sageHandlers.lua => AutolobbyMessages.lua} | 87 +++++++++--------- 2 files changed, 123 insertions(+), 52 deletions(-) rename lua/ui/lobby/autolobby/{AutolobbyMessageHandlers.lua => AutolobbyMessages.lua} (70%) diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index ed5bc1477c..02eae1b8a0 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -25,7 +25,7 @@ local Utils = import("/lua/system/utils.lua") local MohoLobbyMethods = moho.lobby_methods local DebugComponent = import("/lua/shared/components/DebugComponent.lua").DebugComponent -local AutolobbyMessageHandlers = import("/lua/ui/lobby/autolobby/AutolobbyMessageHandlers.lua").AutolobbyMessageHandlers +local AutolobbyMessages = import("/lua/ui/lobby/autolobby/AutolobbyMessages.lua").AutolobbyMessages local AutolobbyEngineStrings = { -- General info strings @@ -183,6 +183,9 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { return options end, + --------------------------------------------------------------------------- + --#region Utilities + ---@param self UIAutolobbyCommunications ---@param peers Peer[] ---@return UIAutolobbyConnections @@ -230,6 +233,78 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { return statuses end, + --------------------------------------------------------------------------- + --#region Message Handlers + -- + -- All the message functions in this section run asynchroniously on each + -- client. They are responsible for processing the data received from + -- other peers. Validation is done in `AutolobbyMessages` before the message + -- processed. + + ---@param self UIAutolobbyCommunications + ---@param data UIAutolobbyIsAliveMessage + ProcessIsAliveMessage = function(self, data) + -- update UI for player options + import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() + :UpdateIsAliveStamp(tonumber(data.SenderID) + 1) + end, + + ---@param self UIAutolobbyCommunications + ---@param data UIAutolobbyAddPlayerMessage + ProcessAddPlayerMessage = function(self, data) + ---@type UIAutolobbyPlayer + local playerOptions = data.PlayerOptions + + -- override some data + playerOptions.OwnerID = data.SenderID + playerOptions.PlayerName = self:MakeValidPlayerName(playerOptions.OwnerID, playerOptions.PlayerName) + + -- TODO: verify that the StartSpot is not occupied + -- put the player where it belongs + self.PlayerOptions[playerOptions.StartSpot] = playerOptions + + -- sync game options with the connected peer + self:SendData(data.SenderID, { Type = "UpdateGameOptions", GameOptions = self.GameOptions }) + + -- sync player options to all connected peers + self:BroadcastData({ Type = "UpdatePlayerOptions", GameOptions = self.PlayerOptions }) + end, + + ---@param self UIAutolobbyCommunications + ---@param data UIAutolobbyUpdatePlayerOptionsMessage + ProcessUpdatePlayerOptionsMessage = function(self, data) + self.PlayerOptions = data.PlayerOptions + + -- update UI for player options + import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() + :UpdatePlayerOptions(self.PlayerOptions) + end, + + ---@param self UIAutolobbyCommunications + ---@param data UIAutolobbyUpdateGameOptionsMessage + ProcessUpdateGameOptionsMessage = function(self, data) + self.GameOptions = data.GameOptions + + PrefetchSession(self.GameOptions.ScenarioFile, {}, true) + + -- update UI for game options + import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() + :UpdateGameOptions(self.GameOptions) + end, + + ---@param self UIAutolobbyCommunications + ---@param data UIAutolobbyLaunchMessage + ProcessLaunchMessage = function(self, data) + self:LaunchGame(data.GameConfig) + end, + + --#endregion + + --#endregion + + --------------------------------------------------------------------------- + --#region Threads + --- A thread to indicate that we're still around. Various properties such as ping are not updated --- until a message is received. This thread introduces occasional traffic between players. ---@param self UIAutolobbyCommunications @@ -245,12 +320,11 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { ---@return boolean CheckForLaunch = function(self, peers) - do return false end -- true iff we are connected to all peers local peers = self:GetPeers() -- check number of peers - if table.getsize(peers) ~= self.PlayerCount -1 then + if table.getsize(peers) ~= self.PlayerCount - 1 then return false end @@ -321,6 +395,8 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { end end, + --#endregion + --------------------------------------------------------------------------- --#region Engine interface @@ -329,7 +405,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { ---@param data UILobbyData BroadcastData = function(self, data) self:DebugSpew("BroadcastData", data.Type) - if not AutolobbyMessageHandlers[data.Type] then + if not AutolobbyMessages[data.Type] then self:DebugWarn("Broadcasting unknown message type", data.Type) end @@ -489,7 +565,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { ---@return nil SendData = function(self, uid, data) self:DebugSpew("SendData", uid, data.Type) - if not AutolobbyMessageHandlers[data.Type] then + if not AutolobbyMessages[data.Type] then self:DebugWarn("Sending unknown message type", data.Type, "to", uid) end @@ -599,7 +675,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { self:DebugSpew("DataReceived", data.Type, data.SenderID, data.SenderName) ---@type UIAutolobbyMessageHandler? - local messageType = AutolobbyMessageHandlers[data.Type] + local messageType = AutolobbyMessages[data.Type] -- verify that the message type exists if not messageType then diff --git a/lua/ui/lobby/autolobby/AutolobbyMessageHandlers.lua b/lua/ui/lobby/autolobby/AutolobbyMessages.lua similarity index 70% rename from lua/ui/lobby/autolobby/AutolobbyMessageHandlers.lua rename to lua/ui/lobby/autolobby/AutolobbyMessages.lua index f41f521a4d..8d8e49d2ea 100644 --- a/lua/ui/lobby/autolobby/AutolobbyMessageHandlers.lua +++ b/lua/ui/lobby/autolobby/AutolobbyMessages.lua @@ -20,6 +20,12 @@ --** SOFTWARE. --****************************************************************************************************** +-- This module represents all valid messages that the autolobby accepts from other +-- peers. Messages can be send with `lobby:SendData` or with `lobby:BroadcastData`. +-- Messages are automatically checked to exist and then verified with the `Accept` +-- function. If the message is accepted the handler is called, which is just a +-- wrapper to another function in the autolobby. + ---@class UIAutolobbyMessageHandler ---@field Accept fun(lobby: UIAutolobbyCommunications, data: UILobbyReceivedMessage): boolean # Responsible for filtering out non-sense ---@field Handler fun(lobby: UIAutolobbyCommunications, data: UILobbyReceivedMessage) # Responsible for handling the message @@ -38,29 +44,32 @@ end --- Represents all valid message types that can be sent between peers. ---@type table -AutolobbyMessageHandlers = { +AutolobbyMessages = { IsAlive = { + + ---@class UIAutolobbyIsAliveMessage : UILobbyReceivedMessage + ---@param lobby UIAutolobbyCommunications - ---@param data UILobbyReceivedMessage + ---@param data UIAutolobbyIsAliveMessage ---@return boolean Accept = function(lobby, data) return true end, ---@param lobby UIAutolobbyCommunications - ---@param data UILobbyReceivedMessage + ---@param data UIAutolobbyIsAliveMessage Handler = function(lobby, data) - lobby:DebugSpew("IsAlive handler") - - -- update UI for player options - import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() - :UpdateIsAliveStamp(tonumber(data.SenderID) + 1) + lobby:ProcessIsAliveMessage(data) end }, AddPlayer = { + + ---@class UIAutolobbyAddPlayerMessage : UILobbyReceivedMessage + ---@field PlayerOptions UIAutolobbyPlayer + ---@param lobby UIAutolobbyCommunications - ---@param data UILobbyReceivedMessage + ---@param data UIAutolobbyAddPlayerMessage ---@return boolean Accept = function(lobby, data) if not IsHost(lobby, data) then @@ -88,31 +97,19 @@ AutolobbyMessageHandlers = { end, ---@param lobby UIAutolobbyCommunications - ---@param data UILobbyReceivedMessage + ---@param data UIAutolobbyAddPlayerMessage Handler = function(lobby, data) - - ---@type UIAutolobbyPlayer - local playerOptions = data.PlayerOptions - - -- override some data - playerOptions.OwnerID = data.SenderID - playerOptions.PlayerName = lobby:MakeValidPlayerName(playerOptions.OwnerID, playerOptions.PlayerName) - - -- TODO: verify that the StartSpot is not occupied - -- put the player where it belongs - lobby.PlayerOptions[playerOptions.StartSpot] = playerOptions - - -- sync game options with the connected peer - lobby:SendData(data.SenderID, { Type = "UpdateGameOptions", GameOptions = lobby.GameOptions }) - - -- sync player options to all connected peers - lobby:BroadcastData({ Type = "UpdatePlayerOptions", GameOptions = lobby.PlayerOptions }) + lobby:ProcessAddPlayerMessage(data) end }, UpdatePlayerOptions = { + + ---@class UIAutolobbyUpdatePlayerOptionsMessage : UILobbyReceivedMessage + ---@field PlayerOptions UIAutolobbyPlayer[] + ---@param lobby UIAutolobbyCommunications - ---@param data UILobbyReceivedMessage + ---@param data UIAutolobbyUpdatePlayerOptionsMessage ---@return boolean Accept = function(lobby, data) if not IsFromHost(lobby, data) then @@ -126,19 +123,19 @@ AutolobbyMessageHandlers = { end, ---@param lobby UIAutolobbyCommunications - ---@param data UILobbyReceivedMessage + ---@param data UIAutolobbyUpdatePlayerOptionsMessage Handler = function(lobby, data) - lobby.PlayerOptions = data.PlayerOptions - - -- update UI for player options - import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() - :UpdatePlayerOptions(lobby.PlayerOptions) + lobby:ProcessUpdatePlayerOptionsMessage(data) end }, UpdateGameOptions = { + + ---@class UIAutolobbyUpdateGameOptionsMessage : UILobbyReceivedMessage + ---@field GameOptions UILobbyLaunchGameOptionsConfiguration + ---@param lobby UIAutolobbyCommunications - ---@param data UILobbyReceivedMessage + ---@param data UIAutolobbyUpdateGameOptionsMessage ---@return boolean Accept = function(lobby, data) if not IsFromHost(lobby, data) then @@ -152,21 +149,19 @@ AutolobbyMessageHandlers = { end, ---@param lobby UIAutolobbyCommunications - ---@param data UILobbyReceivedMessage + ---@param data UIAutolobbyUpdateGameOptionsMessage Handler = function(lobby, data) - lobby.GameOptions = data.GameOptions - - PrefetchSession(lobby.GameOptions.ScenarioFile, {}, true) - - -- update UI for game options - import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() - :UpdateGameOptions(lobby.GameOptions) + lobby:ProcessUpdateGameOptionsMessage(data) end }, Launch = { + + ---@class UIAutolobbyLaunchMessage : UILobbyReceivedMessage + ---@field GameConfig UILobbyLaunchConfiguration + ---@param lobby UIAutolobbyCommunications - ---@param data UILobbyReceivedMessage + ---@param data UIAutolobbyLaunchMessage ---@return boolean Accept = function(lobby, data) if not IsFromHost(lobby, data) then @@ -180,9 +175,9 @@ AutolobbyMessageHandlers = { end, ---@param lobby UIAutolobbyCommunications - ---@param data UILobbyReceivedMessage + ---@param data UIAutolobbyLaunchMessage Handler = function(lobby, data) - lobby:LaunchGame(data.GameConfig) + lobby:ProcessLaunchMessage(data) end } } From d1da9f4c42a7fe51d2c92630b1f60b0806ab0d66 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sat, 19 Oct 2024 14:58:14 +0200 Subject: [PATCH 20/83] Better document the use of the peer identifier --- engine/User/CLobby.lua | 44 ++++----- lua/ui/globals/GpgNetSend.lua | 4 +- lua/ui/lobby/autolobby.lua | 6 +- .../lobby/autolobby/AutolobbyController.lua | 90 +++++++++---------- lua/ui/lobby/autolobby/AutolobbyInterface.lua | 4 +- 5 files changed, 74 insertions(+), 74 deletions(-) diff --git a/engine/User/CLobby.lua b/engine/User/CLobby.lua index 7235bdd960..22c61ac1cf 100644 --- a/engine/User/CLobby.lua +++ b/engine/User/CLobby.lua @@ -6,15 +6,15 @@ local CLobby = {} --- "0", "1", "2", but also "32254" and the like. ----@alias UILobbyPlayerId string +---@alias UILobbyPeerId string ---@alias GPGNetAddress string | number ---@alias UIPeerStatus 'None' | 'Pending' | 'Connecting' | 'Answering' | 'Established' | 'TimedOut' | 'Errored' ---@class Peer ----@field establishedPeers string[] ----@field id string +---@field establishedPeers UILobbyPeerId[] +---@field id UILobbyPeerId ---@field ping number ---@field name string ---@field quiet number @@ -22,7 +22,7 @@ local CLobby = {} --- A piece of data that is one can send with `BroadcastData` or `SendData` to other player(s) in the lobby. ---@class UILobbyReceivedMessage : table ----@field SenderID UILobbyPlayerId # Set by the engine, allows us to identify the source. +---@field SenderID UILobbyPeerId # Set by the engine, allows us to identify the source. ---@field SenderName string # Set by the engine, nickname of the source. ---@field Type string # Type of message @@ -40,14 +40,14 @@ local CLobby = {} ---@field ScenarioFile any # Read by the engine to load the scenario of the game. ---@field Timeouts any # Read by the engine to determine the behavior of time outs. ---@field CivilianAlliance any # Read by the engine to determine the alliance towards civilians. ----@field GameSpeed any # Read by the engine to determine the behavior of game speed (adjustments). +---@field GameSpeed any # Read by the engine to determine the behavior of game speed (adjustments). ---@class UILobbyLaunchGameModsConfiguration ---@field name string # Read by the engine, TODO ---@field uid string # Read by the engine, TODO ---@class UILobbyLaunchObserverConfiguration ----@field OwnerID UILobbyPlayerId # Read by the engine, TODO +---@field OwnerID UILobbyPeerId # Read by the engine, TODO ---@field PlayerName string # Read by the engine, TODO ---@class UILobbyLaunchPlayerConfiguration @@ -60,7 +60,7 @@ local CLobby = {} ---@field ArmyColor number # Read by the engine, is mapped to a color by reading the values of `lua\GameColors.lua`. ---@field PlayerColor number # Read by the engine, is mapped to a color by reading the values of `lua\GameColors.lua` ---@field Faction number # Read by the engine to determine the faction of the player. ----@field OwnerID UILobbyPlayerId # Read by the engine, TODO +---@field OwnerID UILobbyPeerId # Read by the engine, TODO --- All the following fields are read by the engine upon launching the lobby. ---@class UILobbyLaunchConfiguration @@ -77,8 +77,8 @@ end --- Connect to a new peer. The peer will now show up in `GetPeer` and `GetPeers` ---@param address GPGNetAddress # includes the port ---@param name string ----@param uid string -function CLobby:ConnectToPeer(address, name, uid) +---@param peerId UILobbyPeerId +function CLobby:ConnectToPeer(address, name, peerId) end --- @@ -90,18 +90,18 @@ function CLobby:Destroy() end --- Disconnect from a peer. The peer will no longer show in `GetPeer` and `GetPeers`. ----@param uid string -function CLobby:DisconnectFromPeer(uid) +---@param peerId UILobbyPeerId +function CLobby:DisconnectFromPeer(peerId) end --- Eject a peer from the lobby. The peer will no longer show in `GetPeer` and `GetPeers`. ----@param targetID string +---@param peerId UILobbyPeerId ---@param reason string -function CLobby:EjectPeer(targetID, reason) +function CLobby:EjectPeer(peerId, reason) end --- Retrieves the local client identifier. ----@return UILobbyPlayerId +---@return UILobbyPeerId function CLobby:GetLocalPlayerID() end @@ -116,9 +116,9 @@ function CLobby:GetLocalPort() end --- Retrieves a specific peer ----@param uid string +---@param peerId UILobbyPeerId ---@return Peer -function CLobby:GetPeer(uid) +function CLobby:GetPeer(peerId) end --- Retrieves all peers @@ -138,8 +138,8 @@ end --- Joins a lobby hosted by another peer. See `HostGame` to host a game. ---@param address GPGNetAddress ---@param remotePlayerName? string | nil ----@param remotePlayerUID? string -function CLobby:JoinGame(address, remotePlayerName, remotePlayerUID) +---@param remotePlayerPeerId? UILobbyPeerId +function CLobby:JoinGame(address, remotePlayerName, remotePlayerPeerId) end --- @@ -154,16 +154,16 @@ function CLobby:MakeValidGameName(origName) end --- Creates a unique, alternative player name if that is required ----@param uid string +---@param peerId UILobbyPeerId ---@param origName string ---@return string -function CLobby:MakeValidPlayerName(uid, origName) +function CLobby:MakeValidPlayerName(peerId, origName) end --- Sends data to a specific peer. See `BroadcastData` for sending to all peers. ----@param targetID string +---@param peerId UILobbyPeerId ---@param data UILobbyData -function CLobby:SendData(targetID, data) +function CLobby:SendData(peerId, data) end return CLobby diff --git a/lua/ui/globals/GpgNetSend.lua b/lua/ui/globals/GpgNetSend.lua index 0d269a196a..84753bb9be 100644 --- a/lua/ui/globals/GpgNetSend.lua +++ b/lua/ui/globals/GpgNetSend.lua @@ -70,7 +70,7 @@ end --- Sends player options to the lobby server. For more context: --- - https://github.com/search?q=org%3AFAForever+player_option&type=code ----@param peerId UILobbyPlayerId +---@param peerId UILobbyPeerId ---@param key 'Team' | 'Army' | 'StartSpot' | 'Faction' | 'Color' ---@param value string | number GpgNetSendPlayerOption = function(peerId, key, value) @@ -102,7 +102,7 @@ end --- Sends game status to the lobby server. For more context: --- - https://github.com/search?q=repo%3AFAForever%2Fserver+GameState&type=code ----@param peerId UILobbyPlayerId +---@param peerId UILobbyPeerId GpgNetSendDisconnected = function(peerId) GpgNetSend('Disconnected', peerId) end diff --git a/lua/ui/lobby/autolobby.lua b/lua/ui/lobby/autolobby.lua index 8664769ccf..1c27fbf762 100644 --- a/lua/ui/lobby/autolobby.lua +++ b/lua/ui/lobby/autolobby.lua @@ -84,7 +84,7 @@ end ---@param address GPGNetAddress ---@param asObserver boolean ---@param playerName string ----@param uid UILobbyPlayerId +---@param uid UILobbyPeerId function JoinGame(address, asObserver, playerName, uid) LOG("JoinGame", address, asObserver, playerName, uid) @@ -118,7 +118,7 @@ end --- Called by the engine. ---@param addressAndPort GPGNetAddress ---@param name any ----@param uid UILobbyPlayerId +---@param uid UILobbyPeerId function ConnectToPeer(addressAndPort, name, uid) LOG("ConnectToPeer", addressAndPort, name, uid) @@ -128,7 +128,7 @@ function ConnectToPeer(addressAndPort, name, uid) end --- Called by the engine. ----@param uid UILobbyPlayerId +---@param uid UILobbyPeerId ---@param doNotUpdateView any function DisconnectFromPeer(uid, doNotUpdateView) LOG("DisconnectFromPeer", uid, doNotUpdateView) diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index 02eae1b8a0..2a17012ead 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -67,9 +67,9 @@ local AutolobbyEngineStrings = { ---@class UIAutolobbyCommunications : moho.lobby_methods, DebugComponent ---@field Trash TrashBag ---@field InterfaceTrash TrashBag ----@field LocalID UILobbyPlayerId # a number that is stringified +---@field LocalID UILobbyPeerId # a number that is stringified ---@field LocalPlayerName string # nickname ----@field HostID UILobbyPlayerId +---@field HostID UILobbyPeerId ---@field PlayerCount number ---@field GameOptions UILobbyLaunchGameOptionsConfiguration # Is synced from the host to the others. ---@field PlayerOptions UIAutolobbyPlayer[] # Is synced from the host to the others. @@ -416,11 +416,11 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { ---@param self any ---@param address any ---@param name any - ---@param uid any + ---@param peerId UILobbyPeerId ---@return nil - ConnectToPeer = function(self, address, name, uid) - self:DebugSpew("ConnectToPeer", address, name, uid) - return MohoLobbyMethods.ConnectToPeer(self, address, name, uid) + ConnectToPeer = function(self, address, name, peerId) + self:DebugSpew("ConnectToPeer", address, name, peerId) + return MohoLobbyMethods.ConnectToPeer(self, address, name, peerId) end, --- ??? @@ -444,26 +444,26 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { --- Disconnects from a peer. --- See also `ConnectToPeer` to connect ---@param self UIAutolobbyCommunications - ---@param uid any + ---@param peerId UILobbyPeerId ---@return nil - DisconnectFromPeer = function(self, uid) - self:DebugSpew("DisconnectFromPeer", uid) - return MohoLobbyMethods.DisconnectFromPeer(self, uid) + DisconnectFromPeer = function(self, peerId) + self:DebugSpew("DisconnectFromPeer", peerId) + return MohoLobbyMethods.DisconnectFromPeer(self, peerId) end, --- Ejects a peer from the lobby. ---@param self UIAutolobbyCommunications - ---@param uid UILobbyPlayerId + ---@param peerId UILobbyPeerId ---@param reason string ---@return nil - EjectPeer = function(self, uid, reason) - self:DebugSpew("EjectPeer", uid, reason) - return MohoLobbyMethods.EjectPeer(self, uid, reason) + EjectPeer = function(self, peerId, reason) + self:DebugSpew("EjectPeer", peerId, reason) + return MohoLobbyMethods.EjectPeer(self, peerId, reason) end, --- Retrieves the local identifier. ---@param self UIAutolobbyCommunications - ---@return UILobbyPlayerId + ---@return UILobbyPeerId GetLocalPlayerID = function(self) self:DebugSpew("GetLocalPlayerID") return MohoLobbyMethods.GetLocalPlayerID(self) @@ -487,11 +487,11 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { --- Retrieves information about a peer. See `GetPeers` to get the same information for all connected peers. ---@param self UIAutolobbyCommunications - ---@param uid UILobbyPlayerId + ---@param peerId UILobbyPeerId ---@return Peer - GetPeer = function(self, uid) - self:DebugSpew("GetPeer", uid) - return MohoLobbyMethods.GetPeer(self, uid) + GetPeer = function(self, peerId) + self:DebugSpew("GetPeer", peerId) + return MohoLobbyMethods.GetPeer(self, peerId) end, --- Retrieves information about all connected peers. See `GetPeer` to get information for a specific peer. @@ -521,11 +521,11 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { ---@param self UIAutolobbyCommunications ---@param address GPGNetAddress ---@param remotePlayerName string - ---@param remotePlayerUID UILobbyPlayerId + ---@param remotePlayerPeerId UILobbyPeerId ---@return nil - JoinGame = function(self, address, remotePlayerName, remotePlayerUID) - self:DebugSpew("JoinGame", address, remotePlayerName, remotePlayerUID) - return MohoLobbyMethods.JoinGame(self, address, remotePlayerName, remotePlayerUID) + JoinGame = function(self, address, remotePlayerName, remotePlayerPeerId) + self:DebugSpew("JoinGame", address, remotePlayerName, remotePlayerPeerId) + return MohoLobbyMethods.JoinGame(self, address, remotePlayerName, remotePlayerPeerId) end, --- Launches the game for the local client. The game configuration that is passed in should originate from the host. @@ -551,25 +551,25 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { --- Returns a valid player name. ---@param self UIAutolobbyCommunications - ---@param uid UILobbyPlayerId + ---@param peerId UILobbyPeerId ---@param name string ---@return string - MakeValidPlayerName = function(self, uid, name) - self:DebugSpew("MakeValidPlayerName", uid, name) - return MohoLobbyMethods.MakeValidPlayerName(self, uid, name) + MakeValidPlayerName = function(self, peerId, name) + self:DebugSpew("MakeValidPlayerName", peerId, name) + return MohoLobbyMethods.MakeValidPlayerName(self, peerId, name) end, ---@param self UIAutolobbyCommunications - ---@param uid UILobbyPlayerId + ---@param peerId UILobbyPeerId ---@param data UILobbyData ---@return nil - SendData = function(self, uid, data) - self:DebugSpew("SendData", uid, data.Type) + SendData = function(self, peerId, data) + self:DebugSpew("SendData", peerId, data.Type) if not AutolobbyMessages[data.Type] then - self:DebugWarn("Sending unknown message type", data.Type, "to", uid) + self:DebugWarn("Sending unknown message type", data.Type, "to", peerId) end - return MohoLobbyMethods.SendData(self, uid, data) + return MohoLobbyMethods.SendData(self, peerId, data) end, --#endregion @@ -622,13 +622,13 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { --- Called by the engine when the connection succeeds with the host. ---@param self UIAutolobbyCommunications - ---@param localId string - ---@param hostId string - ConnectionToHostEstablished = function(self, localId, newLocalName, hostId) - self:DebugSpew("ConnectionToHostEstablished", localId, newLocalName, hostId) + ---@param localPeerId UILobbyPeerId + ---@param hostPeerId string + ConnectionToHostEstablished = function(self, localPeerId, newLocalName, hostPeerId) + self:DebugSpew("ConnectionToHostEstablished", localPeerId, newLocalName, hostPeerId) self.LocalPlayerName = newLocalName - self.LocalID = localId - self.HostID = hostId + self.LocalID = localPeerId + self.HostID = hostPeerId GpgNetSendGameState('Lobby') @@ -641,10 +641,10 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { --- Called by the engine when a peer establishes a connection. ---@param self UIAutolobbyCommunications - ---@param playerId string - ---@param playerConnectedTo string[] # all established conenctions for the given player - EstablishedPeers = function(self, playerId, playerConnectedTo) - self:DebugSpew("EstablishedPeers", playerId, reprs(playerConnectedTo)) + ---@param peerId UILobbyPeerId + ---@param peerConnectedTo UILobbyPeerId[] # all established conenctions for the given player + EstablishedPeers = function(self, peerId, peerConnectedTo) + self:DebugSpew("EstablishedPeers", peerId, reprs(peerConnectedTo)) end, --#endregion @@ -702,9 +702,9 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { --- Called by the engine when a peer disconnects. ---@param self UIAutolobbyCommunications ---@param peerName string - ---@param otherId string - PeerDisconnected = function(self, peerName, otherId) - self:DebugSpew("PeerDisconnected", peerName, otherId) + ---@param peerId UILobbyPeerId + PeerDisconnected = function(self, peerName, peerId) + self:DebugSpew("PeerDisconnected", peerName, peerId) end, --- Called by the engine when the game is launched. diff --git a/lua/ui/lobby/autolobby/AutolobbyInterface.lua b/lua/ui/lobby/autolobby/AutolobbyInterface.lua index d2fa5aef65..a37730d182 100644 --- a/lua/ui/lobby/autolobby/AutolobbyInterface.lua +++ b/lua/ui/lobby/autolobby/AutolobbyInterface.lua @@ -36,7 +36,7 @@ local AutolobbyMapPreview = import("/lua/ui/lobby/autolobby/AutolobbyMapPreview. local AutolobbyConnectionMatrix = import("/lua/ui/lobby/autolobby/AutolobbyConnectionMatrix.lua") ---@class UIAutolobbyInterfaceState ----@field PlayerOptions? table +---@field PlayerOptions? table ---@field GameOptions? UILobbyLaunchGameOptionsConfiguration ---@field Connections? UIAutolobbyConnections ---@field Statuses? UIAutolobbyStatus @@ -113,7 +113,7 @@ local AutolobbyInterface = Class(Group) { end, ---@param self UIAutolobbyInterface - ---@param playerOptions table + ---@param playerOptions table UpdatePlayerOptions = function(self, playerOptions) self.State.PlayerOptions = playerOptions end, From 5be5fffab62adbc639e4dd36d5834ac009c29688 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sat, 19 Oct 2024 17:04:49 +0200 Subject: [PATCH 21/83] Add support for player colors based on the start position. --- lua/GameColors.lua | 64 ++++++++++++++++++- .../lobby/autolobby/AutolobbyController.lua | 8 +-- 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/lua/GameColors.lua b/lua/GameColors.lua index c8b31dffcc..6a1d956efe 100644 --- a/lua/GameColors.lua +++ b/lua/GameColors.lua @@ -1,3 +1,66 @@ +--****************************************************************************************************** +--** Copyright (c) 2024 Willem 'Jip' Wijnia +--** +--** Permission is hereby granted, free of charge, to any person obtaining a copy +--** of this software and associated documentation files (the "Software"), to deal +--** in the Software without restriction, including without limitation the rights +--** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +--** copies of the Software, and to permit persons to whom the Software is +--** furnished to do so, subject to the following conditions: +--** +--** The above copyright notice and this permission notice shall be included in all +--** copies or substantial portions of the Software. +--** +--** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +--** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +--** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +--** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +--** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +--** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +--** SOFTWARE. +--****************************************************************************************************** + +-- When launching a lobby each player has a configuration. This configuration has the +-- fields `ArmyColor` and `PlayerColor`. The values of these fields are numbers. This +-- module is responsible for converting the integer-based player and army colors +-- into a hex-based color string that the engine understands. + +local WarmColdMapping = { + -- 1v1 + 11, -- "ff436eee" (11) new blue1 + 01, -- "FFe80a0a" (01) Cybran red + + -- 2v2 + 12, -- "FF2929e1" (12) UEF blue + 02, -- "ff901427" (02) dark red + + -- 3v3 + 14, -- "ff9161ff" (14) purple + 03, -- "FFFF873E" (03) Nomads orange + + -- 4v4 + 15, -- "ff66ffcc" (15) aqua + 05, -- "ffa79602" (05) Sera golden + + -- beyond 4v4, which we'll not likely support any time soon. + 08, + 19, + 07, + 05, + 13, + 16, + 17, + 04 +} + +--- Maps the start location of a player into a a warm vs cold color scheme. Read the +--- introduction of this module for more context. +---@param startSpot number +---@return number +MapToWarmCold = function(startSpot) + return WarmColdMapping[startSpot] +end + --- Determines the available colors for players and the default color order for -- matchmaking. See autolobby.lua and lobby.lua for more information. GameColors = { @@ -7,7 +70,6 @@ GameColors = { -- Default color order used for lobbies/TMM if not otherwise specified. Tightly coupled -- with the ArmyColors and the PlayerColors tables. LobbyColorOrder = { 01, 02, 03, 04, 05, 06, 07, 08, 09, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }, -- rainbow-like color for Fearghal - TMMColorOrder = { 11, 01, 12, 02, 14, 03, 15, 05, 08, 19, 07, 05, 13, 16, 17, 04 }, -- warm vs cold -- If you end up working with this file, suggestion to install the Color Highlight extension: -- - https://marketplace.visualstudio.com/items?itemName=naumovs.color-highlight diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index 2a17012ead..077b37a150 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -22,6 +22,7 @@ local Utils = import("/lua/system/utils.lua") +local GameColors = import("/lua/GameColors.lua") local MohoLobbyMethods = moho.lobby_methods local DebugComponent = import("/lua/shared/components/DebugComponent.lua").DebugComponent @@ -110,9 +111,6 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { ---@type UIAutolobbyPlayer local info = {} - info.Team = 1 - info.PlayerColor = 1 - info.ArmyColor = 1 info.Human = true info.Civilian = false @@ -131,7 +129,9 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { -- retrieve team and start spot info.Team = tonumber(GetCommandLineArg("/team", 1)[1]) - info.StartSpot = tonumber(GetCommandLineArg("/startspot", 1)[1]) or false + info.StartSpot = tonumber(GetCommandLineArg("/startspot", 1)[1]) + info.PlayerColor = GameColors.MapToWarmCold(info.StartSpot) + info.ArmyColor = GameColors.MapToWarmCold(info.StartSpot) -- retrieve rating info.DEV = tonumber(GetCommandLineArg("/deviation", 1)[1]) or 500 From 4d00504aeb3f519fa5a8c286386f800ae66d0eb8 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sat, 19 Oct 2024 17:10:01 +0200 Subject: [PATCH 22/83] Add a comment --- lua/ui/lobby/autolobby/AutolobbyController.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index 077b37a150..d189380358 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -130,6 +130,8 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { -- retrieve team and start spot info.Team = tonumber(GetCommandLineArg("/team", 1)[1]) info.StartSpot = tonumber(GetCommandLineArg("/startspot", 1)[1]) + + -- determine army color based on start location info.PlayerColor = GameColors.MapToWarmCold(info.StartSpot) info.ArmyColor = GameColors.MapToWarmCold(info.StartSpot) From fbe418390b4338163f7db580becb5d6b50e4a424 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sat, 19 Oct 2024 17:54:18 +0200 Subject: [PATCH 23/83] Introduce a mapping from the peerId to an index When running locally the peer Ids line up nicely. However, in practice the peerIds can be all over the place and can not be easily mapped to a number. Therefore we manually keep track of them and map them to a number so that we can visualize it. --- engine/User/CLobby.lua | 2 +- .../lobby/autolobby/AutolobbyController.lua | 54 +++++++++++++++---- lua/ui/lobby/autolobby/AutolobbyInterface.lua | 4 +- scripts/LaunchFAInstances.ps1 | 2 +- 4 files changed, 49 insertions(+), 13 deletions(-) diff --git a/engine/User/CLobby.lua b/engine/User/CLobby.lua index 22c61ac1cf..93e5f811a2 100644 --- a/engine/User/CLobby.lua +++ b/engine/User/CLobby.lua @@ -14,7 +14,7 @@ local CLobby = {} ---@class Peer ---@field establishedPeers UILobbyPeerId[] ----@field id UILobbyPeerId +---@field id UILobbyPeerId # Is -1 when the status is pending ---@field ping number ---@field name string ---@field quiet number diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index d189380358..bb95ce9673 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -74,6 +74,7 @@ local AutolobbyEngineStrings = { ---@field PlayerCount number ---@field GameOptions UILobbyLaunchGameOptionsConfiguration # Is synced from the host to the others. ---@field PlayerOptions UIAutolobbyPlayer[] # Is synced from the host to the others. +---@field PeerToIndexMapping table AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { BackgroundTextures = { @@ -89,14 +90,15 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { self.Trash = TrashBag() self.InterfaceTrash = self.Trash:Add(TrashBag()) - self.LocalID = "-1" + self.LocalID = "-2" self.LocalPlayerName = "Charlie" self.PlayerCount = tonumber(GetCommandLineArg("/players", 1)[1]) or 2 self.Connections = {} - self.HostID = "-1" + self.HostID = "-2" self.GameOptions = self:CreateLocalGameOptions() self.PlayerOptions = {} + self.PeerToIndexMapping = {} end, ---@param self UIAutolobbyCommunications @@ -203,11 +205,13 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { end end + -- reprsl(peers, { depth = 3}) + -- populate the matrix for _, peer in peers do - local peerIdNumber = tonumber(peer.id) + 1 + local peerIdNumber = self:PeerIdToIndex(peer.id) for _, peerConnectedToId in peer.establishedPeers do - local peerConnectedToIdNumber = tonumber(peerConnectedToId) + 1 + local peerConnectedToIdNumber = self:PeerIdToIndex(peerConnectedToId) -- connection works both ways connections[peerIdNumber][peerConnectedToIdNumber] = true @@ -228,13 +232,38 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { end for _, peer in peers do - local peerIdNumber = tonumber(peer.id) + 1 + local peerIdNumber = self:PeerIdToIndex(peer.id) statuses[peerIdNumber] = peer.status end return statuses end, + --- Maps a peer id to an index that can be used in the interface. + ---@param self UIAutolobbyCommunications + ---@param peerId UILobbyPeerId + ---@return number + PeerIdToIndex = function(self, peerId) + if type(peerId) ~= 'string' then + self:DebugWarn("Invalid peer id", peerId) + return 1 + end + + -- happens before the connection is established + if peerId == "-1" then + -- just return some index, but do not store it + return table.getsize(self.PeerToIndexMapping) + 1 + end + + local index = self.PeerToIndexMapping[peerId] + if not index then + index = table.getsize(self.PeerToIndexMapping) + 1 + self.PeerToIndexMapping[peerId] = index + end + + return index + end, + --------------------------------------------------------------------------- --#region Message Handlers -- @@ -248,7 +277,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { ProcessIsAliveMessage = function(self, data) -- update UI for player options import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() - :UpdateIsAliveStamp(tonumber(data.SenderID) + 1) + :UpdateIsAliveStamp(self:PeerIdToIndex(data.SenderID)) end, ---@param self UIAutolobbyCommunications @@ -322,6 +351,8 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { ---@return boolean CheckForLaunch = function(self, peers) + do return false end + -- true iff we are connected to all peers local peers = self:GetPeers() @@ -393,7 +424,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() :UpdateConnectionStatuses(statuses) - WaitFrames(10) + WaitFrames(1) end end, @@ -499,7 +530,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { --- Retrieves information about all connected peers. See `GetPeer` to get information for a specific peer. ---@param self UIAutolobbyCommunications GetPeers = function(self) - self:DebugSpew("GetPeers") + -- self:DebugSpew("GetPeers") return MohoLobbyMethods.GetPeers(self) end, @@ -767,7 +798,12 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { return end - error("Autolobby communications", unpack(arg)) + local message = "Autolobby communications" + for _, arg in ipairs(arg) do + message = message .. "\t" .. tostring(arg) + end + + error(message) end, --#endregion diff --git a/lua/ui/lobby/autolobby/AutolobbyInterface.lua b/lua/ui/lobby/autolobby/AutolobbyInterface.lua index a37730d182..9657b35a16 100644 --- a/lua/ui/lobby/autolobby/AutolobbyInterface.lua +++ b/lua/ui/lobby/autolobby/AutolobbyInterface.lua @@ -67,7 +67,7 @@ local AutolobbyInterface = Class(Group) { self.Background = UIUtil.CreateBitmap(self, self.BackgroundTextures[math.random(1, 5)]) self.Preview = AutolobbyMapPreview.GetInstance(self) - self.ConnectionMatrix = AutolobbyConnectionMatrix.Create(self, 4) -- TODO: determine this number dynamically + self.ConnectionMatrix = AutolobbyConnectionMatrix.Create(self, 8) -- TODO: determine this number dynamically end, ---@param self UIAutolobbyInterface @@ -98,7 +98,7 @@ local AutolobbyInterface = Class(Group) { ---@param connections UIAutolobbyConnections UpdateConnections = function(self, connections) self.State.Connections = connections - + self.ConnectionMatrix:Show() self.ConnectionMatrix:UpdateConnections(connections) end, diff --git a/scripts/LaunchFAInstances.ps1 b/scripts/LaunchFAInstances.ps1 index 648fc8f111..87e997cd9c 100644 --- a/scripts/LaunchFAInstances.ps1 +++ b/scripts/LaunchFAInstances.ps1 @@ -1,5 +1,5 @@ param ( - [int]$players = 4, # Default to 2 instances (1 host, 1 client) + [int]$players = 8, # Default to 2 instances (1 host, 1 client) [string]$map = "/maps/scmp_009/SCMP_009_scenario.lua", # Default map: Seton's Clutch [int]$port = 15000, # Default port for hosting the game [int]$teams = 2 # Default to two teams, 0 for FFA From d41177b22d82b49fe2a3f1931f2816e2ec18e382 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sat, 19 Oct 2024 17:55:39 +0200 Subject: [PATCH 24/83] Add documentation --- lua/ui/lobby/autolobby/AutolobbyController.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index bb95ce9673..48d987cb63 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -239,7 +239,10 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { return statuses end, - --- Maps a peer id to an index that can be used in the interface. + --- Maps a peer id to an index that can be used in the interface. In + --- practice the peer id can be all over the place, ranging from -1 + --- to numbers such as 35240. With this function we map it to a sane + --- index that we can use in the interface. ---@param self UIAutolobbyCommunications ---@param peerId UILobbyPeerId ---@return number From 45a57478f44d0b7382f9be3a3d170929f55de9db Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sat, 19 Oct 2024 19:23:40 +0200 Subject: [PATCH 25/83] Fix prefetch not working properly --- engine/User.lua | 2 +- .../lobby/autolobby/AutolobbyController.lua | 28 +++++++++++++++---- lua/ui/lobby/autolobby/AutolobbyInterface.lua | 2 +- scripts/LaunchFAInstances.ps1 | 2 +- 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/engine/User.lua b/engine/User.lua index f2b205cd6a..eeb6e54703 100644 --- a/engine/User.lua +++ b/engine/User.lua @@ -919,7 +919,7 @@ end --- Start a background load with the given map and mods. --- If `hipri` is true, this will interrupt any previous loads in progress. ----@param mapname string +---@param mapname string # path to the `scmap` file ---@param mods ModInfo[] ---@param hipri? boolean function PrefetchSession(mapname, mods, hipri) diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index 48d987cb63..18f2ed8d4b 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -21,6 +21,7 @@ --****************************************************************************************************** local Utils = import("/lua/system/utils.lua") +local MapUtil = import("/lua/ui/maputil.lua") local GameColors = import("/lua/GameColors.lua") local MohoLobbyMethods = moho.lobby_methods @@ -72,6 +73,7 @@ local AutolobbyEngineStrings = { ---@field LocalPlayerName string # nickname ---@field HostID UILobbyPeerId ---@field PlayerCount number +---@field GameMods UILobbyLaunchGameModsConfiguration[] ---@field GameOptions UILobbyLaunchGameOptionsConfiguration # Is synced from the host to the others. ---@field PlayerOptions UIAutolobbyPlayer[] # Is synced from the host to the others. ---@field PeerToIndexMapping table @@ -96,6 +98,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { self.Connections = {} self.HostID = "-2" + self.GameMods = {} self.GameOptions = self:CreateLocalGameOptions() self.PlayerOptions = {} self.PeerToIndexMapping = {} @@ -267,6 +270,19 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { return index end, + ---@param self UIAutolobbyCommunications + ---@param gameOptions UILobbyLaunchGameOptionsConfiguration + ---@param gameMods UILobbyLaunchGameModsConfiguration[] + Prefetch = function(self, gameOptions, gameMods) + local scenarioPath = gameOptions.ScenarioFile + if not scenarioPath then + return + end + + local scenarioFile = MapUtil.LoadScenario(gameOptions.ScenarioFile) + PrefetchSession(scenarioFile.map, gameMods, true) + end, + --------------------------------------------------------------------------- --#region Message Handlers -- @@ -319,7 +335,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { ProcessUpdateGameOptionsMessage = function(self, data) self.GameOptions = data.GameOptions - PrefetchSession(self.GameOptions.ScenarioFile, {}, true) + self:Prefetch(self.GameOptions, self.GameMods) -- update UI for game options import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() @@ -334,8 +350,6 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { --#endregion - --#endregion - --------------------------------------------------------------------------- --#region Threads @@ -354,7 +368,8 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { ---@return boolean CheckForLaunch = function(self, peers) - do return false end + -- for debugging :) + -- do return false end -- true iff we are connected to all peers local peers = self:GetPeers() @@ -389,10 +404,11 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { local peers = self:GetPeers() local canLaunch = self:CheckForLaunch(peers) + LOG("CanLaunch", canLaunch) if canLaunch then ---@type UILobbyLaunchConfiguration local gameConfiguration = { - GameMods = {}, + GameMods = self.GameMods, GameOptions = self.GameOptions, PlayerOptions = self.PlayerOptions, Observers = {}, @@ -634,7 +650,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { self.Trash:Add(ForkThread(self.CheckForLaunchThread, self)) -- start prefetching the scenario - PrefetchSession(self.GameOptions.ScenarioFile, {}, true) + self:Prefetch(self.GameOptions, self.GameMods) GpgNetSendGameState('Lobby') diff --git a/lua/ui/lobby/autolobby/AutolobbyInterface.lua b/lua/ui/lobby/autolobby/AutolobbyInterface.lua index 9657b35a16..df04574b53 100644 --- a/lua/ui/lobby/autolobby/AutolobbyInterface.lua +++ b/lua/ui/lobby/autolobby/AutolobbyInterface.lua @@ -67,7 +67,7 @@ local AutolobbyInterface = Class(Group) { self.Background = UIUtil.CreateBitmap(self, self.BackgroundTextures[math.random(1, 5)]) self.Preview = AutolobbyMapPreview.GetInstance(self) - self.ConnectionMatrix = AutolobbyConnectionMatrix.Create(self, 8) -- TODO: determine this number dynamically + self.ConnectionMatrix = AutolobbyConnectionMatrix.Create(self, 4) -- TODO: determine this number dynamically end, ---@param self UIAutolobbyInterface diff --git a/scripts/LaunchFAInstances.ps1 b/scripts/LaunchFAInstances.ps1 index 87e997cd9c..648fc8f111 100644 --- a/scripts/LaunchFAInstances.ps1 +++ b/scripts/LaunchFAInstances.ps1 @@ -1,5 +1,5 @@ param ( - [int]$players = 8, # Default to 2 instances (1 host, 1 client) + [int]$players = 4, # Default to 2 instances (1 host, 1 client) [string]$map = "/maps/scmp_009/SCMP_009_scenario.lua", # Default map: Seton's Clutch [int]$port = 15000, # Default port for hosting the game [int]$teams = 2 # Default to two teams, 0 for FFA From fb26d785ba87d7ceea4f2770841b12522903185c Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sat, 19 Oct 2024 21:22:01 +0200 Subject: [PATCH 26/83] Introduce a delayed-joining of the lobby This prevents all connections being established at roughly the same time. --- lua/ui/lobby/autolobby.lua | 36 +++++++++++++++--------------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/lua/ui/lobby/autolobby.lua b/lua/ui/lobby/autolobby.lua index 1c27fbf762..3b56dfacc8 100644 --- a/lua/ui/lobby/autolobby.lua +++ b/lua/ui/lobby/autolobby.lua @@ -88,27 +88,21 @@ end function JoinGame(address, asObserver, playerName, uid) LOG("JoinGame", address, asObserver, playerName, uid) - if AutolobbyCommunicationsInstance then - AutolobbyCommunicationsInstance:JoinGame(address, playerName, uid) - end - - -- ForkThread( - -- function() - - -- local seconds = tonumber(GetCommandLineArg("/startspot", 1)[1]) or 1 - -- WaitSeconds(seconds) - -- if AutolobbyCommunicationsInstance then - -- AutolobbyCommunicationsInstance:JoinGame(address, playerName, uid) - - -- if seconds == 2 then - -- WaitSeconds(seconds) - -- DisconnectFromPeer(AutolobbyCommunicationsInstance:GetPeers()[2].id, false) - -- end - -- end - - - -- end - -- ) + -- if AutolobbyCommunicationsInstance then + -- AutolobbyCommunicationsInstance:JoinGame(address, playerName, uid) + -- end + + -- join over time + ForkThread( + function() + local seconds = tonumber(GetCommandLineArg("/startspot", 1)[1]) or 1 + WaitSeconds(seconds) + + if AutolobbyCommunicationsInstance then + AutolobbyCommunicationsInstance:JoinGame(address, playerName, uid) + end + end + ) -- -- start with a loading dialog -- import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() From 1be791b5336d9962089d9116e0fd5a06b62c5e84 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sun, 20 Oct 2024 08:01:18 +0200 Subject: [PATCH 27/83] Improve management of peer indices Upon disconnecting it is possible that some peers have not updated out representation of who they are established to. --- .../lobby/autolobby/AutolobbyController.lua | 47 ++++++++++++++----- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index 18f2ed8d4b..586a6399ee 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -76,7 +76,8 @@ local AutolobbyEngineStrings = { ---@field GameMods UILobbyLaunchGameModsConfiguration[] ---@field GameOptions UILobbyLaunchGameOptionsConfiguration # Is synced from the host to the others. ---@field PlayerOptions UIAutolobbyPlayer[] # Is synced from the host to the others. ----@field PeerToIndexMapping table +---@field PeerToIndexMapping table +---@field DisconnectedPeers table # AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { BackgroundTextures = { @@ -102,6 +103,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { self.GameOptions = self:CreateLocalGameOptions() self.PlayerOptions = {} self.PeerToIndexMapping = {} + self.DisconnectedPeers = {} end, ---@param self UIAutolobbyCommunications @@ -214,11 +216,14 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { for _, peer in peers do local peerIdNumber = self:PeerIdToIndex(peer.id) for _, peerConnectedToId in peer.establishedPeers do + local peerIdNumber = self:PeerIdToIndex(peer.id) local peerConnectedToIdNumber = self:PeerIdToIndex(peerConnectedToId) -- connection works both ways - connections[peerIdNumber][peerConnectedToIdNumber] = true - connections[peerConnectedToIdNumber][peerIdNumber] = true + if peerIdNumber and peerConnectedToIdNumber then + connections[peerIdNumber][peerConnectedToIdNumber] = true + connections[peerConnectedToIdNumber][peerIdNumber] = true + end end end @@ -236,23 +241,30 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { for _, peer in peers do local peerIdNumber = self:PeerIdToIndex(peer.id) - statuses[peerIdNumber] = peer.status + if peerIdNumber then + statuses[peerIdNumber] = peer.status + end end return statuses end, - --- Maps a peer id to an index that can be used in the interface. In - --- practice the peer id can be all over the place, ranging from -1 - --- to numbers such as 35240. With this function we map it to a sane + --- Maps a peer id to an index that can be used in the interface. In + --- practice the peer id can be all over the place, ranging from -1 + --- to numbers such as 35240. With this function we map it to a sane --- index that we can use in the interface. ---@param self UIAutolobbyCommunications ---@param peerId UILobbyPeerId - ---@return number + ---@return number | false PeerIdToIndex = function(self, peerId) if type(peerId) ~= 'string' then self:DebugWarn("Invalid peer id", peerId) - return 1 + return false + end + + -- happens when a peer disconnected from us, but not (yet) to other players + if self.DisconnectedPeers[peerId] then + return false end -- happens before the connection is established @@ -295,8 +307,12 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { ---@param data UIAutolobbyIsAliveMessage ProcessIsAliveMessage = function(self, data) -- update UI for player options - import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() - :UpdateIsAliveStamp(self:PeerIdToIndex(data.SenderID)) + + local peerIndex = self:PeerIdToIndex(data.SenderID) + if peerIndex then + import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() + :UpdateIsAliveStamp(peerIndex) + end end, ---@param self UIAutolobbyCommunications @@ -500,6 +516,11 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { ---@return nil DisconnectFromPeer = function(self, peerId) self:DebugSpew("DisconnectFromPeer", peerId) + + -- reset mapping + self.PeerToIndexMapping = {} + self.DisconnectedPeers[peerId] = GetSystemTimeSeconds() + return MohoLobbyMethods.DisconnectFromPeer(self, peerId) end, @@ -757,6 +778,10 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { ---@param peerId UILobbyPeerId PeerDisconnected = function(self, peerName, peerId) self:DebugSpew("PeerDisconnected", peerName, peerId) + + -- reset mapping + self.PeerToIndexMapping = {} + self.DisconnectedPeers[peerId] = GetSystemTimeSeconds() end, --- Called by the engine when the game is launched. From dd2613ecf5fe6817eae05b0c7cdf411dfe3996d8 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sun, 20 Oct 2024 08:03:34 +0200 Subject: [PATCH 28/83] Add an initial delay before we populate the connection matrix It appears that if we try to retrieve the peers too soon then it may crash the game. --- lua/ui/lobby/autolobby/AutolobbyController.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index 586a6399ee..7d52c59094 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -447,6 +447,9 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { ---@param self UIAutolobbyCommunications ConnectionMatrixThread = function(self) + -- right at the start it can be a little jumpy, so we wait a second + WaitSeconds(1) + while not IsDestroyed(self) do local peers = self:GetPeers() @@ -459,7 +462,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() :UpdateConnectionStatuses(statuses) - WaitFrames(1) + WaitFrames(10) end end, From b67ba7ed312e4a0465b83d2997f342055468728d Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sun, 20 Oct 2024 08:04:30 +0200 Subject: [PATCH 29/83] Fix for peer to index functionality --- lua/ui/lobby/autolobby/AutolobbyController.lua | 3 --- 1 file changed, 3 deletions(-) diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index 7d52c59094..8eee7f04f0 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -210,11 +210,8 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { end end - -- reprsl(peers, { depth = 3}) - -- populate the matrix for _, peer in peers do - local peerIdNumber = self:PeerIdToIndex(peer.id) for _, peerConnectedToId in peer.establishedPeers do local peerIdNumber = self:PeerIdToIndex(peer.id) local peerConnectedToIdNumber = self:PeerIdToIndex(peerConnectedToId) From 10877eaf6dd1407d46cb85376c36d7dc1f8a38b6 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sun, 20 Oct 2024 08:11:50 +0200 Subject: [PATCH 30/83] Keep track of the lobby creation/hosting/joining parameters --- engine/User/CLobby.lua | 4 +++ lua/ui/lobby/autolobby.lua | 32 +++++++++++++++---- .../lobby/autolobby/AutolobbyController.lua | 24 ++++++++++++-- 3 files changed, 51 insertions(+), 9 deletions(-) diff --git a/engine/User/CLobby.lua b/engine/User/CLobby.lua index 93e5f811a2..77d07aa3eb 100644 --- a/engine/User/CLobby.lua +++ b/engine/User/CLobby.lua @@ -10,6 +10,8 @@ local CLobby = {} ---@alias GPGNetAddress string | number +---@alias UILobbyProtocol 'UDP' | 'TCP' + ---@alias UIPeerStatus 'None' | 'Pending' | 'Connecting' | 'Answering' | 'Established' | 'TimedOut' | 'Errored' ---@class Peer @@ -136,6 +138,8 @@ function CLobby:IsHost() end --- Joins a lobby hosted by another peer. See `HostGame` to host a game. +--- +--- Is not idempotent - joining twice will generate an error. ---@param address GPGNetAddress ---@param remotePlayerName? string | nil ---@param remotePlayerPeerId? UILobbyPeerId diff --git a/lua/ui/lobby/autolobby.lua b/lua/ui/lobby/autolobby.lua index 3b56dfacc8..a641791516 100644 --- a/lua/ui/lobby/autolobby.lua +++ b/lua/ui/lobby/autolobby.lua @@ -35,10 +35,10 @@ local AutolobbyCommunicationsInstance = false --- Creates the lobby communications, called (indirectly) by the engine to setup the module state. ----@param protocol any ----@param localPort any ----@param desiredPlayerName any ----@param localPlayerUID any +---@param protocol UILobbyProtocol +---@param localPort number +---@param desiredPlayerName string +---@param localPlayerUID UILobbyPeerId ---@param natTraversalProvider any function CreateLobby(protocol, localPort, desiredPlayerName, localPlayerUID, natTraversalProvider) LOG("CreateLobby", protocol, localPort, desiredPlayerName, localPlayerUID, natTraversalProvider) @@ -50,6 +50,14 @@ function CreateLobby(protocol, localPort, desiredPlayerName, localPlayerUID, nat localPlayerUID, natTraversalProvider ) + AutolobbyCommunicationsInstance.LobbyParameters = AutolobbyCommunicationsInstance.LobbyParameters or {} + AutolobbyCommunicationsInstance.LobbyParameters.Protocol = protocol + AutolobbyCommunicationsInstance.LobbyParameters.LocalPort = localPort + AutolobbyCommunicationsInstance.LobbyParameters.MaxConnections = maxConnections + AutolobbyCommunicationsInstance.LobbyParameters.DesiredPlayerName = desiredPlayerName + AutolobbyCommunicationsInstance.LobbyParameters.LocalPlayerPeerId = localPlayerUID + AutolobbyCommunicationsInstance.LobbyParameters.NatTraversalProvider = natTraversalProvider + GpgNetSendGameState('Idle') -- create the singleton for the interface @@ -67,6 +75,12 @@ function HostGame(gameName, scenarioFileName, singlePlayer) LOG("HostGame", gameName, scenarioFileName, singlePlayer) if AutolobbyCommunicationsInstance then + + AutolobbyCommunicationsInstance.HostParameters = AutolobbyCommunicationsInstance.HostParameters or {} + AutolobbyCommunicationsInstance.HostParameters.GameName = gameName + AutolobbyCommunicationsInstance.HostParameters.ScenarioFile = scenarioFileName + AutolobbyCommunicationsInstance.HostParameters.SinglePlayer = singlePlayer + AutolobbyCommunicationsInstance.GameOptions.ScenarioFile = string.gsub(scenarioFileName, ".v%d%d%d%d_scenario.lua", "_scenario.lua") @@ -88,9 +102,13 @@ end function JoinGame(address, asObserver, playerName, uid) LOG("JoinGame", address, asObserver, playerName, uid) - -- if AutolobbyCommunicationsInstance then - -- AutolobbyCommunicationsInstance:JoinGame(address, playerName, uid) - -- end + if AutolobbyCommunicationsInstance then + AutolobbyCommunicationsInstance.JoinParameters = AutolobbyCommunicationsInstance.JoinParameters or {} + AutolobbyCommunicationsInstance.JoinParameters.Address = address + AutolobbyCommunicationsInstance.JoinParameters.AsObserver = asObserver + AutolobbyCommunicationsInstance.JoinParameters.DesiredPlayerName = playerName + AutolobbyCommunicationsInstance.JoinParameters.DesiredPeerId = uid + end -- join over time ForkThread( diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index 8eee7f04f0..1b45b64847 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -64,12 +64,29 @@ local AutolobbyEngineStrings = { ---@alias UIAutolobbyConnections boolean[][] ---@alias UIAutolobbyStatus UIPeerStatus[] +---@class UIAutolobbyParameters +---@field Protocol UILobbyProtocol +---@field LocalPort number +---@field MaxConnections number +---@field DesiredPlayerName string +---@field LocalPlayerPeerId UILobbyPeerId +---@field NatTraversalProvider any + +---@class UIAutolobbyHostParameters +---@field GameName string +---@field ScenarioFile string # path to the _scenario.lua file +---@field SinglePlayer boolean + +---@class UIAutolobbyJoinParameters +---@field Address GPGNetAddress +---@field AsObserver boolean +---@field DesiredPlayerName string +---@field DesiredPeerId UILobbyPeerId --- Responsible for the behavior of the automated lobby. ---@class UIAutolobbyCommunications : moho.lobby_methods, DebugComponent ---@field Trash TrashBag ----@field InterfaceTrash TrashBag ----@field LocalID UILobbyPeerId # a number that is stringified +---@field LocalID UILobbyPeerId # a number that is stringified ---@field LocalPlayerName string # nickname ---@field HostID UILobbyPeerId ---@field PlayerCount number @@ -78,6 +95,9 @@ local AutolobbyEngineStrings = { ---@field PlayerOptions UIAutolobbyPlayer[] # Is synced from the host to the others. ---@field PeerToIndexMapping table ---@field DisconnectedPeers table # +---@field LobbyParameters? UIAutolobbyParameters # Used for rejoining functionality +---@field HostParameters? UIAutolobbyHostParameters # Used for rejoining functionality +---@field JoinParameters? UIAutolobbyJoinParameters # Used for rejoining functionality AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { BackgroundTextures = { From 2ca02b3bfc1d9e9f22a03ccf6f116abece673293 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sun, 20 Oct 2024 08:22:52 +0200 Subject: [PATCH 31/83] Move GpgNetSend statement to the controller --- lua/ui/lobby/autolobby.lua | 35 +++++++++++++++++-- .../lobby/autolobby/AutolobbyController.lua | 10 ++++-- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/lua/ui/lobby/autolobby.lua b/lua/ui/lobby/autolobby.lua index a641791516..38ec79579c 100644 --- a/lua/ui/lobby/autolobby.lua +++ b/lua/ui/lobby/autolobby.lua @@ -119,6 +119,38 @@ function JoinGame(address, asObserver, playerName, uid) if AutolobbyCommunicationsInstance then AutolobbyCommunicationsInstance:JoinGame(address, playerName, uid) end + + if seconds == 2 then + + WaitSeconds(1.0) + + if AutolobbyCommunicationsInstance then + + AutolobbyCommunicationsInstance:Destroy() + + WaitSeconds(1.0) + + LOG("Rejoining...") + + local joinParameters = AutolobbyCommunicationsInstance.JoinParameters + + CreateLobby( + AutolobbyCommunicationsInstance.LobbyParameters.Protocol, + AutolobbyCommunicationsInstance.LobbyParameters.LocalPort, + AutolobbyCommunicationsInstance.LobbyParameters.DesiredPlayerName, + AutolobbyCommunicationsInstance.LobbyParameters.LocalPlayerPeerId, + AutolobbyCommunicationsInstance.LobbyParameters.NatTraversalProvider + ) + + JoinGame( + joinParameters.Address, + joinParameters.AsObserver, + joinParameters.DesiredPlayerName, + joinParameters.DesiredPeerId + ) + end + + end end ) @@ -145,9 +177,6 @@ end function DisconnectFromPeer(uid, doNotUpdateView) LOG("DisconnectFromPeer", uid, doNotUpdateView) - -- inform the server of the event - GpgNetSendDisconnected(uid) - if AutolobbyCommunicationsInstance then AutolobbyCommunicationsInstance:DisconnectFromPeer(uid) end diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index 1b45b64847..ddacd93c75 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -111,7 +111,6 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { ---@param self UIAutolobbyCommunications __init = function(self) self.Trash = TrashBag() - self.InterfaceTrash = self.Trash:Add(TrashBag()) self.LocalID = "-2" self.LocalPlayerName = "Charlie" @@ -402,11 +401,13 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { CheckForLaunch = function(self, peers) -- for debugging :) - -- do return false end + do return false end -- true iff we are connected to all peers local peers = self:GetPeers() + + -- check number of peers if table.getsize(peers) ~= self.PlayerCount - 1 then return false @@ -525,7 +526,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { Destroy = function(self) self:DebugSpew("Destroy") - self.Trash:Destroy() + -- self.Trash:Destroy() return MohoLobbyMethods.Destroy(self) end, @@ -537,6 +538,9 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { DisconnectFromPeer = function(self, peerId) self:DebugSpew("DisconnectFromPeer", peerId) + -- inform the server of the event + GpgNetSendDisconnected(peerId) + -- reset mapping self.PeerToIndexMapping = {} self.DisconnectedPeers[peerId] = GetSystemTimeSeconds() From 9d86ee4f695f9085434bf6262f4c580feb560b8a Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sun, 20 Oct 2024 09:15:34 +0200 Subject: [PATCH 32/83] Create rejoin functionality --- .../lobby/autolobby/AutolobbyController.lua | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index ddacd93c75..1bc14d7541 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -311,6 +311,26 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { PrefetchSession(scenarioFile.map, gameMods, true) end, + ---@param self UIAutolobbyCommunications + ---@param lobbyParameters UIAutolobbyParameters + ---@param joinParameters UIAutolobbyJoinParameters + RejoinThread = function(self, lobbyParameters, joinParameters) + local autolobbyModule = import("/lua/ui/lobby/autolobby.lua") + + WaitSeconds(2.0) + + self:Destroy() + local newLobby = autolobbyModule.CreateLobby( + lobbyParameters.Protocol, + lobbyParameters.LocalPort, + lobbyParameters.DesiredPlayerName, + lobbyParameters.LocalPlayerPeerId, + lobbyParameters.NatTraversalProvider + ) + + autolobbyModule.JoinGame(joinParameters.Address, joinParameters.AsObserver, joinParameters.DesiredPlayerName, joinParameters.DesiredPeerId) + end, + --------------------------------------------------------------------------- --#region Message Handlers -- From f5d430ae68f805d63e3bc18ca2e7698e36a1d956 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sun, 20 Oct 2024 09:17:16 +0200 Subject: [PATCH 33/83] Always blip the interface when a message is received --- lua/ui/lobby/autolobby.lua | 3 +++ .../lobby/autolobby/AutolobbyController.lua | 19 +++++++------------ lua/ui/lobby/autolobby/AutolobbyMessages.lua | 2 +- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/lua/ui/lobby/autolobby.lua b/lua/ui/lobby/autolobby.lua index 38ec79579c..628b398369 100644 --- a/lua/ui/lobby/autolobby.lua +++ b/lua/ui/lobby/autolobby.lua @@ -40,6 +40,7 @@ local AutolobbyCommunicationsInstance = false ---@param desiredPlayerName string ---@param localPlayerUID UILobbyPeerId ---@param natTraversalProvider any +---@return UIAutolobbyCommunications function CreateLobby(protocol, localPort, desiredPlayerName, localPlayerUID, natTraversalProvider) LOG("CreateLobby", protocol, localPort, desiredPlayerName, localPlayerUID, natTraversalProvider) @@ -63,6 +64,8 @@ function CreateLobby(protocol, localPort, desiredPlayerName, localPlayerUID, nat -- create the singleton for the interface local interface = import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() AutolobbyCommunicationsInstance.Trash:Add(interface) + + return AutolobbyCommunicationsInstance end --- Instantiates a lobby instance by hosting one. diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index 1bc14d7541..28f1143a4c 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -339,18 +339,6 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { -- other peers. Validation is done in `AutolobbyMessages` before the message -- processed. - ---@param self UIAutolobbyCommunications - ---@param data UIAutolobbyIsAliveMessage - ProcessIsAliveMessage = function(self, data) - -- update UI for player options - - local peerIndex = self:PeerIdToIndex(data.SenderID) - if peerIndex then - import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() - :UpdateIsAliveStamp(peerIndex) - end - end, - ---@param self UIAutolobbyCommunications ---@param data UIAutolobbyAddPlayerMessage ProcessAddPlayerMessage = function(self, data) @@ -794,6 +782,13 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { ---@type UIAutolobbyMessageHandler? local messageType = AutolobbyMessages[data.Type] + -- signal UI that we received something + local peerIndex = self:PeerIdToIndex(data.SenderID) + if peerIndex then + import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() + :UpdateIsAliveStamp(peerIndex) + end + -- verify that the message type exists if not messageType then self:DebugError('Unknown message received: ', data.Type) diff --git a/lua/ui/lobby/autolobby/AutolobbyMessages.lua b/lua/ui/lobby/autolobby/AutolobbyMessages.lua index 8d8e49d2ea..e3c1820d4c 100644 --- a/lua/ui/lobby/autolobby/AutolobbyMessages.lua +++ b/lua/ui/lobby/autolobby/AutolobbyMessages.lua @@ -59,7 +59,7 @@ AutolobbyMessages = { ---@param lobby UIAutolobbyCommunications ---@param data UIAutolobbyIsAliveMessage Handler = function(lobby, data) - lobby:ProcessIsAliveMessage(data) + -- do nothing, we're interested in the side effect end }, From 07ee763576278d187e3635a64079ff3b0924e165 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sun, 20 Oct 2024 09:23:15 +0200 Subject: [PATCH 34/83] Do not add the interface to the trashbag of the controller This messes up rejoining. --- lua/ui/lobby/autolobby.lua | 2 -- lua/ui/lobby/autolobby/AutolobbyController.lua | 9 +++++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lua/ui/lobby/autolobby.lua b/lua/ui/lobby/autolobby.lua index 628b398369..54e998af7c 100644 --- a/lua/ui/lobby/autolobby.lua +++ b/lua/ui/lobby/autolobby.lua @@ -63,8 +63,6 @@ function CreateLobby(protocol, localPort, desiredPlayerName, localPlayerUID, nat -- create the singleton for the interface local interface = import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() - AutolobbyCommunicationsInstance.Trash:Add(interface) - return AutolobbyCommunicationsInstance end diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index 28f1143a4c..991b39d0bd 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -534,7 +534,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { Destroy = function(self) self:DebugSpew("Destroy") - -- self.Trash:Destroy() + self.Trash:Destroy() return MohoLobbyMethods.Destroy(self) end, @@ -828,8 +828,13 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { GameLaunched = function(self) self:DebugSpew("GameLaunched") - -- GpgNetSend('GameState', 'Launching') + -- clear out the interface + import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton():Destroy() + + -- destroy ourselves, the game takes over the management of peers self:Destroy() + + GpgNetSend('GameState', 'Launching') end, --- Called by the engine when the launch failed. From 6c865fa75f5024ec311a8e91dc0f941d6b8aac41 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sun, 20 Oct 2024 09:29:43 +0200 Subject: [PATCH 35/83] Remove reload testing code --- lua/ui/lobby/autolobby.lua | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/lua/ui/lobby/autolobby.lua b/lua/ui/lobby/autolobby.lua index 54e998af7c..b70234d68c 100644 --- a/lua/ui/lobby/autolobby.lua +++ b/lua/ui/lobby/autolobby.lua @@ -120,38 +120,6 @@ function JoinGame(address, asObserver, playerName, uid) if AutolobbyCommunicationsInstance then AutolobbyCommunicationsInstance:JoinGame(address, playerName, uid) end - - if seconds == 2 then - - WaitSeconds(1.0) - - if AutolobbyCommunicationsInstance then - - AutolobbyCommunicationsInstance:Destroy() - - WaitSeconds(1.0) - - LOG("Rejoining...") - - local joinParameters = AutolobbyCommunicationsInstance.JoinParameters - - CreateLobby( - AutolobbyCommunicationsInstance.LobbyParameters.Protocol, - AutolobbyCommunicationsInstance.LobbyParameters.LocalPort, - AutolobbyCommunicationsInstance.LobbyParameters.DesiredPlayerName, - AutolobbyCommunicationsInstance.LobbyParameters.LocalPlayerPeerId, - AutolobbyCommunicationsInstance.LobbyParameters.NatTraversalProvider - ) - - JoinGame( - joinParameters.Address, - joinParameters.AsObserver, - joinParameters.DesiredPlayerName, - joinParameters.DesiredPeerId - ) - end - - end end ) From 19a3fa7f6e7f86ce1ead175eff7d7c57453a0ce9 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sun, 20 Oct 2024 10:57:55 +0200 Subject: [PATCH 36/83] Make it crash-happy for 4z0t --- lua/ui/lobby/autolobby/AutolobbyController.lua | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index 991b39d0bd..ad26c69224 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -473,9 +473,6 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { ---@param self UIAutolobbyCommunications ConnectionMatrixThread = function(self) - -- right at the start it can be a little jumpy, so we wait a second - WaitSeconds(1) - while not IsDestroyed(self) do local peers = self:GetPeers() @@ -488,7 +485,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() :UpdateConnectionStatuses(statuses) - WaitFrames(10) + WaitFrames(1) end end, From 5e7b21a5ee6feb2260249405feea7cd7dca00a8c Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sun, 20 Oct 2024 10:59:08 +0200 Subject: [PATCH 37/83] expand the matrix --- lua/ui/lobby/autolobby/AutolobbyInterface.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/ui/lobby/autolobby/AutolobbyInterface.lua b/lua/ui/lobby/autolobby/AutolobbyInterface.lua index df04574b53..9657b35a16 100644 --- a/lua/ui/lobby/autolobby/AutolobbyInterface.lua +++ b/lua/ui/lobby/autolobby/AutolobbyInterface.lua @@ -67,7 +67,7 @@ local AutolobbyInterface = Class(Group) { self.Background = UIUtil.CreateBitmap(self, self.BackgroundTextures[math.random(1, 5)]) self.Preview = AutolobbyMapPreview.GetInstance(self) - self.ConnectionMatrix = AutolobbyConnectionMatrix.Create(self, 4) -- TODO: determine this number dynamically + self.ConnectionMatrix = AutolobbyConnectionMatrix.Create(self, 8) -- TODO: determine this number dynamically end, ---@param self UIAutolobbyInterface From e9670d3bc0f5938a71912b512da4fb675516bfae Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Tue, 22 Oct 2024 17:36:08 +0200 Subject: [PATCH 38/83] Introduce sharing of local launch status --- .../AutolobbyConnectionMatrixDot.lua | 20 +- .../lobby/autolobby/AutolobbyController.lua | 270 ++++++++++-------- lua/ui/lobby/autolobby/AutolobbyInterface.lua | 6 +- lua/ui/lobby/autolobby/AutolobbyMessages.lua | 19 ++ 4 files changed, 181 insertions(+), 134 deletions(-) diff --git a/lua/ui/lobby/autolobby/AutolobbyConnectionMatrixDot.lua b/lua/ui/lobby/autolobby/AutolobbyConnectionMatrixDot.lua index 2ca2424f45..7baea1d657 100644 --- a/lua/ui/lobby/autolobby/AutolobbyConnectionMatrixDot.lua +++ b/lua/ui/lobby/autolobby/AutolobbyConnectionMatrixDot.lua @@ -38,7 +38,7 @@ local AutolobbyConnectionMatrixDot = Class(Bitmap) { -- initial state self:SetConnected(false) - self:SetStatus('None') + self:SetSolidColor('ffffff') end, ---@param self UIAutolobbyConnectionMatrixDot @@ -66,22 +66,16 @@ local AutolobbyConnectionMatrixDot = Class(Bitmap) { end, ---@param self UIAutolobbyConnectionMatrixDot - ---@param status UIPeerStatus + ---@param status UIAutolobbyLaunchStatus SetStatus = function(self, status) - if status == 'None' then - self:SetSolidColor(EnumColors.AliceBlue) - elseif status == 'Pending' then + if status == 'Missing local peers' then + self:SetSolidColor(EnumColors.Orange) + elseif status == 'Not all local peers are established' then self:SetSolidColor(EnumColors.Yellow) - elseif status == 'Connecting' then + elseif status == 'Not all peers are connected' then self:SetSolidColor(EnumColors.YellowGreen) - elseif status == 'Answering' then - self:SetSolidColor(EnumColors.YellowGreen) - elseif status == 'Established' then + elseif status == 'Ready for launch' then self:SetSolidColor(EnumColors.Green) - elseif status == 'TimedOut' then - self:SetSolidColor(EnumColors.Red) - elseif status == 'Errored' then - self:SetSolidColor(EnumColors.Crimson) end end, diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index ad26c69224..e56ba6514e 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -52,6 +52,12 @@ local AutolobbyEngineStrings = { ['LaunchRejected'] = "Some players are using an incompatible client version.", } +---@alias UIAutolobbyLaunchStatus +--- | 'Missing local peers' +--- | 'Not all local peers are established' +--- | 'Not all peers are connected' +--- | 'Ready for launch' + ---@class UIAutolobbyPlayer: UILobbyLaunchPlayerConfiguration ---@field StartSpot number ---@field DEV number # Related to rating/divisions @@ -62,10 +68,10 @@ local AutolobbyEngineStrings = { ---@field PL number # Related to rating/divisions ---@alias UIAutolobbyConnections boolean[][] ----@alias UIAutolobbyStatus UIPeerStatus[] +---@alias UIAutolobbyStatus UIAutolobbyLaunchStatus[] ---@class UIAutolobbyParameters ----@field Protocol UILobbyProtocol +---@field Protocol UILobbyProtocol ---@field LocalPort number ---@field MaxConnections number ---@field DesiredPlayerName string @@ -86,7 +92,7 @@ local AutolobbyEngineStrings = { --- Responsible for the behavior of the automated lobby. ---@class UIAutolobbyCommunications : moho.lobby_methods, DebugComponent ---@field Trash TrashBag ----@field LocalID UILobbyPeerId # a number that is stringified +---@field LocalPeerId UILobbyPeerId # a number that is stringified ---@field LocalPlayerName string # nickname ---@field HostID UILobbyPeerId ---@field PlayerCount number @@ -95,6 +101,7 @@ local AutolobbyEngineStrings = { ---@field PlayerOptions UIAutolobbyPlayer[] # Is synced from the host to the others. ---@field PeerToIndexMapping table ---@field DisconnectedPeers table # +---@field LaunchStatutes table ---@field LobbyParameters? UIAutolobbyParameters # Used for rejoining functionality ---@field HostParameters? UIAutolobbyHostParameters # Used for rejoining functionality ---@field JoinParameters? UIAutolobbyJoinParameters # Used for rejoining functionality @@ -112,7 +119,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { __init = function(self) self.Trash = TrashBag() - self.LocalID = "-2" + self.LocalPeerId = "-2" self.LocalPlayerName = "Charlie" self.PlayerCount = tonumber(GetCommandLineArg("/players", 1)[1]) or 2 self.Connections = {} @@ -123,6 +130,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { self.PlayerOptions = {} self.PeerToIndexMapping = {} self.DisconnectedPeers = {} + self.LaunchStatutes = {} end, ---@param self UIAutolobbyCommunications @@ -246,23 +254,73 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { return connections end, + ---@param self UIAutolobbyCommunications + ---@param statuses table + ---@return UIAutolobbyStatus + CreateConnectionStatuses = function(self, statuses) + local output = {} + for peerId, launchStatus in statuses do + local peerIdNumber = self:PeerIdToIndex(peerId) + if peerIdNumber then + output[peerIdNumber] = launchStatus + end + end + + return output + end, + + --- Determines the launch status of the local peer. ---@param self UIAutolobbyCommunications ---@param peers Peer[] - ---@return UIPeerStatus[] - CreateConnectionStatuses = function(self, peers) - local statuses = {} - for k = 1, self.PlayerCount do - statuses[k] = 'None' + ---@return UIAutolobbyLaunchStatus + CreateLaunchStatus = function(self, peers) + + -- check number of peers + local validPeerCount = self.PlayerCount - 1 + if table.getsize(peers) < validPeerCount then + return 'Missing local peers' + end + + -- check number of established peers + local establishedPeerCount = 0 + for k, peer in peers do + if peer.status == "Established" then + establishedPeerCount = establishedPeerCount + 1 + end + end + + if establishedPeerCount < validPeerCount then + return 'Not all local peers are established' end + -- check confirmed established connections of peers for _, peer in peers do - local peerIdNumber = self:PeerIdToIndex(peer.id) - if peerIdNumber then - statuses[peerIdNumber] = peer.status + if table.getsize(peer.establishedPeers) ~= self.PlayerCount - 1 then + return 'Not all peers are connected' end end - return statuses + return 'Ready for launch' + end, + + --- Verifies whether we can launch the game. + ---@param self UIAutolobbyCommunications + ---@param peerStatus UIAutolobbyStatus + ---@return boolean + CanLaunch = function(self, peerStatus) + -- check if we know of all peers + if table.getsize(peerStatus) ~= self.PlayerCount then + return false + end + + -- check if all peers are ready for launch + for k, launchStatus in peerStatus do + if launchStatus ~= 'Ready for launch' then + return false + end + end + + return true end, --- Maps a peer id to an index that can be used in the interface. In @@ -328,9 +386,74 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { lobbyParameters.NatTraversalProvider ) - autolobbyModule.JoinGame(joinParameters.Address, joinParameters.AsObserver, joinParameters.DesiredPlayerName, joinParameters.DesiredPeerId) + autolobbyModule.JoinGame(joinParameters.Address, joinParameters.AsObserver, joinParameters.DesiredPlayerName, + joinParameters.DesiredPeerId) + end, + + + --------------------------------------------------------------------------- + --#region Threads + + --- Passes the local launch status to all peers. + ---@param self UIAutolobbyCommunications + ShareLaunchStatusThread = function(self) + while not IsDestroyed(self) do + local peers = self:GetPeers() + local launchStatus = self:CreateLaunchStatus(peers) + self.LaunchStatutes[self.LocalPeerId] = launchStatus + self:BroadcastData({ Type = "UpdateLaunchStatus", LaunchStatus = launchStatus }) + + -- update UI for launch statuses + import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() + :UpdateLaunchStatuses(self:CreateConnectionStatuses(self.LaunchStatutes)) + + WaitSeconds(0.5) + end + end, + + ---@param self UIAutolobbyCommunications + LaunchThread = function(self) + while not IsDestroyed(self) do + + do return end + + if self:CanLaunch(self.LaunchStatutes) then + + WaitSeconds(5.0) + if (not IsDestroyed(self)) and self:CanLaunch(self.LaunchStatutes) then + local gameConfiguration = { + GameMods = self.GameMods, + GameOptions = self.GameOptions, + PlayerOptions = self.PlayerOptions, + Observers = {}, + } + + self:BroadcastData({ Type = "Launch", GameConfig = gameConfiguration }) + self:LaunchGame(gameConfiguration) + end + end + + WaitSeconds(1.0) + end + end, + + ---@param self UIAutolobbyCommunications + ConnectionMatrixThread = function(self) + WaitSeconds(1.0) + + while not IsDestroyed(self) do + local peers = self:GetPeers() + + local connections = self:CreateConnectionsMatrix(peers) + import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() + :UpdateConnections(connections) + + WaitFrames(10) + end end, + --#endregion + --------------------------------------------------------------------------- --#region Message Handlers -- @@ -388,105 +511,14 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { self:LaunchGame(data.GameConfig) end, - --#endregion - - --------------------------------------------------------------------------- - --#region Threads - - --- A thread to indicate that we're still around. Various properties such as ping are not updated - --- until a message is received. This thread introduces occasional traffic between players. - ---@param self UIAutolobbyCommunications - IsAliveThread = function(self) - while not IsDestroyed(self) do - self:BroadcastData({ Type = "IsAlive" }) - WaitSeconds(0.5) - end - end, - - ---@param self any - ---@param peers any - ---@return boolean - CheckForLaunch = function(self, peers) - - -- for debugging :) - do return false end - - -- true iff we are connected to all peers - local peers = self:GetPeers() - - - - -- check number of peers - if table.getsize(peers) ~= self.PlayerCount - 1 then - return false - end - - -- check connection status - for k, peer in peers do - if peer.status ~= "Established" then - return false - end - end - - -- check confirmed established connections of peers - for _, peer in peers do - if table.getsize(peer.establishedPeers) ~= self.PlayerCount - 1 then - return false - end - end - - return true - end, - - ---@param self UIAutolobbyCommunications - CheckForLaunchThread = function(self) - - while not IsDestroyed(self) do - - local peers = self:GetPeers() - local canLaunch = self:CheckForLaunch(peers) - - LOG("CanLaunch", canLaunch) - if canLaunch then - ---@type UILobbyLaunchConfiguration - local gameConfiguration = { - GameMods = self.GameMods, - GameOptions = self.GameOptions, - PlayerOptions = self.PlayerOptions, - Observers = {}, - } - - -- delay slightly - WaitSeconds(5) - - -- check again and if still good, we launch - local peers = self:GetPeers() - if self:CheckForLaunch(peers) then - self:BroadcastData({ Type = "Launch", GameConfig = gameConfiguration }) - self:LaunchGame(gameConfiguration) - end - end - - WaitSeconds(5.0) - end - end, - ---@param self UIAutolobbyCommunications - ConnectionMatrixThread = function(self) - while not IsDestroyed(self) do - local peers = self:GetPeers() + ---@param data UIAutolobbyUpdateLaunchStatusMessage + ProcessUpdateLaunchStatusMessage = function(self, data) + self.LaunchStatutes[data.SenderID] = data.LaunchStatus - local connections = self:CreateConnectionsMatrix(peers) - local statuses = self:CreateConnectionStatuses(peers) - - import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() - :UpdateConnections(connections) - - import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() - :UpdateConnectionStatuses(statuses) - - WaitFrames(1) - end + -- update UI for launch statuses + import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() + :UpdateLaunchStatuses(self:CreateConnectionStatuses(self.LaunchStatutes)) end, --#endregion @@ -548,6 +580,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { -- reset mapping self.PeerToIndexMapping = {} + self.LaunchStatutes = {} self.DisconnectedPeers[peerId] = GetSystemTimeSeconds() return MohoLobbyMethods.DisconnectFromPeer(self, peerId) @@ -684,20 +717,20 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { Hosting = function(self) self:DebugSpew("Hosting") - self.LocalID = self:GetLocalPlayerID() + self.LocalPeerId = self:GetLocalPlayerID() self.LocalPlayerName = self:GetLocalPlayerName() self.HostID = self:GetLocalPlayerID() -- give ourself a seat at the table local hostPlayerOptions = self:CreateLocalPlayer() - hostPlayerOptions.OwnerID = self.LocalID - hostPlayerOptions.PlayerName = self:MakeValidPlayerName(self.LocalID, self.LocalPlayerName) + hostPlayerOptions.OwnerID = self.LocalPeerId + hostPlayerOptions.PlayerName = self:MakeValidPlayerName(self.LocalPeerId, self.LocalPlayerName) self.PlayerOptions[hostPlayerOptions.StartSpot] = hostPlayerOptions -- occasionally send data over the network to create pings on screen - self.Trash:Add(ForkThread(self.IsAliveThread, self)) self.Trash:Add(ForkThread(self.ConnectionMatrixThread, self)) - self.Trash:Add(ForkThread(self.CheckForLaunchThread, self)) + self.Trash:Add(ForkThread(self.ShareLaunchStatusThread, self)) + self.Trash:Add(ForkThread(self.LaunchThread, self)) -- start prefetching the scenario self:Prefetch(self.GameOptions, self.GameMods) @@ -729,14 +762,14 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { ConnectionToHostEstablished = function(self, localPeerId, newLocalName, hostPeerId) self:DebugSpew("ConnectionToHostEstablished", localPeerId, newLocalName, hostPeerId) self.LocalPlayerName = newLocalName - self.LocalID = localPeerId + self.LocalPeerId = localPeerId self.HostID = hostPeerId GpgNetSendGameState('Lobby') -- occasionally send data over the network to create pings on screen - self.Trash:Add(ForkThread(self.IsAliveThread, self)) self.Trash:Add(ForkThread(self.ConnectionMatrixThread, self)) + self.Trash:Add(ForkThread(self.ShareLaunchStatusThread, self)) self:SendData(self.HostID, { Type = "AddPlayer", PlayerOptions = self:CreateLocalPlayer() }) end, @@ -817,6 +850,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { -- reset mapping self.PeerToIndexMapping = {} + self.LaunchStatutes = {} self.DisconnectedPeers[peerId] = GetSystemTimeSeconds() end, diff --git a/lua/ui/lobby/autolobby/AutolobbyInterface.lua b/lua/ui/lobby/autolobby/AutolobbyInterface.lua index 9657b35a16..219f57009b 100644 --- a/lua/ui/lobby/autolobby/AutolobbyInterface.lua +++ b/lua/ui/lobby/autolobby/AutolobbyInterface.lua @@ -67,7 +67,7 @@ local AutolobbyInterface = Class(Group) { self.Background = UIUtil.CreateBitmap(self, self.BackgroundTextures[math.random(1, 5)]) self.Preview = AutolobbyMapPreview.GetInstance(self) - self.ConnectionMatrix = AutolobbyConnectionMatrix.Create(self, 8) -- TODO: determine this number dynamically + self.ConnectionMatrix = AutolobbyConnectionMatrix.Create(self, 4) -- TODO: determine this number dynamically end, ---@param self UIAutolobbyInterface @@ -105,7 +105,7 @@ local AutolobbyInterface = Class(Group) { ---@param self UIAutolobbyInterface ---@param statuses UIAutolobbyStatus - UpdateConnectionStatuses = function(self, statuses) + UpdateLaunchStatuses = function(self, statuses) self.State.Statuses = statuses self.ConnectionMatrix:Show() @@ -167,7 +167,7 @@ local AutolobbyInterface = Class(Group) { end if state.Statuses then - local ok, msg = pcall(self.UpdateConnectionStatuses, self, state.Statuses) + local ok, msg = pcall(self.UpdateLaunchStatuses, self, state.Statuses) if not ok then WARN(msg) end diff --git a/lua/ui/lobby/autolobby/AutolobbyMessages.lua b/lua/ui/lobby/autolobby/AutolobbyMessages.lua index e3c1820d4c..348891cb3c 100644 --- a/lua/ui/lobby/autolobby/AutolobbyMessages.lua +++ b/lua/ui/lobby/autolobby/AutolobbyMessages.lua @@ -63,6 +63,25 @@ AutolobbyMessages = { end }, + UpdateLaunchStatus = { + ---@class UIAutolobbyUpdateLaunchStatusMessage : UILobbyReceivedMessage + ---@field LaunchStatus UIAutolobbyLaunchStatus + + ---@param lobby UIAutolobbyCommunications + ---@param data UIAutolobbyUpdateLaunchStatusMessage + ---@return boolean + Accept = function(lobby, data) + return true + end, + + ---@param lobby UIAutolobbyCommunications + ---@param data UIAutolobbyUpdateLaunchStatusMessage + Handler = function(lobby, data) + lobby:ProcessUpdateLaunchStatusMessage(data) + end + }, + + AddPlayer = { ---@class UIAutolobbyAddPlayerMessage : UILobbyReceivedMessage From a64bba3278ef061a6c20e51f5009b76fb54ddbb5 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Tue, 22 Oct 2024 17:36:24 +0200 Subject: [PATCH 39/83] Set depth of `reprsl` to 2 by default --- lua/system/repr.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/system/repr.lua b/lua/system/repr.lua index b774b56711..693f29c993 100644 --- a/lua/system/repr.lua +++ b/lua/system/repr.lua @@ -260,7 +260,7 @@ end local function inspect(root, options) options = options or {} - local depth = options.depth or 1 + local depth = options.depth or 2 local newline = options.newline or '\n' local indent = options.indent or ' ' local meta = options.meta or false From 360fe25fe3b295e472d3e99f142c5414c35012c3 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Tue, 22 Oct 2024 18:52:48 +0200 Subject: [PATCH 40/83] Rename to 'Ready' instead of 'Ready for launch' --- lua/ui/lobby/autolobby.lua | 2 +- lua/ui/lobby/autolobby/AutolobbyController.lua | 17 ++++++++++------- lua/ui/lobby/autolobby/AutolobbyInterface.lua | 2 +- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/lua/ui/lobby/autolobby.lua b/lua/ui/lobby/autolobby.lua index b70234d68c..7d4cb8da4e 100644 --- a/lua/ui/lobby/autolobby.lua +++ b/lua/ui/lobby/autolobby.lua @@ -115,7 +115,7 @@ function JoinGame(address, asObserver, playerName, uid) ForkThread( function() local seconds = tonumber(GetCommandLineArg("/startspot", 1)[1]) or 1 - WaitSeconds(seconds) + WaitSeconds(0.25 * seconds) if AutolobbyCommunicationsInstance then AutolobbyCommunicationsInstance:JoinGame(address, playerName, uid) diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index e56ba6514e..d6ea08822e 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -56,7 +56,7 @@ local AutolobbyEngineStrings = { --- | 'Missing local peers' --- | 'Not all local peers are established' --- | 'Not all peers are connected' ---- | 'Ready for launch' +--- | 'Ready' ---@class UIAutolobbyPlayer: UILobbyLaunchPlayerConfiguration ---@field StartSpot number @@ -300,7 +300,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { end end - return 'Ready for launch' + return 'Ready' end, --- Verifies whether we can launch the game. @@ -315,7 +315,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { -- check if all peers are ready for launch for k, launchStatus in peerStatus do - if launchStatus ~= 'Ready for launch' then + if launchStatus ~= 'Ready' then return false end end @@ -356,6 +356,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { return index end, + --- Prefetches a scenario to try and reduce the loading screen time. ---@param self UIAutolobbyCommunications ---@param gameOptions UILobbyLaunchGameOptionsConfiguration ---@param gameMods UILobbyLaunchGameModsConfiguration[] @@ -397,6 +398,8 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { --- Passes the local launch status to all peers. ---@param self UIAutolobbyCommunications ShareLaunchStatusThread = function(self) + WaitSeconds(1.0) + while not IsDestroyed(self) do local peers = self:GetPeers() local launchStatus = self:CreateLaunchStatus(peers) @@ -442,11 +445,11 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { WaitSeconds(1.0) while not IsDestroyed(self) do - local peers = self:GetPeers() + -- local peers = self:GetPeers() - local connections = self:CreateConnectionsMatrix(peers) - import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() - :UpdateConnections(connections) + -- local connections = self:CreateConnectionsMatrix(peers) + -- import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() + -- :UpdateConnections(connections) WaitFrames(10) end diff --git a/lua/ui/lobby/autolobby/AutolobbyInterface.lua b/lua/ui/lobby/autolobby/AutolobbyInterface.lua index 219f57009b..cba974339b 100644 --- a/lua/ui/lobby/autolobby/AutolobbyInterface.lua +++ b/lua/ui/lobby/autolobby/AutolobbyInterface.lua @@ -67,7 +67,7 @@ local AutolobbyInterface = Class(Group) { self.Background = UIUtil.CreateBitmap(self, self.BackgroundTextures[math.random(1, 5)]) self.Preview = AutolobbyMapPreview.GetInstance(self) - self.ConnectionMatrix = AutolobbyConnectionMatrix.Create(self, 4) -- TODO: determine this number dynamically + self.ConnectionMatrix = AutolobbyConnectionMatrix.Create(self, 6) -- TODO: determine this number dynamically end, ---@param self UIAutolobbyInterface From 5823e21134fb41c2d86101545593927f1f1eebae Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Tue, 22 Oct 2024 19:36:01 +0200 Subject: [PATCH 41/83] Setup with working reconnect Includes a test where peer 2 automatically loses connection to the host --- lua/ui/lobby/autolobby.lua | 18 ++- .../AutolobbyConnectionMatrixDot.lua | 6 +- .../lobby/autolobby/AutolobbyController.lua | 111 +++++++++++++----- lua/ui/lobby/autolobby/AutolobbyInterface.lua | 36 ++++-- 4 files changed, 129 insertions(+), 42 deletions(-) diff --git a/lua/ui/lobby/autolobby.lua b/lua/ui/lobby/autolobby.lua index 7d4cb8da4e..a97da1e315 100644 --- a/lua/ui/lobby/autolobby.lua +++ b/lua/ui/lobby/autolobby.lua @@ -44,6 +44,11 @@ local AutolobbyCommunicationsInstance = false function CreateLobby(protocol, localPort, desiredPlayerName, localPlayerUID, natTraversalProvider) LOG("CreateLobby", protocol, localPort, desiredPlayerName, localPlayerUID, natTraversalProvider) + -- create the interface, needs to be done before the lobby is + local playerCount = tonumber(GetCommandLineArg("/players", 1)[1]) or 8 + local interface = import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").SetupSingleton(playerCount) + + -- create the lobby local maxConnections = 16 AutolobbyCommunicationsInstance = InternalCreateLobby( import("/lua/ui/lobby/autolobby/AutolobbyController.lua").AutolobbyCommunications, @@ -61,8 +66,6 @@ function CreateLobby(protocol, localPort, desiredPlayerName, localPlayerUID, nat GpgNetSendGameState('Idle') - -- create the singleton for the interface - local interface = import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() return AutolobbyCommunicationsInstance end @@ -93,6 +96,8 @@ function HostGame(gameName, scenarioFileName, singlePlayer) -- :CreateLoadingDialog() end +local rejoinTest = true + --- Joins an instantiated lobby instance. --- --- Assumes that the lobby communications is initialized by calling `CreateLobby`. @@ -120,6 +125,15 @@ function JoinGame(address, asObserver, playerName, uid) if AutolobbyCommunicationsInstance then AutolobbyCommunicationsInstance:JoinGame(address, playerName, uid) end + + if rejoinTest and seconds == 2 then + rejoinTest = false + + WaitSeconds(4) + if AutolobbyCommunicationsInstance then + AutolobbyCommunicationsInstance:DisconnectFromPeer("0") + end + end end ) diff --git a/lua/ui/lobby/autolobby/AutolobbyConnectionMatrixDot.lua b/lua/ui/lobby/autolobby/AutolobbyConnectionMatrixDot.lua index 7baea1d657..7a329fcaaf 100644 --- a/lua/ui/lobby/autolobby/AutolobbyConnectionMatrixDot.lua +++ b/lua/ui/lobby/autolobby/AutolobbyConnectionMatrixDot.lua @@ -68,13 +68,15 @@ local AutolobbyConnectionMatrixDot = Class(Bitmap) { ---@param self UIAutolobbyConnectionMatrixDot ---@param status UIAutolobbyLaunchStatus SetStatus = function(self, status) - if status == 'Missing local peers' then + if status == 'Rejoining' then + self:SetSolidColor(EnumColors.HotPink) + elseif status == 'Missing local peers' then self:SetSolidColor(EnumColors.Orange) elseif status == 'Not all local peers are established' then self:SetSolidColor(EnumColors.Yellow) elseif status == 'Not all peers are connected' then self:SetSolidColor(EnumColors.YellowGreen) - elseif status == 'Ready for launch' then + elseif status == 'Ready' then self:SetSolidColor(EnumColors.Green) end end, diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index d6ea08822e..6c019f44b8 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -56,6 +56,7 @@ local AutolobbyEngineStrings = { --- | 'Missing local peers' --- | 'Not all local peers are established' --- | 'Not all peers are connected' +--- | 'Rejoining' --- | 'Ready' ---@class UIAutolobbyPlayer: UILobbyLaunchPlayerConfiguration @@ -373,28 +374,81 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { ---@param self UIAutolobbyCommunications ---@param lobbyParameters UIAutolobbyParameters ---@param joinParameters UIAutolobbyJoinParameters - RejoinThread = function(self, lobbyParameters, joinParameters) + Rejoin = function(self, lobbyParameters, joinParameters) local autolobbyModule = import("/lua/ui/lobby/autolobby.lua") - WaitSeconds(2.0) + LOG("Rejoining!") - self:Destroy() - local newLobby = autolobbyModule.CreateLobby( - lobbyParameters.Protocol, - lobbyParameters.LocalPort, - lobbyParameters.DesiredPlayerName, - lobbyParameters.LocalPlayerPeerId, - lobbyParameters.NatTraversalProvider - ) + -- start disposing threads to prevent race conditions + self.Trash:Destroy() - autolobbyModule.JoinGame(joinParameters.Address, joinParameters.AsObserver, joinParameters.DesiredPlayerName, - joinParameters.DesiredPeerId) + ForkThread( + function() + -- give time for any messages that are already send + WaitSeconds(0.5) + + -- inform peers that we're rejoining + self:BroadcastData({ Type = "UpdateLaunchStatus", LaunchStatus = 'Rejoining' }) + + -- give the message time to arrive + WaitSeconds(0.5) + + -- replace ourselves with the new lobby and rejoin + self:Destroy() + local newLobby = autolobbyModule.CreateLobby( + lobbyParameters.Protocol, + lobbyParameters.LocalPort, + lobbyParameters.DesiredPlayerName, + lobbyParameters.LocalPlayerPeerId, + lobbyParameters.NatTraversalProvider + ) + + autolobbyModule.JoinGame(joinParameters.Address, joinParameters.AsObserver, + joinParameters.DesiredPlayerName, + joinParameters.DesiredPeerId) + end + ) end, --------------------------------------------------------------------------- --#region Threads + ---@param self UIAutolobbyCommunications + CheckForRejoinThread = function(self) + + local rejoinThreshold = 3 + local rejoinCount = 0 + + while not IsDestroyed(self) do + + -- check if we're ready to launch + if self.LaunchStatutes[self.LocalPeerId] ~= 'Ready' then + + -- if we're not, check if a peer is ready to launch + local onePeerIsReady = false + for k, launchStatus in self.LaunchStatutes do + if launchStatus == 'Ready' then + onePeerIsReady = true + end + end + + if onePeerIsReady then + rejoinCount = rejoinCount + 1 + end + else + rejoinCount = 0 + end + + -- if we reached the threshold, time to rejoin! + if rejoinCount > rejoinThreshold then + self:Rejoin(self.LobbyParameters, self.JoinParameters) + end + + WaitSeconds(1.0) + end + end, + --- Passes the local launch status to all peers. ---@param self UIAutolobbyCommunications ShareLaunchStatusThread = function(self) @@ -418,8 +472,6 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { LaunchThread = function(self) while not IsDestroyed(self) do - do return end - if self:CanLaunch(self.LaunchStatutes) then WaitSeconds(5.0) @@ -440,21 +492,6 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { end end, - ---@param self UIAutolobbyCommunications - ConnectionMatrixThread = function(self) - WaitSeconds(1.0) - - while not IsDestroyed(self) do - -- local peers = self:GetPeers() - - -- local connections = self:CreateConnectionsMatrix(peers) - -- import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() - -- :UpdateConnections(connections) - - WaitFrames(10) - end - end, - --#endregion --------------------------------------------------------------------------- @@ -731,7 +768,6 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { self.PlayerOptions[hostPlayerOptions.StartSpot] = hostPlayerOptions -- occasionally send data over the network to create pings on screen - self.Trash:Add(ForkThread(self.ConnectionMatrixThread, self)) self.Trash:Add(ForkThread(self.ShareLaunchStatusThread, self)) self.Trash:Add(ForkThread(self.LaunchThread, self)) @@ -756,6 +792,9 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { ---@param reason string # reason for connection failure, populated by the engine ConnectionFailed = function(self, reason) self:DebugSpew("ConnectionFailed", reason) + + -- try to rejoin + -- self:Rejoin(self.LobbyParameters, self.JoinParameters) end, --- Called by the engine when the connection succeeds with the host. @@ -771,8 +810,8 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { GpgNetSendGameState('Lobby') -- occasionally send data over the network to create pings on screen - self.Trash:Add(ForkThread(self.ConnectionMatrixThread, self)) self.Trash:Add(ForkThread(self.ShareLaunchStatusThread, self)) + self.Trash:Add(ForkThread(self.CheckForRejoinThread, self)) self:SendData(self.HostID, { Type = "AddPlayer", PlayerOptions = self:CreateLocalPlayer() }) end, @@ -783,6 +822,16 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { ---@param peerConnectedTo UILobbyPeerId[] # all established conenctions for the given player EstablishedPeers = function(self, peerId, peerConnectedTo) self:DebugSpew("EstablishedPeers", peerId, reprs(peerConnectedTo)) + + ForkThread( + function() + WaitFrames(4) + local peers = self:GetPeers() + local connections = self:CreateConnectionsMatrix(peers) + import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() + :UpdateConnections(connections) + end + ) end, --#endregion diff --git a/lua/ui/lobby/autolobby/AutolobbyInterface.lua b/lua/ui/lobby/autolobby/AutolobbyInterface.lua index cba974339b..180be71459 100644 --- a/lua/ui/lobby/autolobby/AutolobbyInterface.lua +++ b/lua/ui/lobby/autolobby/AutolobbyInterface.lua @@ -36,6 +36,7 @@ local AutolobbyMapPreview = import("/lua/ui/lobby/autolobby/AutolobbyMapPreview. local AutolobbyConnectionMatrix = import("/lua/ui/lobby/autolobby/AutolobbyConnectionMatrix.lua") ---@class UIAutolobbyInterfaceState +---@field PlayerCount number ---@field PlayerOptions? table ---@field GameOptions? UILobbyLaunchGameOptionsConfiguration ---@field Connections? UIAutolobbyConnections @@ -59,15 +60,17 @@ local AutolobbyInterface = Class(Group) { ---@param self UIAutolobbyInterface ---@param parent Control - __init = function(self, parent) + __init = function(self, parent, playerCount) Group.__init(self, parent, "AutolobbyInterface") -- initial, empty state - self.State = {} + self.State = { + PlayerCount = playerCount + } self.Background = UIUtil.CreateBitmap(self, self.BackgroundTextures[math.random(1, 5)]) self.Preview = AutolobbyMapPreview.GetInstance(self) - self.ConnectionMatrix = AutolobbyConnectionMatrix.Create(self, 6) -- TODO: determine this number dynamically + self.ConnectionMatrix = AutolobbyConnectionMatrix.Create(self, playerCount) -- TODO: determine this number dynamically end, ---@param self UIAutolobbyInterface @@ -98,7 +101,7 @@ local AutolobbyInterface = Class(Group) { ---@param connections UIAutolobbyConnections UpdateConnections = function(self, connections) self.State.Connections = connections - + self.ConnectionMatrix:Show() self.ConnectionMatrix:UpdateConnections(connections) end, @@ -183,13 +186,32 @@ local ModuleTrash = TrashBag() ---@type UIAutolobbyInterface | false local AutolobbyInterfaceInstance = false +---@param playerCount? number ---@return UIAutolobbyInterface -GetSingleton = function() +GetSingleton = function(playerCount) if AutolobbyInterfaceInstance then return AutolobbyInterfaceInstance end - AutolobbyInterfaceInstance = AutolobbyInterface(GetFrame(0)) + -- default + playerCount = playerCount or 8 + + AutolobbyInterfaceInstance = AutolobbyInterface(GetFrame(0), playerCount) + ModuleTrash:Add(AutolobbyInterfaceInstance) + return AutolobbyInterfaceInstance +end + +---@param playerCount? number +---@return UIAutolobbyInterface +SetupSingleton = function(playerCount) + if AutolobbyInterfaceInstance then + AutolobbyInterfaceInstance:Destroy() + end + + -- default + playerCount = playerCount or tonumber(GetCommandLineArg("/players", 1)[1]) or 8 + + AutolobbyInterfaceInstance = AutolobbyInterface(GetFrame(0), playerCount) ModuleTrash:Add(AutolobbyInterfaceInstance) return AutolobbyInterfaceInstance end @@ -201,7 +223,7 @@ end ---@param newModule any function __moduleinfo.OnReload(newModule) if AutolobbyInterfaceInstance then - local handle = newModule.GetSingleton(GetFrame(0)) + local handle = newModule.SetupSingleton(GetFrame(0), AutolobbyInterfaceInstance.State.PlayerCount) handle:RestoreState(AutolobbyInterfaceInstance.State) end end From 0acd515ce441f88480ab1304929a2dda853ca1e4 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Tue, 22 Oct 2024 19:39:13 +0200 Subject: [PATCH 42/83] Remove LOG statement --- lua/ui/lobby/autolobby/AutolobbyController.lua | 2 -- 1 file changed, 2 deletions(-) diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index 6c019f44b8..0b0a2f85bc 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -377,8 +377,6 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { Rejoin = function(self, lobbyParameters, joinParameters) local autolobbyModule = import("/lua/ui/lobby/autolobby.lua") - LOG("Rejoining!") - -- start disposing threads to prevent race conditions self.Trash:Destroy() From e3ca5a9fac7fcb232a2e5137181d2d3e64515cce Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Tue, 22 Oct 2024 21:04:44 +0200 Subject: [PATCH 43/83] Various tweaks all over --- lua/ui/lobby/autolobby.lua | 17 +-- .../AutolobbyConnectionMatrixDot.lua | 8 +- .../lobby/autolobby/AutolobbyController.lua | 127 ++++++++++-------- 3 files changed, 81 insertions(+), 71 deletions(-) diff --git a/lua/ui/lobby/autolobby.lua b/lua/ui/lobby/autolobby.lua index a97da1e315..c8fe648bd6 100644 --- a/lua/ui/lobby/autolobby.lua +++ b/lua/ui/lobby/autolobby.lua @@ -116,22 +116,23 @@ function JoinGame(address, asObserver, playerName, uid) AutolobbyCommunicationsInstance.JoinParameters.DesiredPeerId = uid end + if AutolobbyCommunicationsInstance then + AutolobbyCommunicationsInstance:JoinGame(address, playerName, uid) + end + + -- join over time ForkThread( function() - local seconds = tonumber(GetCommandLineArg("/startspot", 1)[1]) or 1 - WaitSeconds(0.25 * seconds) - - if AutolobbyCommunicationsInstance then - AutolobbyCommunicationsInstance:JoinGame(address, playerName, uid) - end + local startSpot = tonumber(GetCommandLineArg("/startspot", 1)[1]) or 1 - if rejoinTest and seconds == 2 then + if rejoinTest and startSpot == 2 then rejoinTest = false WaitSeconds(4) + LOG(" -- REJOIN TEST -- ") if AutolobbyCommunicationsInstance then - AutolobbyCommunicationsInstance:DisconnectFromPeer("0") + AutolobbyCommunicationsInstance:DisconnectFromPeer("3") end end end diff --git a/lua/ui/lobby/autolobby/AutolobbyConnectionMatrixDot.lua b/lua/ui/lobby/autolobby/AutolobbyConnectionMatrixDot.lua index 7a329fcaaf..5fecf7bcd3 100644 --- a/lua/ui/lobby/autolobby/AutolobbyConnectionMatrixDot.lua +++ b/lua/ui/lobby/autolobby/AutolobbyConnectionMatrixDot.lua @@ -68,14 +68,12 @@ local AutolobbyConnectionMatrixDot = Class(Bitmap) { ---@param self UIAutolobbyConnectionMatrixDot ---@param status UIAutolobbyLaunchStatus SetStatus = function(self, status) - if status == 'Rejoining' then + if status == 'Unknown' then + self:SetSolidColor(EnumColors.Blue) + elseif status == 'Rejoining' then self:SetSolidColor(EnumColors.HotPink) elseif status == 'Missing local peers' then self:SetSolidColor(EnumColors.Orange) - elseif status == 'Not all local peers are established' then - self:SetSolidColor(EnumColors.Yellow) - elseif status == 'Not all peers are connected' then - self:SetSolidColor(EnumColors.YellowGreen) elseif status == 'Ready' then self:SetSolidColor(EnumColors.Green) end diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index 0b0a2f85bc..0d8428ac3e 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -53,9 +53,9 @@ local AutolobbyEngineStrings = { } ---@alias UIAutolobbyLaunchStatus +--- | 'Disconnected' +--- | 'Unknown' --- | 'Missing local peers' ---- | 'Not all local peers are established' ---- | 'Not all peers are connected' --- | 'Rejoining' --- | 'Ready' @@ -100,6 +100,7 @@ local AutolobbyEngineStrings = { ---@field GameMods UILobbyLaunchGameModsConfiguration[] ---@field GameOptions UILobbyLaunchGameOptionsConfiguration # Is synced from the host to the others. ---@field PlayerOptions UIAutolobbyPlayer[] # Is synced from the host to the others. +---@field ConnectionMatrix table ---@field PeerToIndexMapping table ---@field DisconnectedPeers table # ---@field LaunchStatutes table @@ -132,6 +133,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { self.PeerToIndexMapping = {} self.DisconnectedPeers = {} self.LaunchStatutes = {} + self.ConnectionMatrix = {} end, ---@param self UIAutolobbyCommunications @@ -224,9 +226,9 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { --#region Utilities ---@param self UIAutolobbyCommunications - ---@param peers Peer[] + ---@param connectionMatrix table ---@return UIAutolobbyConnections - CreateConnectionsMatrix = function(self, peers) + CreateConnectionsMatrix = function(self, connectionMatrix) ---@type UIAutolobbyConnections local connections = {} @@ -239,15 +241,20 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { end -- populate the matrix - for _, peer in peers do - for _, peerConnectedToId in peer.establishedPeers do - local peerIdNumber = self:PeerIdToIndex(peer.id) + for peerId, establishedPeers in connectionMatrix do + for _, peerConnectedToId in establishedPeers do + local peerIdNumber = self:PeerIdToIndex(peerId) local peerConnectedToIdNumber = self:PeerIdToIndex(peerConnectedToId) -- connection works both ways if peerIdNumber and peerConnectedToIdNumber then - connections[peerIdNumber][peerConnectedToIdNumber] = true - connections[peerConnectedToIdNumber][peerIdNumber] = true + if peerIdNumber > self.PlayerCount or peerConnectedToIdNumber > self.PlayerCount then + -- reset, we have weird data somehow + self.PeerToIndexMapping = {} + else + connections[peerIdNumber][peerConnectedToIdNumber] = true + connections[peerConnectedToIdNumber][peerIdNumber] = true + end end end end @@ -272,35 +279,15 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { --- Determines the launch status of the local peer. ---@param self UIAutolobbyCommunications - ---@param peers Peer[] + ---@param connectionMatrix table ---@return UIAutolobbyLaunchStatus - CreateLaunchStatus = function(self, peers) - + CreateLaunchStatus = function(self, connectionMatrix) -- check number of peers local validPeerCount = self.PlayerCount - 1 - if table.getsize(peers) < validPeerCount then + if table.getsize(connectionMatrix) < validPeerCount then return 'Missing local peers' end - -- check number of established peers - local establishedPeerCount = 0 - for k, peer in peers do - if peer.status == "Established" then - establishedPeerCount = establishedPeerCount + 1 - end - end - - if establishedPeerCount < validPeerCount then - return 'Not all local peers are established' - end - - -- check confirmed established connections of peers - for _, peer in peers do - if table.getsize(peer.establishedPeers) ~= self.PlayerCount - 1 then - return 'Not all peers are connected' - end - end - return 'Ready' end, @@ -377,22 +364,27 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { Rejoin = function(self, lobbyParameters, joinParameters) local autolobbyModule = import("/lua/ui/lobby/autolobby.lua") + LOG("REJOINING") + -- start disposing threads to prevent race conditions self.Trash:Destroy() ForkThread( function() - -- give time for any messages that are already send - WaitSeconds(0.5) + -- prevent race condition on network + WaitSeconds(1.0) -- inform peers that we're rejoining self:BroadcastData({ Type = "UpdateLaunchStatus", LaunchStatus = 'Rejoining' }) - -- give the message time to arrive - WaitSeconds(0.5) + -- prevent race condition on network + WaitSeconds(1.0) - -- replace ourselves with the new lobby and rejoin + -- create a new lobby self:Destroy() + + -- prevent race conditions + WaitSeconds(1.0) local newLobby = autolobbyModule.CreateLobby( lobbyParameters.Protocol, lobbyParameters.LocalPort, @@ -401,6 +393,9 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { lobbyParameters.NatTraversalProvider ) + -- wait a bit before we join + WaitSeconds(1.0) + autolobbyModule.JoinGame(joinParameters.Address, joinParameters.AsObserver, joinParameters.DesiredPlayerName, joinParameters.DesiredPeerId) @@ -423,17 +418,22 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { -- check if we're ready to launch if self.LaunchStatutes[self.LocalPeerId] ~= 'Ready' then - -- if we're not, check if a peer is ready to launch + -- if we're not, check the status of peers + local onePeerIsRejoining = false local onePeerIsReady = false for k, launchStatus in self.LaunchStatutes do - if launchStatus == 'Ready' then - onePeerIsReady = true - end + onePeerIsReady = onePeerIsReady or (launchStatus == 'Ready') + onePeerIsRejoining = onePeerIsRejoining or (launchStatus == 'Rejoining') end if onePeerIsReady then rejoinCount = rejoinCount + 1 end + + -- try to not rejoin at the same time + if onePeerIsRejoining then + rejoinCount = 0 + end else rejoinCount = 0 end @@ -443,7 +443,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { self:Rejoin(self.LobbyParameters, self.JoinParameters) end - WaitSeconds(1.0) + WaitSeconds(2.0 + 2 * Random()) end end, @@ -453,8 +453,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { WaitSeconds(1.0) while not IsDestroyed(self) do - local peers = self:GetPeers() - local launchStatus = self:CreateLaunchStatus(peers) + local launchStatus = self:CreateLaunchStatus(self.ConnectionMatrix) self.LaunchStatutes[self.LocalPeerId] = launchStatus self:BroadcastData({ Type = "UpdateLaunchStatus", LaunchStatus = launchStatus }) @@ -471,6 +470,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { while not IsDestroyed(self) do if self:CanLaunch(self.LaunchStatutes) then + LOG("We can launch...") WaitSeconds(5.0) if (not IsDestroyed(self)) and self:CanLaunch(self.LaunchStatutes) then @@ -481,6 +481,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { Observers = {}, } + LOG("Launching!") self:BroadcastData({ Type = "Launch", GameConfig = gameConfiguration }) self:LaunchGame(gameConfiguration) end @@ -616,11 +617,10 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { -- inform the server of the event GpgNetSendDisconnected(peerId) - -- reset mapping + -- reset mappings self.PeerToIndexMapping = {} - self.LaunchStatutes = {} + self.ConnectionMatrix[peerId] = nil self.DisconnectedPeers[peerId] = GetSystemTimeSeconds() - return MohoLobbyMethods.DisconnectFromPeer(self, peerId) end, @@ -792,7 +792,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { self:DebugSpew("ConnectionFailed", reason) -- try to rejoin - -- self:Rejoin(self.LobbyParameters, self.JoinParameters) + self:Rejoin(self.LobbyParameters, self.JoinParameters) end, --- Called by the engine when the connection succeeds with the host. @@ -821,15 +821,20 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { EstablishedPeers = function(self, peerId, peerConnectedTo) self:DebugSpew("EstablishedPeers", peerId, reprs(peerConnectedTo)) - ForkThread( - function() - WaitFrames(4) - local peers = self:GetPeers() - local connections = self:CreateConnectionsMatrix(peers) - import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() - :UpdateConnections(connections) - end - ) + + self.LaunchStatutes[peerId] = self.LaunchStatutes[peerId] or 'Unknown' + -- update UI for launch statuses + import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() + :UpdateLaunchStatuses(self:CreateConnectionStatuses(self.LaunchStatutes)) + + -- update the matrix and the UI + self.ConnectionMatrix[peerId] = peerConnectedTo + local connections = self:CreateConnectionsMatrix(self.ConnectionMatrix) + import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() + :UpdateConnections(connections) + + + end, --#endregion @@ -900,7 +905,13 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { -- reset mapping self.PeerToIndexMapping = {} - self.LaunchStatutes = {} + + self.LaunchStatutes[peerId] = nil + -- update UI for launch statuses + import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() + :UpdateLaunchStatuses(self:CreateConnectionStatuses(self.LaunchStatutes)) + + self.ConnectionMatrix[peerId] = nil self.DisconnectedPeers[peerId] = GetSystemTimeSeconds() end, From 4350d0f0bd7e86ab58c13f9bb6ffa329e97c058d Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Tue, 22 Oct 2024 21:08:02 +0200 Subject: [PATCH 44/83] Rewrite test logic --- lua/ui/lobby/autolobby.lua | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/lua/ui/lobby/autolobby.lua b/lua/ui/lobby/autolobby.lua index c8fe648bd6..b34306aa53 100644 --- a/lua/ui/lobby/autolobby.lua +++ b/lua/ui/lobby/autolobby.lua @@ -120,23 +120,21 @@ function JoinGame(address, asObserver, playerName, uid) AutolobbyCommunicationsInstance:JoinGame(address, playerName, uid) end - - -- join over time - ForkThread( - function() - local startSpot = tonumber(GetCommandLineArg("/startspot", 1)[1]) or 1 - - if rejoinTest and startSpot == 2 then - rejoinTest = false - - WaitSeconds(4) - LOG(" -- REJOIN TEST -- ") - if AutolobbyCommunicationsInstance then - AutolobbyCommunicationsInstance:DisconnectFromPeer("3") + if rejoinTest then + rejoinTest = false + ForkThread( + function() + local startSpot = tonumber(GetCommandLineArg("/startspot", 1)[1]) or 1 + if startSpot == 2 then + WaitSeconds(3) + LOG(" -- REJOIN TEST -- ") + if AutolobbyCommunicationsInstance then + AutolobbyCommunicationsInstance:DisconnectFromPeer("3") + end end end - end - ) + ) + end -- -- start with a loading dialog -- import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() From fa733a96021c28b58898c817d90e3d411330bd54 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Tue, 22 Oct 2024 21:10:47 +0200 Subject: [PATCH 45/83] Fix annotation issues --- lua/maui/bitmap.lua | 2 +- lua/ui/lobby/autolobby/AutolobbyController.lua | 2 +- lua/ui/lobby/autolobby/AutolobbyInterface.lua | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lua/maui/bitmap.lua b/lua/maui/bitmap.lua index 0e92ad4eb0..b0e23a25bf 100644 --- a/lua/maui/bitmap.lua +++ b/lua/maui/bitmap.lua @@ -40,7 +40,7 @@ local ScaleNumber = import("/lua/maui/layouthelpers.lua").ScaleNumber Bitmap = ClassUI(moho.bitmap_methods, Control) { ---@param self Bitmap ---@param parent Control - ---@param filename Lazy + ---@param filename? Lazy ---@param debugname? string __init = function(self, parent, filename, debugname) InternalCreateBitmap(self, parent) diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index 0d8428ac3e..b0f8f5b715 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -166,7 +166,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { -- retrieve team and start spot info.Team = tonumber(GetCommandLineArg("/team", 1)[1]) - info.StartSpot = tonumber(GetCommandLineArg("/startspot", 1)[1]) + info.StartSpot = tonumber(GetCommandLineArg("/startspot", 1)[1]) or -1 -- TODO -- determine army color based on start location info.PlayerColor = GameColors.MapToWarmCold(info.StartSpot) diff --git a/lua/ui/lobby/autolobby/AutolobbyInterface.lua b/lua/ui/lobby/autolobby/AutolobbyInterface.lua index 180be71459..be47f9d390 100644 --- a/lua/ui/lobby/autolobby/AutolobbyInterface.lua +++ b/lua/ui/lobby/autolobby/AutolobbyInterface.lua @@ -68,7 +68,8 @@ local AutolobbyInterface = Class(Group) { PlayerCount = playerCount } - self.Background = UIUtil.CreateBitmap(self, self.BackgroundTextures[math.random(1, 5)]) + local backgroundTexture = self.BackgroundTextures[math.random(1, 5)] --[[@as FileName]] + self.Background = UIUtil.CreateBitmap(self, backgroundTexture) self.Preview = AutolobbyMapPreview.GetInstance(self) self.ConnectionMatrix = AutolobbyConnectionMatrix.Create(self, playerCount) -- TODO: determine this number dynamically end, From 14821634039dbc333fe0c1b0ac170352e8623ef7 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Tue, 22 Oct 2024 21:16:00 +0200 Subject: [PATCH 46/83] Reduce time for rejoin to kick in --- lua/ui/lobby/autolobby/AutolobbyController.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index b0f8f5b715..37e9bc6874 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -443,7 +443,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { self:Rejoin(self.LobbyParameters, self.JoinParameters) end - WaitSeconds(2.0 + 2 * Random()) + WaitSeconds(1.0 + 1 * Random()) end end, From 58cf87be8df079fad29c1394643e201ab3a2ab61 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Wed, 23 Oct 2024 19:21:25 +0200 Subject: [PATCH 47/83] Reduce the update frequency of the status message Based on observations the lobby is more likely to crash upon reconnecting when it is processing a message. By reducing the number of messages we make it less likely for the lobby to crash. --- lua/ui/lobby/autolobby/AutolobbyController.lua | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index 37e9bc6874..aa7604487c 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -95,15 +95,15 @@ local AutolobbyEngineStrings = { ---@field Trash TrashBag ---@field LocalPeerId UILobbyPeerId # a number that is stringified ---@field LocalPlayerName string # nickname ----@field HostID UILobbyPeerId ----@field PlayerCount number ----@field GameMods UILobbyLaunchGameModsConfiguration[] ----@field GameOptions UILobbyLaunchGameOptionsConfiguration # Is synced from the host to the others. ----@field PlayerOptions UIAutolobbyPlayer[] # Is synced from the host to the others. ----@field ConnectionMatrix table +---@field HostID UILobbyPeerId +---@field PlayerCount number # Originates from the command line +---@field GameMods UILobbyLaunchGameModsConfiguration[] +---@field GameOptions UILobbyLaunchGameOptionsConfiguration # Is synced from the host via `SendData` or `BroadcastData`. +---@field PlayerOptions UIAutolobbyPlayer[] # Is synced from the host via `SendData` or `BroadcastData`. +---@field ConnectionMatrix table # Is synced between players via `EstablishedPeers` ---@field PeerToIndexMapping table ---@field DisconnectedPeers table # ----@field LaunchStatutes table +---@field LaunchStatutes table # Is synced between players via `BroadcastData` ---@field LobbyParameters? UIAutolobbyParameters # Used for rejoining functionality ---@field HostParameters? UIAutolobbyHostParameters # Used for rejoining functionality ---@field JoinParameters? UIAutolobbyJoinParameters # Used for rejoining functionality @@ -450,8 +450,6 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { --- Passes the local launch status to all peers. ---@param self UIAutolobbyCommunications ShareLaunchStatusThread = function(self) - WaitSeconds(1.0) - while not IsDestroyed(self) do local launchStatus = self:CreateLaunchStatus(self.ConnectionMatrix) self.LaunchStatutes[self.LocalPeerId] = launchStatus @@ -461,7 +459,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() :UpdateLaunchStatuses(self:CreateConnectionStatuses(self.LaunchStatutes)) - WaitSeconds(0.5) + WaitSeconds(2.0) end end, From 7259a1f497d3e9c552ac3371d83b5f4ceea66f60 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Fri, 25 Oct 2024 14:40:00 +0200 Subject: [PATCH 48/83] Improve documentation of server communication --- engine/User.lua | 6 +- engine/User/CLobby.lua | 4 +- lua/ui/globals/GpgNetSend.lua | 77 +++-------- lua/ui/lobby/autolobby.lua | 2 +- .../AutolobbyConnectionMatrixDot.lua | 2 +- .../lobby/autolobby/AutolobbyController.lua | 58 ++++---- lua/ui/lobby/autolobby/AutolobbyMessages.lua | 2 +- ...AutolobbyServerCommunicationsComponent.lua | 130 ++++++++++++++++++ 8 files changed, 186 insertions(+), 95 deletions(-) create mode 100644 lua/ui/lobby/autolobby/components/AutolobbyServerCommunicationsComponent.lua diff --git a/engine/User.lua b/engine/User.lua index eeb6e54703..529e714738 100644 --- a/engine/User.lua +++ b/engine/User.lua @@ -624,9 +624,9 @@ end function GpgNetActive() end ----@param cmd string ----@param ... any -function GpgNetSend(cmd, ...) +---@param command string +---@param ... number | string +function GpgNetSend(command, ...) end --- diff --git a/engine/User/CLobby.lua b/engine/User/CLobby.lua index 77d07aa3eb..eb132bd52e 100644 --- a/engine/User/CLobby.lua +++ b/engine/User/CLobby.lua @@ -12,7 +12,7 @@ local CLobby = {} ---@alias UILobbyProtocol 'UDP' | 'TCP' ----@alias UIPeerStatus 'None' | 'Pending' | 'Connecting' | 'Answering' | 'Established' | 'TimedOut' | 'Errored' +---@alias UIPeerConnectionStatus 'None' | 'Pending' | 'Connecting' | 'Answering' | 'Established' | 'TimedOut' | 'Errored' ---@class Peer ---@field establishedPeers UILobbyPeerId[] @@ -20,7 +20,7 @@ local CLobby = {} ---@field ping number ---@field name string ---@field quiet number ----@field status UIPeerStatus +---@field status UIPeerConnectionStatus --- A piece of data that is one can send with `BroadcastData` or `SendData` to other player(s) in the lobby. ---@class UILobbyReceivedMessage : table diff --git a/lua/ui/globals/GpgNetSend.lua b/lua/ui/globals/GpgNetSend.lua index 84753bb9be..97df7d9540 100644 --- a/lua/ui/globals/GpgNetSend.lua +++ b/lua/ui/globals/GpgNetSend.lua @@ -22,15 +22,32 @@ --** SOFTWARE. --****************************************************************************************************** +------------------------------------------------------------------------------- +--#region Game <-> Server communications + +-- All the following logic is tightly coupled with functionality on either the +-- lobby server, the ice adapter, the java server and/or the client. For more +-- context you can search for the various keywords in the following repositories: +-- - Lobby server: https://github.com/FAForever/server +-- - Java server: https://github.com/FAForever/faf-java-server +-- - Java Ice adapter: https://github.com/FAForever/java-ice-adapter +-- - Kotlin Ice adapter: https://github.com/FAForever/kotlin-ice-adapter +-- +-- If we do not send this information then the client is unaware of changes made +-- to the lobby after hosting. These messages are usually only accepted from the +-- host of the lobby. + --- Original function that we should not use directly local oldGpgNetSend = GpgNetSend ---- Adds a hook that generates sim callbacks for communication to the ---- server. Useful for moderation purposes. ----@param command any ----@param ... unknown + +---@param command string +---@param ... number | string _G.GpgNetSend = function(command, ...) + --- Add a hook that generates sim callbacks for communication to the + --- server. Useful for moderation purposes. + if SessionIsActive() and not SessionIsReplay() then local stringifiedArgs = "" for k = 1, table.getn(arg) do @@ -53,56 +70,4 @@ _G.GpgNetSend = function(command, ...) oldGpgNetSend(command, unpack(arg)) end -------------------------------------------------------------------------------- ---#region Game <-> Server communications - --- All the following logic is tightly coupled with functionality on either the --- lobby server, the ice adapter, the java server and/or the client. For more --- context you can search for the various keywords in the following repositories: --- - Lobby server: https://github.com/FAForever/server --- - Java server: https://github.com/FAForever/faf-java-server --- - Java Ice adapter: https://github.com/FAForever/java-ice-adapter --- - Kotlin Ice adapter: https://github.com/FAForever/kotlin-ice-adapter --- --- If we do not send this information then the client is unaware of changes made --- to the lobby after hosting. These messages are usually only accepted from the --- host of the lobby. - ---- Sends player options to the lobby server. For more context: ---- - https://github.com/search?q=org%3AFAForever+player_option&type=code ----@param peerId UILobbyPeerId ----@param key 'Team' | 'Army' | 'StartSpot' | 'Faction' | 'Color' ----@param value string | number -GpgNetSendPlayerOption = function(peerId, key, value) - _G.GpgNetSend('PlayerOption', peerId, key, value) -end - ---- Sends AI options to the lobby server. For more context: ---- - https://github.com/search?q=org%3AFAForever+ai_option&type=code ----@param key string ----@param value string -GpgNetSendAIOption = function(aiName, key, value) - _G.GpgNetSend('AIOption', aiName, key, value) -end - ---- Sends game options to the lobby server. For more context: ---- - https://github.com/search?q=repo%3AFAForever%2Fserver+game_option&type=code ----@param key 'ScenarioFile' | 'Slots' | 'RestrictedCategories' ----@param value string | number -GpgNetSendGameOption = function(key, value) - _G.GpgNetSend('GameOption', key, value) -end ---- Sends game status to the lobby server. For more context: ---- - https://github.com/search?q=repo%3AFAForever%2Fserver+GameState&type=code ----@param value 'None' | 'Idle' | 'Lobby' | 'Launching' | 'Ended' -GpgNetSendGameState = function(value) - _G.GpgNetSend('GameState', value) -end - ---- Sends game status to the lobby server. For more context: ---- - https://github.com/search?q=repo%3AFAForever%2Fserver+GameState&type=code ----@param peerId UILobbyPeerId -GpgNetSendDisconnected = function(peerId) - GpgNetSend('Disconnected', peerId) -end diff --git a/lua/ui/lobby/autolobby.lua b/lua/ui/lobby/autolobby.lua index b34306aa53..d29ff5d378 100644 --- a/lua/ui/lobby/autolobby.lua +++ b/lua/ui/lobby/autolobby.lua @@ -64,7 +64,7 @@ function CreateLobby(protocol, localPort, desiredPlayerName, localPlayerUID, nat AutolobbyCommunicationsInstance.LobbyParameters.LocalPlayerPeerId = localPlayerUID AutolobbyCommunicationsInstance.LobbyParameters.NatTraversalProvider = natTraversalProvider - GpgNetSendGameState('Idle') + AutolobbyCommunicationsInstance:SendGameStateToServer('Idle') return AutolobbyCommunicationsInstance end diff --git a/lua/ui/lobby/autolobby/AutolobbyConnectionMatrixDot.lua b/lua/ui/lobby/autolobby/AutolobbyConnectionMatrixDot.lua index 5fecf7bcd3..2067b1ea29 100644 --- a/lua/ui/lobby/autolobby/AutolobbyConnectionMatrixDot.lua +++ b/lua/ui/lobby/autolobby/AutolobbyConnectionMatrixDot.lua @@ -66,7 +66,7 @@ local AutolobbyConnectionMatrixDot = Class(Bitmap) { end, ---@param self UIAutolobbyConnectionMatrixDot - ---@param status UIAutolobbyLaunchStatus + ---@param status UIPeerLaunchStatus SetStatus = function(self, status) if status == 'Unknown' then self:SetSolidColor(EnumColors.Blue) diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index aa7604487c..77fda1a2a3 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -22,11 +22,13 @@ local Utils = import("/lua/system/utils.lua") local MapUtil = import("/lua/ui/maputil.lua") - local GameColors = import("/lua/GameColors.lua") + local MohoLobbyMethods = moho.lobby_methods local DebugComponent = import("/lua/shared/components/DebugComponent.lua").DebugComponent - +local AutolobbyServerCommunicationsComponent = import("/lua/ui/lobby/autolobby/components/AutolobbyServerCommunicationsComponent.lua") + .AutolobbyServerCommunicationsComponent + local AutolobbyMessages = import("/lua/ui/lobby/autolobby/AutolobbyMessages.lua").AutolobbyMessages local AutolobbyEngineStrings = { @@ -52,12 +54,6 @@ local AutolobbyEngineStrings = { ['LaunchRejected'] = "Some players are using an incompatible client version.", } ----@alias UIAutolobbyLaunchStatus ---- | 'Disconnected' ---- | 'Unknown' ---- | 'Missing local peers' ---- | 'Rejoining' ---- | 'Ready' ---@class UIAutolobbyPlayer: UILobbyLaunchPlayerConfiguration ---@field StartSpot number @@ -69,7 +65,7 @@ local AutolobbyEngineStrings = { ---@field PL number # Related to rating/divisions ---@alias UIAutolobbyConnections boolean[][] ----@alias UIAutolobbyStatus UIAutolobbyLaunchStatus[] +---@alias UIAutolobbyStatus UIPeerLaunchStatus[] ---@class UIAutolobbyParameters ---@field Protocol UILobbyProtocol @@ -91,23 +87,23 @@ local AutolobbyEngineStrings = { ---@field DesiredPeerId UILobbyPeerId --- Responsible for the behavior of the automated lobby. ----@class UIAutolobbyCommunications : moho.lobby_methods, DebugComponent +---@class UIAutolobbyCommunications : moho.lobby_methods, DebugComponent, UIAutolobbyServerCommunicationsComponent ---@field Trash TrashBag ---@field LocalPeerId UILobbyPeerId # a number that is stringified ---@field LocalPlayerName string # nickname ----@field HostID UILobbyPeerId +---@field HostID UILobbyPeerId ---@field PlayerCount number # Originates from the command line ----@field GameMods UILobbyLaunchGameModsConfiguration[] +---@field GameMods UILobbyLaunchGameModsConfiguration[] ---@field GameOptions UILobbyLaunchGameOptionsConfiguration # Is synced from the host via `SendData` or `BroadcastData`. ---@field PlayerOptions UIAutolobbyPlayer[] # Is synced from the host via `SendData` or `BroadcastData`. ---@field ConnectionMatrix table # Is synced between players via `EstablishedPeers` ---@field PeerToIndexMapping table ---@field DisconnectedPeers table # ----@field LaunchStatutes table # Is synced between players via `BroadcastData` +---@field LaunchStatutes table # Is synced between players via `BroadcastData` ---@field LobbyParameters? UIAutolobbyParameters # Used for rejoining functionality ---@field HostParameters? UIAutolobbyHostParameters # Used for rejoining functionality ---@field JoinParameters? UIAutolobbyJoinParameters # Used for rejoining functionality -AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { +AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsComponent, DebugComponent) { BackgroundTextures = { "/menus02/background-paint01_bmp.dds", @@ -263,7 +259,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { end, ---@param self UIAutolobbyCommunications - ---@param statuses table + ---@param statuses table ---@return UIAutolobbyStatus CreateConnectionStatuses = function(self, statuses) local output = {} @@ -280,7 +276,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { --- Determines the launch status of the local peer. ---@param self UIAutolobbyCommunications ---@param connectionMatrix table - ---@return UIAutolobbyLaunchStatus + ---@return UIPeerLaunchStatus CreateLaunchStatus = function(self, connectionMatrix) -- check number of peers local validPeerCount = self.PlayerCount - 1 @@ -364,17 +360,17 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { Rejoin = function(self, lobbyParameters, joinParameters) local autolobbyModule = import("/lua/ui/lobby/autolobby.lua") - LOG("REJOINING") - -- start disposing threads to prevent race conditions self.Trash:Destroy() ForkThread( function() + self:SendLaunchStatusToServer('Rejoining') + -- prevent race condition on network WaitSeconds(1.0) - -- inform peers that we're rejoining + -- inform peers and server that we're rejoining self:BroadcastData({ Type = "UpdateLaunchStatus", LaunchStatus = 'Rejoining' }) -- prevent race condition on network @@ -453,8 +449,13 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { while not IsDestroyed(self) do local launchStatus = self:CreateLaunchStatus(self.ConnectionMatrix) self.LaunchStatutes[self.LocalPeerId] = launchStatus + + -- update peers self:BroadcastData({ Type = "UpdateLaunchStatus", LaunchStatus = launchStatus }) + -- update server + self:SendLaunchStatusToServer(launchStatus) + -- update UI for launch statuses import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() :UpdateLaunchStatuses(self:CreateConnectionStatuses(self.LaunchStatutes)) @@ -468,7 +469,6 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { while not IsDestroyed(self) do if self:CanLaunch(self.LaunchStatutes) then - LOG("We can launch...") WaitSeconds(5.0) if (not IsDestroyed(self)) and self:CanLaunch(self.LaunchStatutes) then @@ -479,7 +479,6 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { Observers = {}, } - LOG("Launching!") self:BroadcastData({ Type = "Launch", GameConfig = gameConfiguration }) self:LaunchGame(gameConfiguration) end @@ -612,9 +611,6 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { DisconnectFromPeer = function(self, peerId) self:DebugSpew("DisconnectFromPeer", peerId) - -- inform the server of the event - GpgNetSendDisconnected(peerId) - -- reset mappings self.PeerToIndexMapping = {} self.ConnectionMatrix[peerId] = nil @@ -706,7 +702,8 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { LaunchGame = function(self, gameConfig) self:DebugSpew("LaunchGame") self:DebugSpew(reprs(gameConfig, { depth = 10 })) - GpgNetSendGameState('Launching') + + self:SendGameStateToServer('Launching') return MohoLobbyMethods.LaunchGame(self, gameConfig) end, @@ -770,7 +767,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { -- start prefetching the scenario self:Prefetch(self.GameOptions, self.GameMods) - GpgNetSendGameState('Lobby') + self:SendGameStateToServer('Lobby') -- update UI for game options import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() @@ -803,7 +800,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { self.LocalPeerId = localPeerId self.HostID = hostPeerId - GpgNetSendGameState('Lobby') + self:SendGameStateToServer('Lobby') -- occasionally send data over the network to create pings on screen self.Trash:Add(ForkThread(self.ShareLaunchStatusThread, self)) @@ -819,6 +816,8 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { EstablishedPeers = function(self, peerId, peerConnectedTo) self:DebugSpew("EstablishedPeers", peerId, reprs(peerConnectedTo)) + -- update server + self:SendEstablishedPeers(peerId, peerConnectedTo) self.LaunchStatutes[peerId] = self.LaunchStatutes[peerId] or 'Unknown' -- update UI for launch statuses @@ -830,9 +829,6 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { local connections = self:CreateConnectionsMatrix(self.ConnectionMatrix) import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() :UpdateConnections(connections) - - - end, --#endregion @@ -924,7 +920,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, DebugComponent) { -- destroy ourselves, the game takes over the management of peers self:Destroy() - GpgNetSend('GameState', 'Launching') + self:SendGameStateToServer('Launching') end, --- Called by the engine when the launch failed. diff --git a/lua/ui/lobby/autolobby/AutolobbyMessages.lua b/lua/ui/lobby/autolobby/AutolobbyMessages.lua index 348891cb3c..0479e5cd09 100644 --- a/lua/ui/lobby/autolobby/AutolobbyMessages.lua +++ b/lua/ui/lobby/autolobby/AutolobbyMessages.lua @@ -65,7 +65,7 @@ AutolobbyMessages = { UpdateLaunchStatus = { ---@class UIAutolobbyUpdateLaunchStatusMessage : UILobbyReceivedMessage - ---@field LaunchStatus UIAutolobbyLaunchStatus + ---@field LaunchStatus UIPeerLaunchStatus ---@param lobby UIAutolobbyCommunications ---@param data UIAutolobbyUpdateLaunchStatusMessage diff --git a/lua/ui/lobby/autolobby/components/AutolobbyServerCommunicationsComponent.lua b/lua/ui/lobby/autolobby/components/AutolobbyServerCommunicationsComponent.lua new file mode 100644 index 0000000000..d3a777e0ff --- /dev/null +++ b/lua/ui/lobby/autolobby/components/AutolobbyServerCommunicationsComponent.lua @@ -0,0 +1,130 @@ +--****************************************************************************************************** +--** Copyright (c) 2024 Willem 'Jip' Wijnia +--** +--** Permission is hereby granted, free of charge, to any person obtaining a copy +--** of this software and associated documentation files (the "Software"), to deal +--** in the Software without restriction, including without limitation the rights +--** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +--** copies of the Software, and to permit persons to whom the Software is +--** furnished to do so, subject to the following conditions: +--** +--** The above copyright notice and this permission notice shall be included in all +--** copies or substantial portions of the Software. +--** +--** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +--** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +--** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +--** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +--** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +--** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +--** SOFTWARE. +--****************************************************************************************************** + +------------------------------------------------------------------------------- +--#region Game <-> Server communications + +-- All the following logic is tightly coupled with functionality on either the +-- lobby server, the ice adapter, the java server and/or the client. For more +-- context you can search for the various keywords in the following repositories: +-- - Lobby server: https://github.com/FAForever/server +-- - Java Ice adapter: https://github.com/FAForever/java-ice-adapter +-- - Kotlin Ice adapter: https://github.com/FAForever/kotlin-ice-adapter +-- +-- If we do not send this information then the client is unaware of changes made +-- to the lobby after hosting. These messages are usually only accepted from the +-- host of the lobby. + +-- upvalue scope for performance +local GpgNetSend = GpgNetSend + +--- Interpretation of the lobby status of a single peer. +---@alias UILobbyState +---| 'None' +---| 'Idle' +---| 'Lobby' +---| 'Launching' +---| 'Ended' + +--- Interpretation of the lobby launch status of a single peer. +---@alias UIPeerLaunchStatus +--- | 'Unknown' # Initial value, is never send. +--- | 'Missing local peers' # Send when the local peer is missing other peers +--- | 'Rejoining' # Send when the local peer is rejoining +--- | 'Ready' # Send when the local peer is ready to start +--- | 'Rejected' # Send when there is a game version missmatch +--- | 'Failed' # Send when the game fails to launch + +--- A component that represent all the supported lobby <-> server communications. +---@class UIAutolobbyServerCommunicationsComponent +AutolobbyServerCommunicationsComponent = ClassSimple { + + ---@param self UIAutolobbyServerCommunicationsComponent | UIAutolobbyCommunications + ---@param peerId UILobbyPeerId + ---@param key 'Team' | 'Army' | 'StartSpot' | 'Faction' + ---@param value any + SendPlayerOptionToServer = function(self, peerId, key, value) + -- message is only accepted by the server if it originates from the host + if not self:IsHost() then + return + end + + GpgNetSend('PlayerOption', peerId, key, value) + end, + + ---@param self UIAutolobbyServerCommunicationsComponent | UIAutolobbyCommunications + ---@param aiName string + ---@param key 'Team' | 'Army' | 'StartSpot' | 'Faction' + ---@param value any + SendAIOptionToServer = function(self, aiName, key, value) + -- message is only accepted by the server if it originates from the host + if not self:IsHost() then + return + end + + GpgNetSend('AIOption', aiName, key, value) + end, + + ---@param self UIAutolobbyServerCommunicationsComponent | UIAutolobbyCommunications + ---@param key 'Slots' | any + ---@param value any + SendGameOptionToServer = function(self, key, value) + -- message is only accepted by the server if it originates from the host + if not self:IsHost() then + return + end + + GpgNetSend('GameOption', key, value) + end, + + ---@param self UIAutolobbyServerCommunicationsComponent | UIAutolobbyCommunications + ---@param value UILobbyState + SendGameStateToServer = function(self, value) + GpgNetSend('GameState', value) + end, + + ---@param self UIAutolobbyServerCommunicationsComponent | UIAutolobbyCommunications + ---@param value UIPeerLaunchStatus + SendLaunchStatusToServer = function(self, value) + GpgNetSend('LaunchStatus', value) + end, + + ---@param self UIAutolobbyServerCommunicationsComponent | UIAutolobbyCommunications + ---@param peerId UILobbyPeerId + ---@param peers UILobbyPeerId[] + SendEstablishedPeers = function(self, peerId, peers) + local establishedPeers = "" + + local establishedPeersCount = table.getn(peers) + if establishedPeersCount == 1 then + establishedPeers = peers[1] + elseif establishedPeersCount > 1 then + establishedPeers = peers[1] + + for k = 2, establishedPeersCount do + establishedPeers = establishedPeers .. " " .. peers[k] + end + end + + GpgNetSend('EstablishedPeers', peerId, establishedPeers) + end, +} From 4226f963c37123ab6e65467a4f9c49369cf2622a Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Fri, 25 Oct 2024 14:40:44 +0200 Subject: [PATCH 49/83] Improve documentation --- .../components/AutolobbyServerCommunicationsComponent.lua | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lua/ui/lobby/autolobby/components/AutolobbyServerCommunicationsComponent.lua b/lua/ui/lobby/autolobby/components/AutolobbyServerCommunicationsComponent.lua index d3a777e0ff..15d24325cf 100644 --- a/lua/ui/lobby/autolobby/components/AutolobbyServerCommunicationsComponent.lua +++ b/lua/ui/lobby/autolobby/components/AutolobbyServerCommunicationsComponent.lua @@ -30,9 +30,8 @@ -- - Java Ice adapter: https://github.com/FAForever/java-ice-adapter -- - Kotlin Ice adapter: https://github.com/FAForever/kotlin-ice-adapter -- --- If we do not send this information then the client is unaware of changes made --- to the lobby after hosting. These messages are usually only accepted from the --- host of the lobby. +-- Specifically, the following file processes these messages on the server: +-- - https://github.com/FAForever/server/blob/98271c421412467fa387f3a6530fe8d24e360fa4/server/gameconnection.py -- upvalue scope for performance local GpgNetSend = GpgNetSend From 2e8051608e31100e26dce8d6e60e7c46f5fa805f Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Fri, 25 Oct 2024 21:28:14 +0200 Subject: [PATCH 50/83] Re-introduce sending player options to the server --- lua/ui/lobby/autolobby/AutolobbyController.lua | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index 77fda1a2a3..89d9637677 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -472,6 +472,17 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC WaitSeconds(5.0) if (not IsDestroyed(self)) and self:CanLaunch(self.LaunchStatutes) then + + -- send player options to the server + for slot, playerOptions in self.PlayerOptions do + local ownerId = playerOptions.OwnerID + self:SendPlayerOptionToServer(ownerId, 'Team', playerOptions.Team) + self:SendPlayerOptionToServer(ownerId, 'Army', playerOptions.StartSpot) + self:SendPlayerOptionToServer(ownerId, 'StartSpot', playerOptions.StartSpot) + self:SendPlayerOptionToServer(ownerId, 'Faction', playerOptions.Faction) + end + + -- create game configuration local gameConfiguration = { GameMods = self.GameMods, GameOptions = self.GameOptions, @@ -479,6 +490,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC Observers = {}, } + -- send it to all players and tell them to launch self:BroadcastData({ Type = "Launch", GameConfig = gameConfiguration }) self:LaunchGame(gameConfiguration) end From 879f8a1601fc4389cdbd8ae356d3fd724a6d79c0 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Fri, 25 Oct 2024 21:32:26 +0200 Subject: [PATCH 51/83] Extend launch status messages --- lua/ui/lobby/autolobby/AutolobbyController.lua | 6 +++++- .../components/AutolobbyServerCommunicationsComponent.lua | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index 89d9637677..3c5edf191d 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -28,7 +28,7 @@ local MohoLobbyMethods = moho.lobby_methods local DebugComponent = import("/lua/shared/components/DebugComponent.lua").DebugComponent local AutolobbyServerCommunicationsComponent = import("/lua/ui/lobby/autolobby/components/AutolobbyServerCommunicationsComponent.lua") .AutolobbyServerCommunicationsComponent - + local AutolobbyMessages = import("/lua/ui/lobby/autolobby/AutolobbyMessages.lua").AutolobbyMessages local AutolobbyEngineStrings = { @@ -780,6 +780,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC self:Prefetch(self.GameOptions, self.GameMods) self:SendGameStateToServer('Lobby') + self:SendLaunchStatusToServer('Hosting') -- update UI for game options import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() @@ -790,6 +791,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC ---@param self UIAutolobbyCommunications Connecting = function(self) self:DebugSpew("Connecting") + self:SendLaunchStatusToServer('Connecting') end, --- Called by the engine when the connection fails. @@ -853,6 +855,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC ---@param reason string # reason for disconnection, populated by the host Ejected = function(self, reason) self:DebugSpew("Ejected", reason) + self:SendLaunchStatusToServer('Ejected') end, --- ??? @@ -940,6 +943,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC ---@param reasonKey string LaunchFailed = function(self, reasonKey) self:DebugSpew("LaunchFailed", LOC(reasonKey)) + self:SendLaunchStatusToServer('Failed') end, --#endregion diff --git a/lua/ui/lobby/autolobby/components/AutolobbyServerCommunicationsComponent.lua b/lua/ui/lobby/autolobby/components/AutolobbyServerCommunicationsComponent.lua index 15d24325cf..6dbbe0b438 100644 --- a/lua/ui/lobby/autolobby/components/AutolobbyServerCommunicationsComponent.lua +++ b/lua/ui/lobby/autolobby/components/AutolobbyServerCommunicationsComponent.lua @@ -47,9 +47,11 @@ local GpgNetSend = GpgNetSend --- Interpretation of the lobby launch status of a single peer. ---@alias UIPeerLaunchStatus --- | 'Unknown' # Initial value, is never send. +--- | 'Connecting' # Send when the local peer is connecting to the lobby --- | 'Missing local peers' # Send when the local peer is missing other peers --- | 'Rejoining' # Send when the local peer is rejoining ---- | 'Ready' # Send when the local peer is ready to start +--- | 'Ready' # Send when the local peer is ready to launch +--- | 'Ejected' # Send when the local peer is ejected --- | 'Rejected' # Send when there is a game version missmatch --- | 'Failed' # Send when the game fails to launch From 19bff398980fac8e4cd6f8a6078c41faec1c6192 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Fri, 25 Oct 2024 21:33:37 +0200 Subject: [PATCH 52/83] Always log the message we send to the server --- lua/ui/globals/GpgNetSend.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lua/ui/globals/GpgNetSend.lua b/lua/ui/globals/GpgNetSend.lua index 97df7d9540..5b5f4b3555 100644 --- a/lua/ui/globals/GpgNetSend.lua +++ b/lua/ui/globals/GpgNetSend.lua @@ -48,6 +48,8 @@ _G.GpgNetSend = function(command, ...) --- Add a hook that generates sim callbacks for communication to the --- server. Useful for moderation purposes. + SPEW("GpgNetSend", command, unpack(arg)) + if SessionIsActive() and not SessionIsReplay() then local stringifiedArgs = "" for k = 1, table.getn(arg) do From 61a3d7d938fefa274b6bb945f7a266c4001bc652 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sat, 26 Oct 2024 14:00:56 +0200 Subject: [PATCH 53/83] Simplify the mapping of a peerId to an index for the UI We now check if we have player options with the same OwnerID that match. If we find that, we return the StartSpot. --- .../lobby/autolobby/AutolobbyController.lua | 61 ++++++------------- 1 file changed, 18 insertions(+), 43 deletions(-) diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index 3c5edf191d..070ed6f8c8 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -97,8 +97,6 @@ local AutolobbyEngineStrings = { ---@field GameOptions UILobbyLaunchGameOptionsConfiguration # Is synced from the host via `SendData` or `BroadcastData`. ---@field PlayerOptions UIAutolobbyPlayer[] # Is synced from the host via `SendData` or `BroadcastData`. ---@field ConnectionMatrix table # Is synced between players via `EstablishedPeers` ----@field PeerToIndexMapping table ----@field DisconnectedPeers table # ---@field LaunchStatutes table # Is synced between players via `BroadcastData` ---@field LobbyParameters? UIAutolobbyParameters # Used for rejoining functionality ---@field HostParameters? UIAutolobbyHostParameters # Used for rejoining functionality @@ -126,8 +124,6 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC self.GameMods = {} self.GameOptions = self:CreateLocalGameOptions() self.PlayerOptions = {} - self.PeerToIndexMapping = {} - self.DisconnectedPeers = {} self.LaunchStatutes = {} self.ConnectionMatrix = {} end, @@ -239,14 +235,13 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC -- populate the matrix for peerId, establishedPeers in connectionMatrix do for _, peerConnectedToId in establishedPeers do - local peerIdNumber = self:PeerIdToIndex(peerId) - local peerConnectedToIdNumber = self:PeerIdToIndex(peerConnectedToId) + local peerIdNumber = self:PeerIdToIndex(self.PlayerOptions, peerId) + local peerConnectedToIdNumber = self:PeerIdToIndex(self.PlayerOptions, peerConnectedToId) -- connection works both ways if peerIdNumber and peerConnectedToIdNumber then if peerIdNumber > self.PlayerCount or peerConnectedToIdNumber > self.PlayerCount then - -- reset, we have weird data somehow - self.PeerToIndexMapping = {} + self:DebugWarn("Invalid peer id", peerIdNumber, peerConnectedToIdNumber) else connections[peerIdNumber][peerConnectedToIdNumber] = true connections[peerConnectedToIdNumber][peerIdNumber] = true @@ -264,7 +259,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC CreateConnectionStatuses = function(self, statuses) local output = {} for peerId, launchStatus in statuses do - local peerIdNumber = self:PeerIdToIndex(peerId) + local peerIdNumber = self:PeerIdToIndex(self.PlayerOptions, peerId) if peerIdNumber then output[peerIdNumber] = launchStatus end @@ -312,32 +307,27 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC --- to numbers such as 35240. With this function we map it to a sane --- index that we can use in the interface. ---@param self UIAutolobbyCommunications + ---@param playerOptions UIAutolobbyPlayer[] ---@param peerId UILobbyPeerId ---@return number | false - PeerIdToIndex = function(self, peerId) + PeerIdToIndex = function(self, playerOptions, peerId) if type(peerId) ~= 'string' then self:DebugWarn("Invalid peer id", peerId) return false end - -- happens when a peer disconnected from us, but not (yet) to other players - if self.DisconnectedPeers[peerId] then - return false - end - - -- happens before the connection is established - if peerId == "-1" then - -- just return some index, but do not store it - return table.getsize(self.PeerToIndexMapping) + 1 - end - - local index = self.PeerToIndexMapping[peerId] - if not index then - index = table.getsize(self.PeerToIndexMapping) + 1 - self.PeerToIndexMapping[peerId] = index + -- try to find matching player options + if playerOptions then + for k, options in playerOptions do + if options.OwnerID == peerId then + if options.StartSpot then + return options.StartSpot + end + end + end end - return index + return false end, --- Prefetches a scenario to try and reduce the loading screen time. @@ -528,7 +518,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC self:SendData(data.SenderID, { Type = "UpdateGameOptions", GameOptions = self.GameOptions }) -- sync player options to all connected peers - self:BroadcastData({ Type = "UpdatePlayerOptions", GameOptions = self.PlayerOptions }) + self:BroadcastData({ Type = "UpdatePlayerOptions", PlayerOptions = self.PlayerOptions }) end, ---@param self UIAutolobbyCommunications @@ -623,10 +613,6 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC DisconnectFromPeer = function(self, peerId) self:DebugSpew("DisconnectFromPeer", peerId) - -- reset mappings - self.PeerToIndexMapping = {} - self.ConnectionMatrix[peerId] = nil - self.DisconnectedPeers[peerId] = GetSystemTimeSeconds() return MohoLobbyMethods.DisconnectFromPeer(self, peerId) end, @@ -877,7 +863,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC local messageType = AutolobbyMessages[data.Type] -- signal UI that we received something - local peerIndex = self:PeerIdToIndex(data.SenderID) + local peerIndex = self:PeerIdToIndex(self.PlayerOptions, data.SenderID) if peerIndex then import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() :UpdateIsAliveStamp(peerIndex) @@ -911,17 +897,6 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC ---@param peerId UILobbyPeerId PeerDisconnected = function(self, peerName, peerId) self:DebugSpew("PeerDisconnected", peerName, peerId) - - -- reset mapping - self.PeerToIndexMapping = {} - - self.LaunchStatutes[peerId] = nil - -- update UI for launch statuses - import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() - :UpdateLaunchStatuses(self:CreateConnectionStatuses(self.LaunchStatutes)) - - self.ConnectionMatrix[peerId] = nil - self.DisconnectedPeers[peerId] = GetSystemTimeSeconds() end, --- Called by the engine when the game is launched. From 8c4ce67a7c4a434be71d3184075718ddda00d837 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sat, 26 Oct 2024 14:03:59 +0200 Subject: [PATCH 54/83] Remove code that is not used --- lua/ui/lobby/autolobby/AutolobbyController.lua | 8 -------- 1 file changed, 8 deletions(-) diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index 070ed6f8c8..d86b2df7f2 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -103,14 +103,6 @@ local AutolobbyEngineStrings = { ---@field JoinParameters? UIAutolobbyJoinParameters # Used for rejoining functionality AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsComponent, DebugComponent) { - BackgroundTextures = { - "/menus02/background-paint01_bmp.dds", - "/menus02/background-paint02_bmp.dds", - "/menus02/background-paint03_bmp.dds", - "/menus02/background-paint04_bmp.dds", - "/menus02/background-paint05_bmp.dds", - }, - ---@param self UIAutolobbyCommunications __init = function(self) self.Trash = TrashBag() From fa9a3d6a3d3ef27d13fbb0c0eee0f56bfde41cac Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sat, 26 Oct 2024 14:33:51 +0200 Subject: [PATCH 55/83] Add message validation to perform basic checks of the format of a message This won't help against malicious messages, but it does help with debugging. --- .../lobby/autolobby/AutolobbyController.lua | 51 +++++++++---- lua/ui/lobby/autolobby/AutolobbyMessages.lua | 74 ++++++++++++++++++- 2 files changed, 108 insertions(+), 17 deletions(-) diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index d86b2df7f2..0f067a87b6 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -110,7 +110,6 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC self.LocalPeerId = "-2" self.LocalPlayerName = "Charlie" self.PlayerCount = tonumber(GetCommandLineArg("/players", 1)[1]) or 2 - self.Connections = {} self.HostID = "-2" self.GameMods = {} @@ -561,8 +560,18 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC ---@param data UILobbyData BroadcastData = function(self, data) self:DebugSpew("BroadcastData", data.Type) - if not AutolobbyMessages[data.Type] then - self:DebugWarn("Broadcasting unknown message type", data.Type) + + -- validate message type + local message = AutolobbyMessages[data.Type] + if not message then + self:DebugWarn("Blocked broadcasting unknown message type", data.Type) + return + end + + -- validate message format + if not message.Validate(self, data) then + self:DebugWarn("Blocked broadcasting malformed message of type", data.Type) + return end return MohoLobbyMethods.BroadcastData(self, data) @@ -723,8 +732,18 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC ---@return nil SendData = function(self, peerId, data) self:DebugSpew("SendData", peerId, data.Type) - if not AutolobbyMessages[data.Type] then - self:DebugWarn("Sending unknown message type", data.Type, "to", peerId) + + -- validate message type + local message = AutolobbyMessages[data.Type] + if not message then + self:DebugWarn("Blocked sending unknown message type", data.Type, "to", peerId) + return + end + + -- validate message type + if not message.Validate(self, data) then + self:DebugWarn("Blocked sending malformed message of type", data.Type, "to", peerId) + return end return MohoLobbyMethods.SendData(self, peerId, data) @@ -851,9 +870,6 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC DataReceived = function(self, data) self:DebugSpew("DataReceived", data.Type, data.SenderID, data.SenderName) - ---@type UIAutolobbyMessageHandler? - local messageType = AutolobbyMessages[data.Type] - -- signal UI that we received something local peerIndex = self:PeerIdToIndex(self.PlayerOptions, data.SenderID) if peerIndex then @@ -861,20 +877,27 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC :UpdateIsAliveStamp(peerIndex) end - -- verify that the message type exists - if not messageType then - self:DebugError('Unknown message received: ', data.Type) + -- validate message type + local message = AutolobbyMessages[data.Type] + if not message then + self:DebugWarn("Ignoring unknown message type", data.Type, "from", data.SenderID) + return + end + + -- validate message data + if not message.Validate(self, data) then + self:DebugWarn("Ignoring malformed message of type", data.Type, "from", data.SenderID) return end - -- verify that we can accept it - if not messageType.Accept(self, data) then + -- validate message source + if not message.Accept(self, data) then self:DebugWarn("Message rejected: ", data.Type) return end -- handle the message - messageType.Handler(self, data) + message.Handler(self, data) end, --- Called by the engine when the game configuration is requested by the discovery service. diff --git a/lua/ui/lobby/autolobby/AutolobbyMessages.lua b/lua/ui/lobby/autolobby/AutolobbyMessages.lua index 0479e5cd09..6cb56b3e3a 100644 --- a/lua/ui/lobby/autolobby/AutolobbyMessages.lua +++ b/lua/ui/lobby/autolobby/AutolobbyMessages.lua @@ -27,7 +27,8 @@ -- wrapper to another function in the autolobby. ---@class UIAutolobbyMessageHandler ----@field Accept fun(lobby: UIAutolobbyCommunications, data: UILobbyReceivedMessage): boolean # Responsible for filtering out non-sense +---@field Validate fun(lobby: UIAutolobbyCommunications, data: UILobbyReceivedMessage): boolean # Responsible for filtering out non-sense +---@field Accept fun(lobby: UIAutolobbyCommunications, data: UILobbyReceivedMessage): boolean # Responsible for filtering out malicous messages ---@field Handler fun(lobby: UIAutolobbyCommunications, data: UILobbyReceivedMessage) # Responsible for handling the message ---@param lobby UIAutolobbyCommunications @@ -49,6 +50,13 @@ AutolobbyMessages = { ---@class UIAutolobbyIsAliveMessage : UILobbyReceivedMessage + ---@param lobby UIAutolobbyCommunications + ---@param data UIAutolobbyIsAliveMessage + ---@return boolean + Validate = function(lobby, data) + return true + end, + ---@param lobby UIAutolobbyCommunications ---@param data UIAutolobbyIsAliveMessage ---@return boolean @@ -67,6 +75,17 @@ AutolobbyMessages = { ---@class UIAutolobbyUpdateLaunchStatusMessage : UILobbyReceivedMessage ---@field LaunchStatus UIPeerLaunchStatus + ---@param lobby UIAutolobbyCommunications + ---@param data UIAutolobbyUpdateLaunchStatusMessage + ---@return boolean + Validate = function(lobby, data) + if not data.LaunchStatus then + return false + end + + return true + end, + ---@param lobby UIAutolobbyCommunications ---@param data UIAutolobbyUpdateLaunchStatusMessage ---@return boolean @@ -87,6 +106,17 @@ AutolobbyMessages = { ---@class UIAutolobbyAddPlayerMessage : UILobbyReceivedMessage ---@field PlayerOptions UIAutolobbyPlayer + ---@param lobby UIAutolobbyCommunications + ---@param data UIAutolobbyAddPlayerMessage + ---@return boolean + Validate = function(lobby, data) + if not data.PlayerOptions then + return false + end + + return true + end, + ---@param lobby UIAutolobbyCommunications ---@param data UIAutolobbyAddPlayerMessage ---@return boolean @@ -127,6 +157,17 @@ AutolobbyMessages = { ---@class UIAutolobbyUpdatePlayerOptionsMessage : UILobbyReceivedMessage ---@field PlayerOptions UIAutolobbyPlayer[] + ---@param lobby UIAutolobbyCommunications + ---@param data UIAutolobbyUpdatePlayerOptionsMessage + ---@return boolean + Validate = function(lobby, data) + if not data.PlayerOptions then + return false + end + + return true + end, + ---@param lobby UIAutolobbyCommunications ---@param data UIAutolobbyUpdatePlayerOptionsMessage ---@return boolean @@ -136,8 +177,6 @@ AutolobbyMessages = { return false end - -- TODO: verify integrity of the message - return true end, @@ -153,6 +192,17 @@ AutolobbyMessages = { ---@class UIAutolobbyUpdateGameOptionsMessage : UILobbyReceivedMessage ---@field GameOptions UILobbyLaunchGameOptionsConfiguration + ---@param lobby UIAutolobbyCommunications + ---@param data UIAutolobbyUpdateGameOptionsMessage + ---@return boolean + Validate = function(lobby, data) + if not data.GameOptions then + return false + end + + return true + end, + ---@param lobby UIAutolobbyCommunications ---@param data UIAutolobbyUpdateGameOptionsMessage ---@return boolean @@ -179,6 +229,24 @@ AutolobbyMessages = { ---@class UIAutolobbyLaunchMessage : UILobbyReceivedMessage ---@field GameConfig UILobbyLaunchConfiguration + ---@param lobby UIAutolobbyCommunications + ---@param data UIAutolobbyLaunchMessage + ---@return boolean + Validate = function(lobby, data) + if not data.GameConfig then + return false + end + + if not + ( + data.GameConfig.GameMods and data.GameConfig.GameOptions and data.GameConfig.Observers and + data.GameConfig.PlayerOptions) then + return false + end + + return true + end, + ---@param lobby UIAutolobbyCommunications ---@param data UIAutolobbyLaunchMessage ---@return boolean From 24fdbd36271b633c47a5c752687d6ab34cdc2b61 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sat, 26 Oct 2024 15:07:51 +0200 Subject: [PATCH 56/83] Fix functions not being pure --- .../lobby/autolobby/AutolobbyController.lua | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index 0f067a87b6..0c1636f9b1 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -209,9 +209,10 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC --#region Utilities ---@param self UIAutolobbyCommunications + ---@param playerOptions UIAutolobbyPlayer[] ---@param connectionMatrix table ---@return UIAutolobbyConnections - CreateConnectionsMatrix = function(self, connectionMatrix) + CreateConnectionsMatrix = function(self, playerOptions, connectionMatrix) ---@type UIAutolobbyConnections local connections = {} @@ -226,8 +227,8 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC -- populate the matrix for peerId, establishedPeers in connectionMatrix do for _, peerConnectedToId in establishedPeers do - local peerIdNumber = self:PeerIdToIndex(self.PlayerOptions, peerId) - local peerConnectedToIdNumber = self:PeerIdToIndex(self.PlayerOptions, peerConnectedToId) + local peerIdNumber = self:PeerIdToIndex(playerOptions, peerId) + local peerConnectedToIdNumber = self:PeerIdToIndex(playerOptions, peerConnectedToId) -- connection works both ways if peerIdNumber and peerConnectedToIdNumber then @@ -245,12 +246,13 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC end, ---@param self UIAutolobbyCommunications + ---@param playerOptions UIAutolobbyPlayer[] ---@param statuses table ---@return UIAutolobbyStatus - CreateConnectionStatuses = function(self, statuses) + CreateConnectionStatuses = function(self, playerOptions, statuses) local output = {} for peerId, launchStatus in statuses do - local peerIdNumber = self:PeerIdToIndex(self.PlayerOptions, peerId) + local peerIdNumber = self:PeerIdToIndex(playerOptions, peerId) if peerIdNumber then output[peerIdNumber] = launchStatus end @@ -439,7 +441,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC -- update UI for launch statuses import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() - :UpdateLaunchStatuses(self:CreateConnectionStatuses(self.LaunchStatutes)) + :UpdateLaunchStatuses(self:CreateConnectionStatuses(self.PlayerOptions, self.LaunchStatutes)) WaitSeconds(2.0) end @@ -547,7 +549,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC -- update UI for launch statuses import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() - :UpdateLaunchStatuses(self:CreateConnectionStatuses(self.LaunchStatutes)) + :UpdateLaunchStatuses(self:CreateConnectionStatuses(self.PlayerOptions, self.LaunchStatutes)) end, --#endregion @@ -833,11 +835,11 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC self.LaunchStatutes[peerId] = self.LaunchStatutes[peerId] or 'Unknown' -- update UI for launch statuses import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() - :UpdateLaunchStatuses(self:CreateConnectionStatuses(self.LaunchStatutes)) + :UpdateLaunchStatuses(self:CreateConnectionStatuses(self.PlayerOptions, self.LaunchStatutes)) -- update the matrix and the UI self.ConnectionMatrix[peerId] = peerConnectedTo - local connections = self:CreateConnectionsMatrix(self.ConnectionMatrix) + local connections = self:CreateConnectionsMatrix(self.PlayerOptions, self.ConnectionMatrix) import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() :UpdateConnections(connections) end, From f0439212b1eb1313a45de792b5070c79b62cb184 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sat, 26 Oct 2024 15:48:50 +0200 Subject: [PATCH 57/83] Cleaning up of logic --- .../lobby/autolobby/AutolobbyController.lua | 1 - lua/ui/lobby/autolobby/AutolobbyMessages.lua | 25 ------------------- 2 files changed, 26 deletions(-) diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index 0c1636f9b1..e822f5cf31 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -54,7 +54,6 @@ local AutolobbyEngineStrings = { ['LaunchRejected'] = "Some players are using an incompatible client version.", } - ---@class UIAutolobbyPlayer: UILobbyLaunchPlayerConfiguration ---@field StartSpot number ---@field DEV number # Related to rating/divisions diff --git a/lua/ui/lobby/autolobby/AutolobbyMessages.lua b/lua/ui/lobby/autolobby/AutolobbyMessages.lua index 6cb56b3e3a..597daa97d8 100644 --- a/lua/ui/lobby/autolobby/AutolobbyMessages.lua +++ b/lua/ui/lobby/autolobby/AutolobbyMessages.lua @@ -46,31 +46,6 @@ end --- Represents all valid message types that can be sent between peers. ---@type table AutolobbyMessages = { - IsAlive = { - - ---@class UIAutolobbyIsAliveMessage : UILobbyReceivedMessage - - ---@param lobby UIAutolobbyCommunications - ---@param data UIAutolobbyIsAliveMessage - ---@return boolean - Validate = function(lobby, data) - return true - end, - - ---@param lobby UIAutolobbyCommunications - ---@param data UIAutolobbyIsAliveMessage - ---@return boolean - Accept = function(lobby, data) - return true - end, - - ---@param lobby UIAutolobbyCommunications - ---@param data UIAutolobbyIsAliveMessage - Handler = function(lobby, data) - -- do nothing, we're interested in the side effect - end - }, - UpdateLaunchStatus = { ---@class UIAutolobbyUpdateLaunchStatusMessage : UILobbyReceivedMessage ---@field LaunchStatus UIPeerLaunchStatus From 0e1edb51e6aa7d249015f5914f06c2c347eae734 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sat, 26 Oct 2024 16:06:52 +0200 Subject: [PATCH 58/83] Undo changes to the launch script --- scripts/LaunchFAInstances.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/LaunchFAInstances.ps1 b/scripts/LaunchFAInstances.ps1 index 648fc8f111..8a22fca4a6 100644 --- a/scripts/LaunchFAInstances.ps1 +++ b/scripts/LaunchFAInstances.ps1 @@ -1,5 +1,5 @@ param ( - [int]$players = 4, # Default to 2 instances (1 host, 1 client) + [int]$players = 2, # Default to 2 instances (1 host, 1 client) [string]$map = "/maps/scmp_009/SCMP_009_scenario.lua", # Default map: Seton's Clutch [int]$port = 15000, # Default port for hosting the game [int]$teams = 2 # Default to two teams, 0 for FFA From 5e0547d06dd8bdbc08d485a0c5ea197b8681c0a5 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sat, 26 Oct 2024 16:10:19 +0200 Subject: [PATCH 59/83] Improve annotations --- lua/ui/maputil.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lua/ui/maputil.lua b/lua/ui/maputil.lua index 8479588e52..6c57dd5911 100644 --- a/lua/ui/maputil.lua +++ b/lua/ui/maputil.lua @@ -229,7 +229,9 @@ function GetStartPositions(scenario) return armyPositions end --- enumerates and returns to key name for all the armies for this map +-- Retrieves all of the playable armies for a scenario +---@param scenario UIScenarioInfo +---@return string[] | nil function GetArmies(scenario) local retArmies = nil From d302a3ed8324ea9d33535c5979753158c6f30c41 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sat, 26 Oct 2024 16:55:13 +0200 Subject: [PATCH 60/83] Add annotations for scenario save file --- lua/ui/maputil.lua | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/lua/ui/maputil.lua b/lua/ui/maputil.lua index 6c57dd5911..f3123a2941 100644 --- a/lua/ui/maputil.lua +++ b/lua/ui/maputil.lua @@ -10,6 +10,45 @@ ---@field teams {name: string, armies: string[]} ---@field customprops table +--- A basic area defined in the scenario. +---@class UIScenarioArea +---@field [1] number # x0 +---@field [2] number # z0 +---@field [3] number # x1 +---@field [4] number # z1 +---@field type 'RECTANGLE' + +--- A marker defined in the scenario. +---@class UIScenarioMarker +---@field color string +---@field type string +---@field prop BlueprintId # path to blueprint +---@field orientation Vector +---@field position Vector + +--- A chain of markers defined in the scenario. +---@class UIScenarioChain +---@field Markers string[] # key of marker in the master chain + +--- An army defined in the scenario. +---@class UIScenarioArmy +---@field personality string +---@field plans string +---@field color number +---@field faction number +---@field Economy { mass: number, energy: number } +---@field Alliances table +---@field PlatoonBuilders { Builders: table } + +---@class UIScenario +---@field Props table # Unknown +---@field Areas table +---@field MasterChain { _MASTERCHAIN_ : table } +---@field Chains table +---@field Orders table # Unknown +---@field Platoons table # Unknown +---@field Armies table + ---@class UIScenarioInfo ---@field AdaptiveMap boolean ---@field description string From e42abe8e3d62676a96c2fb3dc49ed1c16dca8419 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sat, 26 Oct 2024 16:56:24 +0200 Subject: [PATCH 61/83] Add documentation --- lua/ui/maputil.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/ui/maputil.lua b/lua/ui/maputil.lua index f3123a2941..f0d84845a2 100644 --- a/lua/ui/maputil.lua +++ b/lua/ui/maputil.lua @@ -40,6 +40,7 @@ ---@field Alliances table ---@field PlatoonBuilders { Builders: table } +--- The scenario of a map that defines all areas, (resource) markers, marker chains and armies. ---@class UIScenario ---@field Props table # Unknown ---@field Areas table From 53d8a5ae558fa8b28656fbcccbe28c05bc57c77f Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sun, 27 Oct 2024 07:13:10 +0100 Subject: [PATCH 62/83] Apply the map utils pull request --- .../lobby/autolobby/AutolobbyController.lua | 5 +++++ .../lobby/autolobby/AutolobbyMapPreview.lua | 22 +++++++++---------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index e822f5cf31..921433899b 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -333,6 +333,11 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC end local scenarioFile = MapUtil.LoadScenario(gameOptions.ScenarioFile) + if not scenarioFile then + -- ??? + return + end + PrefetchSession(scenarioFile.map, gameMods, true) end, diff --git a/lua/ui/lobby/autolobby/AutolobbyMapPreview.lua b/lua/ui/lobby/autolobby/AutolobbyMapPreview.lua index 1e6a549042..28e6266656 100644 --- a/lua/ui/lobby/autolobby/AutolobbyMapPreview.lua +++ b/lua/ui/lobby/autolobby/AutolobbyMapPreview.lua @@ -30,8 +30,8 @@ local MapPreview = import("/lua/ui/controls/mappreview.lua").MapPreview ---@class UIAutolobbyMapPreview : Group ---@field Preview MapPreview ---@field Border Control ----@field Scenario? string ----@field ScenarioInfo? UIScenarioInfo +---@field PathToScenarioFile? FileName +---@field ScenarioInfo? UILobbyScenarioInfo ---@field EnergyIcon Bitmap # Acts as a pool ---@field MassIcon Bitmap # Acts as a pool ---@field WreckageIcon Bitmap # Acts as a pool @@ -130,7 +130,7 @@ local AutolobbyMapPreview = ClassUI(Group) { --- --- This function is private and should not be called from outside the class. ---@param self UIAutolobbyMapPreview - ---@param scenarioInfo UIScenarioInfo + ---@param scenarioInfo UILobbyScenarioInfo _UpdatePreview = function(self, scenarioInfo) if not self.Preview:SetTexture(scenarioInfo.preview) then self.Preview:SetTextureFromMap(scenarioInfo.map) @@ -141,7 +141,7 @@ local AutolobbyMapPreview = ClassUI(Group) { --- --- This function is private and should not be called from outside the class. ---@param self UIAutolobbyMapPreview - ---@param scenarioInfo UIScenarioInfo + ---@param scenarioInfo UILobbyScenarioInfo _UpdateMarkers = function(self, scenarioInfo) local scenarioWidth = scenarioInfo.size[1] local scenarioHeight = scenarioInfo.size[2] @@ -180,7 +180,7 @@ local AutolobbyMapPreview = ClassUI(Group) { --- --- This function is private and should not be called from outside the class. ---@param self UIAutolobbyMapPreview - ---@param scenarioInfo UIScenarioInfo + ---@param scenarioInfo UILobbyScenarioInfo _UpdateWreckages = function(self, scenarioInfo) -- TODO end, @@ -189,20 +189,20 @@ local AutolobbyMapPreview = ClassUI(Group) { --- --- This function is private and should not be called from outside the class. ---@param self UIAutolobbyMapPreview - ---@param scenarioInfo UIScenarioInfo + ---@param scenarioInfo UILobbyScenarioInfo _UpdateSpawnLocations = function(self, scenarioInfo) -- TODO end, ---@param self UIAutolobbyMapPreview - ---@param scenario string # a reference to a _scenario.lua file - UpdateScenario = function(self, scenario) + ---@param pathToScenarioInfo FileName # a reference to a _scenario.lua file + UpdateScenario = function(self, pathToScenarioInfo) -- clear up previous iteration self.IconTrash:Destroy() self.Preview:ClearTexture() - self.Scenario = scenario - self.ScenarioInfo = MapUtil.LoadScenario(scenario) + self.PathToScenarioFile = pathToScenarioInfo + self.ScenarioInfo = MapUtil.LoadScenario(pathToScenarioInfo) if self.ScenarioInfo then self:_UpdatePreview(self.ScenarioInfo) self:_UpdateMarkers(self.ScenarioInfo) @@ -231,4 +231,4 @@ local AutolobbyMapPreview = ClassUI(Group) { ---@return UIAutolobbyMapPreview GetInstance = function(parent) return AutolobbyMapPreview(parent) -end \ No newline at end of file +end From 179ad59e423ea3bb4b74c80aedae12547304f19b Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sun, 27 Oct 2024 07:13:19 +0100 Subject: [PATCH 63/83] Fix issue with hot reload --- lua/ui/lobby/autolobby/AutolobbyController.lua | 9 ++++++++- lua/ui/lobby/autolobby/AutolobbyInterface.lua | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index 921433899b..e5ba4691ed 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -116,6 +116,12 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC self.PlayerOptions = {} self.LaunchStatutes = {} self.ConnectionMatrix = {} + + -- local meta = getmetatable(self) + -- meta.__index = function(self, key) + -- LOG(key) + -- return meta[key] + -- end end, ---@param self UIAutolobbyCommunications @@ -453,10 +459,11 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC ---@param self UIAutolobbyCommunications LaunchThread = function(self) - while not IsDestroyed(self) do + while not IsDestroyed(self) and false do if self:CanLaunch(self.LaunchStatutes) then + WaitSeconds(5.0) if (not IsDestroyed(self)) and self:CanLaunch(self.LaunchStatutes) then diff --git a/lua/ui/lobby/autolobby/AutolobbyInterface.lua b/lua/ui/lobby/autolobby/AutolobbyInterface.lua index be47f9d390..a9aa090e1c 100644 --- a/lua/ui/lobby/autolobby/AutolobbyInterface.lua +++ b/lua/ui/lobby/autolobby/AutolobbyInterface.lua @@ -71,7 +71,7 @@ local AutolobbyInterface = Class(Group) { local backgroundTexture = self.BackgroundTextures[math.random(1, 5)] --[[@as FileName]] self.Background = UIUtil.CreateBitmap(self, backgroundTexture) self.Preview = AutolobbyMapPreview.GetInstance(self) - self.ConnectionMatrix = AutolobbyConnectionMatrix.Create(self, playerCount) -- TODO: determine this number dynamically + self.ConnectionMatrix = AutolobbyConnectionMatrix.Create(self, playerCount) end, ---@param self UIAutolobbyInterface @@ -224,7 +224,7 @@ end ---@param newModule any function __moduleinfo.OnReload(newModule) if AutolobbyInterfaceInstance then - local handle = newModule.SetupSingleton(GetFrame(0), AutolobbyInterfaceInstance.State.PlayerCount) + local handle = newModule.SetupSingleton(AutolobbyInterfaceInstance.State.PlayerCount) handle:RestoreState(AutolobbyInterfaceInstance.State) end end From bfa9d1d62a341c777998e6939d52ac4a369dc5d0 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sun, 27 Oct 2024 07:22:29 +0100 Subject: [PATCH 64/83] Process feedback of Askaholic --- ...AutolobbyServerCommunicationsComponent.lua | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/lua/ui/lobby/autolobby/components/AutolobbyServerCommunicationsComponent.lua b/lua/ui/lobby/autolobby/components/AutolobbyServerCommunicationsComponent.lua index 6dbbe0b438..c89e55be99 100644 --- a/lua/ui/lobby/autolobby/components/AutolobbyServerCommunicationsComponent.lua +++ b/lua/ui/lobby/autolobby/components/AutolobbyServerCommunicationsComponent.lua @@ -51,7 +51,7 @@ local GpgNetSend = GpgNetSend --- | 'Missing local peers' # Send when the local peer is missing other peers --- | 'Rejoining' # Send when the local peer is rejoining --- | 'Ready' # Send when the local peer is ready to launch ---- | 'Ejected' # Send when the local peer is ejected +--- | 'Ejected' # Send when the local peer is ejected --- | 'Rejected' # Send when there is a game version missmatch --- | 'Failed' # Send when the game fails to launch @@ -59,6 +59,7 @@ local GpgNetSend = GpgNetSend ---@class UIAutolobbyServerCommunicationsComponent AutolobbyServerCommunicationsComponent = ClassSimple { + --- Sends a message to the server to update relevant army options of a player. ---@param self UIAutolobbyServerCommunicationsComponent | UIAutolobbyCommunications ---@param peerId UILobbyPeerId ---@param key 'Team' | 'Army' | 'StartSpot' | 'Faction' @@ -66,12 +67,14 @@ AutolobbyServerCommunicationsComponent = ClassSimple { SendPlayerOptionToServer = function(self, peerId, key, value) -- message is only accepted by the server if it originates from the host if not self:IsHost() then + self:DebugWarn("Ignoring server message of type `PlayerOption` since that is only accepted when it originates from the host.") return end GpgNetSend('PlayerOption', peerId, key, value) end, + --- Sends a message to the server to update relevant army options of an AI. ---@param self UIAutolobbyServerCommunicationsComponent | UIAutolobbyCommunications ---@param aiName string ---@param key 'Team' | 'Army' | 'StartSpot' | 'Faction' @@ -79,53 +82,52 @@ AutolobbyServerCommunicationsComponent = ClassSimple { SendAIOptionToServer = function(self, aiName, key, value) -- message is only accepted by the server if it originates from the host if not self:IsHost() then + self:DebugWarn("Ignoring server message of type `AIOption` since that is only accepted when it originates from the host.") return end GpgNetSend('AIOption', aiName, key, value) end, + --- Sends a message to the server to update relevant game options. ---@param self UIAutolobbyServerCommunicationsComponent | UIAutolobbyCommunications ---@param key 'Slots' | any ---@param value any SendGameOptionToServer = function(self, key, value) -- message is only accepted by the server if it originates from the host if not self:IsHost() then + self:DebugWarn("Ignoring server message of type `GameOption` since that is only accepted when it originates from the host.") return end GpgNetSend('GameOption', key, value) end, + --- Sends a message to the server indicating what the status of the lobby as a whole. ---@param self UIAutolobbyServerCommunicationsComponent | UIAutolobbyCommunications ---@param value UILobbyState SendGameStateToServer = function(self, value) GpgNetSend('GameState', value) end, + --- sends a message to the server about the status of the local peer. ---@param self UIAutolobbyServerCommunicationsComponent | UIAutolobbyCommunications ---@param value UIPeerLaunchStatus SendLaunchStatusToServer = function(self, value) GpgNetSend('LaunchStatus', value) end, + --- Sends a message to the server that we established a connection to a peer. This message can be send multiple times for the same peer and the server should be idempotent to it. ---@param self UIAutolobbyServerCommunicationsComponent | UIAutolobbyCommunications ---@param peerId UILobbyPeerId - ---@param peers UILobbyPeerId[] - SendEstablishedPeers = function(self, peerId, peers) - local establishedPeers = "" - - local establishedPeersCount = table.getn(peers) - if establishedPeersCount == 1 then - establishedPeers = peers[1] - elseif establishedPeersCount > 1 then - establishedPeers = peers[1] - - for k = 2, establishedPeersCount do - establishedPeers = establishedPeers .. " " .. peers[k] - end - end + SendEstablishedPeer = function(self, peerId) + GpgNetSend('EstablishedPeers', peerId) + end, - GpgNetSend('EstablishedPeers', peerId, establishedPeers) + --- Sends a message to the server that we disconnected from a peer. Note that a peer may be trying to rejoin. See also the launch status of the given peer. + ---@param self UIAutolobbyServerCommunicationsComponent | UIAutolobbyCommunications + ---@param peerId UILobbyPeerId + SendDisconnectedPeer = function(self, peerId) + GpgNetSend('DisconnectedPeer', peerId) end, } From c79983e7d1f3f8a3c06a5eeae852f7e24ffcdf55 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sun, 27 Oct 2024 07:23:35 +0100 Subject: [PATCH 65/83] Send the disconnect message to the server --- lua/ui/lobby/autolobby/AutolobbyController.lua | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index e5ba4691ed..522fe9c6f3 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -116,12 +116,6 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC self.PlayerOptions = {} self.LaunchStatutes = {} self.ConnectionMatrix = {} - - -- local meta = getmetatable(self) - -- meta.__index = function(self, key) - -- LOG(key) - -- return meta[key] - -- end end, ---@param self UIAutolobbyCommunications @@ -841,7 +835,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC self:DebugSpew("EstablishedPeers", peerId, reprs(peerConnectedTo)) -- update server - self:SendEstablishedPeers(peerId, peerConnectedTo) + self:SendEstablishedPeers(peerId) self.LaunchStatutes[peerId] = self.LaunchStatutes[peerId] or 'Unknown' -- update UI for launch statuses @@ -925,6 +919,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC ---@param peerId UILobbyPeerId PeerDisconnected = function(self, peerName, peerId) self:DebugSpew("PeerDisconnected", peerName, peerId) + self:SendDisconnectedPeer(peerId) end, --- Called by the engine when the game is launched. From bd8f664503d5ecb3d993dc9051f61eb383386c71 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sun, 27 Oct 2024 07:27:46 +0100 Subject: [PATCH 66/83] Fix a typo --- lua/ui/lobby/autolobby/AutolobbyController.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index 522fe9c6f3..9e198b8d44 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -835,7 +835,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC self:DebugSpew("EstablishedPeers", peerId, reprs(peerConnectedTo)) -- update server - self:SendEstablishedPeers(peerId) + self:SendEstablishedPeer(peerId) self.LaunchStatutes[peerId] = self.LaunchStatutes[peerId] or 'Unknown' -- update UI for launch statuses From 232660e7665132da51fc4fa13afd913df29fc77c Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sun, 27 Oct 2024 07:27:53 +0100 Subject: [PATCH 67/83] Improve documentation --- lua/ui/maputil.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/ui/maputil.lua b/lua/ui/maputil.lua index e74974a92a..2b3b31285c 100644 --- a/lua/ui/maputil.lua +++ b/lua/ui/maputil.lua @@ -148,7 +148,7 @@ function GetPathToScenarioStrings(pathToScenarioInfo) "strings.lua" --[[@as FileName]] end ---- Loads in the scenario save. +--- Loads in the scenario save. This function is expensive and should be used sparingly. ---@param pathToScenarioSave FileName ---@return UIScenarioSaveFile | nil function LoadScenarioSaveFile(pathToScenarioSave) From f621a31790e0e8274d47bef6f3ff96669d0fe344 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sun, 27 Oct 2024 07:29:13 +0100 Subject: [PATCH 68/83] Remove debug code --- lua/ui/lobby/autolobby/AutolobbyController.lua | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index 9e198b8d44..4c02fe55cb 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -453,11 +453,9 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC ---@param self UIAutolobbyCommunications LaunchThread = function(self) - while not IsDestroyed(self) and false do - + while not IsDestroyed(self) do if self:CanLaunch(self.LaunchStatutes) then - WaitSeconds(5.0) if (not IsDestroyed(self)) and self:CanLaunch(self.LaunchStatutes) then From 750e51e1b935200716a47384a2f29afba67a6b97 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sun, 27 Oct 2024 15:48:32 +0100 Subject: [PATCH 69/83] Fix typo --- .../components/AutolobbyServerCommunicationsComponent.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/ui/lobby/autolobby/components/AutolobbyServerCommunicationsComponent.lua b/lua/ui/lobby/autolobby/components/AutolobbyServerCommunicationsComponent.lua index c89e55be99..c3fb273ac1 100644 --- a/lua/ui/lobby/autolobby/components/AutolobbyServerCommunicationsComponent.lua +++ b/lua/ui/lobby/autolobby/components/AutolobbyServerCommunicationsComponent.lua @@ -121,7 +121,7 @@ AutolobbyServerCommunicationsComponent = ClassSimple { ---@param self UIAutolobbyServerCommunicationsComponent | UIAutolobbyCommunications ---@param peerId UILobbyPeerId SendEstablishedPeer = function(self, peerId) - GpgNetSend('EstablishedPeers', peerId) + GpgNetSend('EstablishedPeer', peerId) end, --- Sends a message to the server that we disconnected from a peer. Note that a peer may be trying to rejoin. See also the launch status of the given peer. From 1922ac65c56d3041af1f0935d39ebbf428e1747d Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sun, 27 Oct 2024 18:37:12 +0100 Subject: [PATCH 70/83] Show what row/column is of the local client in connection matrix --- .../autolobby/AutolobbyConnectionMatrix.lua | 14 +++++++ .../AutolobbyConnectionMatrixDot.lua | 12 +++++- .../lobby/autolobby/AutolobbyController.lua | 37 ++++++++++++++++++- lua/ui/lobby/autolobby/AutolobbyInterface.lua | 9 +++++ 4 files changed, 70 insertions(+), 2 deletions(-) diff --git a/lua/ui/lobby/autolobby/AutolobbyConnectionMatrix.lua b/lua/ui/lobby/autolobby/AutolobbyConnectionMatrix.lua index 2dd96ef6f0..336c69b7c2 100644 --- a/lua/ui/lobby/autolobby/AutolobbyConnectionMatrix.lua +++ b/lua/ui/lobby/autolobby/AutolobbyConnectionMatrix.lua @@ -75,6 +75,20 @@ local AutolobbyConnectionMatrix = Class(Group) { end end, + ---@param self UIAutolobbyConnectionMatrix + ---@param ownershipMatrix boolean[][] + UpdateOwnership = function(self, ownershipMatrix) + for y, connectionRow in ownershipMatrix do + for x, isOwned in connectionRow do + ---@type UIAutolobbyConnectionMatrixDot + local dot = self.Elements[y][x] + if dot and y ~= x then + dot:SetOwnership(isOwned) + end + end + end + end, + ---@param self UIAutolobbyConnectionMatrix ---@param connectionMatrix UIAutolobbyConnections UpdateConnections = function(self, connectionMatrix) diff --git a/lua/ui/lobby/autolobby/AutolobbyConnectionMatrixDot.lua b/lua/ui/lobby/autolobby/AutolobbyConnectionMatrixDot.lua index 2067b1ea29..8d7ef1a853 100644 --- a/lua/ui/lobby/autolobby/AutolobbyConnectionMatrixDot.lua +++ b/lua/ui/lobby/autolobby/AutolobbyConnectionMatrixDot.lua @@ -38,7 +38,7 @@ local AutolobbyConnectionMatrixDot = Class(Bitmap) { -- initial state self:SetConnected(false) - self:SetSolidColor('ffffff') + self:SetSolidColor('999999') end, ---@param self UIAutolobbyConnectionMatrixDot @@ -55,6 +55,16 @@ local AutolobbyConnectionMatrixDot = Class(Bitmap) { self:SetAlpha(math.max(0, 1 - (0.25 * diff))) end, + ---@param self UIAutolobbyConnectionMatrixDot + ---@param relatedToLocalPeer boolean + SetOwnership = function(self, relatedToLocalPeer) + if relatedToLocalPeer then + self:SetSolidColor('ffffff') + else + self:SetSolidColor('999999') + end + end, + ---@param self UIAutolobbyConnectionMatrixDot ---@param isConnected boolean SetConnected = function(self, isConnected) diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index 4c02fe55cb..c6b7e69bc6 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -260,6 +260,26 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC return output end, + ---@param self UIAutolobbyCommunications + ---@param playerCount number + ---@param localIndex number + ---@return boolean[][] + CreateOwnershipMatrix = function(self, playerCount, localIndex) + local output = {} + for y = 1, playerCount do + output[y] = {} + for x = 1, playerCount do + output[y][x] = false + end + end + + for k = 1, playerCount do + output[localIndex][k] = true + output[k][localIndex] = true + end + return output + end, + --- Determines the launch status of the local peer. ---@param self UIAutolobbyCommunications ---@param connectionMatrix table @@ -453,7 +473,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC ---@param self UIAutolobbyCommunications LaunchThread = function(self) - while not IsDestroyed(self) do + while not IsDestroyed(self) and false do if self:CanLaunch(self.LaunchStatutes) then WaitSeconds(5.0) @@ -515,6 +535,13 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC -- sync player options to all connected peers self:BroadcastData({ Type = "UpdatePlayerOptions", PlayerOptions = self.PlayerOptions }) + + local localIndex = self:PeerIdToIndex(self.PlayerOptions, self.LocalPeerId) + if localIndex then + local ownershipMatrix = self:CreateOwnershipMatrix(self.PlayerCount, localIndex) + import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() + :UpdateOwnership(ownershipMatrix) + end end, ---@param self UIAutolobbyCommunications @@ -525,6 +552,14 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC -- update UI for player options import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() :UpdatePlayerOptions(self.PlayerOptions) + + local localIndex = self:PeerIdToIndex(self.PlayerOptions, self.LocalPeerId) + if localIndex then + local ownershipMatrix = self:CreateOwnershipMatrix(self.PlayerCount, localIndex) + -- update UI for player options + import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() + :UpdateOwnership(ownershipMatrix) + end end, ---@param self UIAutolobbyCommunications diff --git a/lua/ui/lobby/autolobby/AutolobbyInterface.lua b/lua/ui/lobby/autolobby/AutolobbyInterface.lua index a9aa090e1c..07026161e0 100644 --- a/lua/ui/lobby/autolobby/AutolobbyInterface.lua +++ b/lua/ui/lobby/autolobby/AutolobbyInterface.lua @@ -98,6 +98,15 @@ local AutolobbyInterface = Class(Group) { :End() end, + ---@param self UIAutolobbyInterface + ---@param ownership boolean[][] + UpdateOwnership = function(self, ownership) + self.State.OwnerShip = ownership + + self.ConnectionMatrix:Show() + self.ConnectionMatrix:UpdateOwnership(ownership) + end, + ---@param self UIAutolobbyInterface ---@param connections UIAutolobbyConnections UpdateConnections = function(self, connections) From d36e0a71614f74d6d57df7aa5aa95361d9705870 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Wed, 30 Oct 2024 07:24:19 +0100 Subject: [PATCH 71/83] Add faction-spawn locations to map preview --- lua/ui/lobby/autolobby.lua | 5 +- .../lobby/autolobby/AutolobbyController.lua | 10 +- lua/ui/lobby/autolobby/AutolobbyInterface.lua | 31 +-- .../lobby/autolobby/AutolobbyMapPreview.lua | 178 ++++++++++++------ .../autolobby/AutolobbyMapPreviewSpawn.lua | 108 +++++++++++ lua/ui/maputil.lua | 70 +++++++ .../ui/common/faction_icon-lg/aeon_med.dds | Bin 0 -> 16512 bytes .../ui/common/faction_icon-lg/aeon_med.png | Bin 0 -> 3565 bytes .../ui/common/faction_icon-lg/aeon_mini.dds | Bin 0 -> 16512 bytes .../ui/common/faction_icon-lg/aeon_mini.png | Bin 0 -> 3563 bytes .../ui/common/faction_icon-lg/aeon_thick.dds | Bin 0 -> 18084 bytes .../ui/common/faction_icon-lg/aeon_thick.png | Bin 0 -> 3652 bytes .../ui/common/faction_icon-lg/cybran_med.dds | Bin 0 -> 16512 bytes .../ui/common/faction_icon-lg/cybran_med.png | Bin 0 -> 4117 bytes .../ui/common/faction_icon-lg/cybran_mini.dds | Bin 0 -> 16512 bytes .../ui/common/faction_icon-lg/cybran_mini.png | Bin 0 -> 4224 bytes .../common/faction_icon-lg/cybran_thick.dds | Bin 0 -> 18084 bytes .../common/faction_icon-lg/cybran_thick.png | Bin 0 -> 4129 bytes .../factionicons-mini_bordered.svg | 54 ++++++ .../common/faction_icon-lg/seraphim_med.dds | Bin 0 -> 16512 bytes .../common/faction_icon-lg/seraphim_med.png | Bin 0 -> 4733 bytes .../common/faction_icon-lg/seraphim_mini.dds | Bin 0 -> 16512 bytes .../common/faction_icon-lg/seraphim_mini.png | Bin 0 -> 5079 bytes .../common/faction_icon-lg/seraphim_thick.dds | Bin 0 -> 18084 bytes .../common/faction_icon-lg/seraphim_thick.png | Bin 0 -> 4863 bytes .../ui/common/faction_icon-lg/uef_med.dds | Bin 0 -> 16512 bytes .../ui/common/faction_icon-lg/uef_med.png | Bin 0 -> 2605 bytes .../ui/common/faction_icon-lg/uef_mini.dds | Bin 0 -> 16512 bytes .../ui/common/faction_icon-lg/uef_mini.png | Bin 0 -> 2500 bytes .../ui/common/faction_icon-lg/uef_thick.dds | Bin 0 -> 18084 bytes .../ui/common/faction_icon-lg/uef_thick.png | Bin 0 -> 2708 bytes 31 files changed, 369 insertions(+), 87 deletions(-) create mode 100644 lua/ui/lobby/autolobby/AutolobbyMapPreviewSpawn.lua create mode 100644 textures/ui/common/faction_icon-lg/aeon_med.dds create mode 100644 textures/ui/common/faction_icon-lg/aeon_med.png create mode 100644 textures/ui/common/faction_icon-lg/aeon_mini.dds create mode 100644 textures/ui/common/faction_icon-lg/aeon_mini.png create mode 100644 textures/ui/common/faction_icon-lg/aeon_thick.dds create mode 100644 textures/ui/common/faction_icon-lg/aeon_thick.png create mode 100644 textures/ui/common/faction_icon-lg/cybran_med.dds create mode 100644 textures/ui/common/faction_icon-lg/cybran_med.png create mode 100644 textures/ui/common/faction_icon-lg/cybran_mini.dds create mode 100644 textures/ui/common/faction_icon-lg/cybran_mini.png create mode 100644 textures/ui/common/faction_icon-lg/cybran_thick.dds create mode 100644 textures/ui/common/faction_icon-lg/cybran_thick.png create mode 100644 textures/ui/common/faction_icon-lg/factionicons-mini_bordered.svg create mode 100644 textures/ui/common/faction_icon-lg/seraphim_med.dds create mode 100644 textures/ui/common/faction_icon-lg/seraphim_med.png create mode 100644 textures/ui/common/faction_icon-lg/seraphim_mini.dds create mode 100644 textures/ui/common/faction_icon-lg/seraphim_mini.png create mode 100644 textures/ui/common/faction_icon-lg/seraphim_thick.dds create mode 100644 textures/ui/common/faction_icon-lg/seraphim_thick.png create mode 100644 textures/ui/common/faction_icon-lg/uef_med.dds create mode 100644 textures/ui/common/faction_icon-lg/uef_med.png create mode 100644 textures/ui/common/faction_icon-lg/uef_mini.dds create mode 100644 textures/ui/common/faction_icon-lg/uef_mini.png create mode 100644 textures/ui/common/faction_icon-lg/uef_thick.dds create mode 100644 textures/ui/common/faction_icon-lg/uef_thick.png diff --git a/lua/ui/lobby/autolobby.lua b/lua/ui/lobby/autolobby.lua index d29ff5d378..37b708c9d1 100644 --- a/lua/ui/lobby/autolobby.lua +++ b/lua/ui/lobby/autolobby.lua @@ -96,7 +96,7 @@ function HostGame(gameName, scenarioFileName, singlePlayer) -- :CreateLoadingDialog() end -local rejoinTest = true +local rejoinTest = false --- Joins an instantiated lobby instance. --- @@ -114,9 +114,6 @@ function JoinGame(address, asObserver, playerName, uid) AutolobbyCommunicationsInstance.JoinParameters.AsObserver = asObserver AutolobbyCommunicationsInstance.JoinParameters.DesiredPlayerName = playerName AutolobbyCommunicationsInstance.JoinParameters.DesiredPeerId = uid - end - - if AutolobbyCommunicationsInstance then AutolobbyCommunicationsInstance:JoinGame(address, playerName, uid) end diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index c6b7e69bc6..e006c88b84 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -536,6 +536,10 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC -- sync player options to all connected peers self:BroadcastData({ Type = "UpdatePlayerOptions", PlayerOptions = self.PlayerOptions }) + -- update UI for player options + import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() + :UpdateScenario(self.GameOptions.ScenarioFile, self.PlayerOptions) + local localIndex = self:PeerIdToIndex(self.PlayerOptions, self.LocalPeerId) if localIndex then local ownershipMatrix = self:CreateOwnershipMatrix(self.PlayerCount, localIndex) @@ -551,7 +555,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC -- update UI for player options import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() - :UpdatePlayerOptions(self.PlayerOptions) + :UpdateScenario(self.GameOptions.ScenarioFile, self.PlayerOptions) local localIndex = self:PeerIdToIndex(self.PlayerOptions, self.LocalPeerId) if localIndex then @@ -571,7 +575,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC -- update UI for game options import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() - :UpdateGameOptions(self.GameOptions) + :UpdateScenario(self.GameOptions.ScenarioFile, self.PlayerOptions) end, ---@param self UIAutolobbyCommunications @@ -821,7 +825,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC -- update UI for game options import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() - :UpdateGameOptions(self.GameOptions) + :UpdateScenario(self.GameOptions.ScenarioFile, self.PlayerOptions) end, --- Called by the engine as we're trying to join a lobby. diff --git a/lua/ui/lobby/autolobby/AutolobbyInterface.lua b/lua/ui/lobby/autolobby/AutolobbyInterface.lua index 07026161e0..dfbaf4e3a8 100644 --- a/lua/ui/lobby/autolobby/AutolobbyInterface.lua +++ b/lua/ui/lobby/autolobby/AutolobbyInterface.lua @@ -38,6 +38,7 @@ local AutolobbyConnectionMatrix = import("/lua/ui/lobby/autolobby/AutolobbyConne ---@class UIAutolobbyInterfaceState ---@field PlayerCount number ---@field PlayerOptions? table +---@field PathToScenarioFile? FileName ---@field GameOptions? UILobbyLaunchGameOptionsConfiguration ---@field Connections? UIAutolobbyConnections ---@field Statuses? UIAutolobbyStatus @@ -126,22 +127,15 @@ local AutolobbyInterface = Class(Group) { end, ---@param self UIAutolobbyInterface - ---@param playerOptions table - UpdatePlayerOptions = function(self, playerOptions) + ---@param pathToScenarioInfo FileName + ---@param playerOptions UIAutolobbyPlayer[] + UpdateScenario = function(self, pathToScenarioInfo, playerOptions) + self.State.ScenarioFile = pathToScenarioInfo self.State.PlayerOptions = playerOptions - end, - - ---@param self UIAutolobbyInterface - ---@param gameOptions UILobbyLaunchGameOptionsConfiguration - UpdateGameOptions = function(self, gameOptions) - self.State.GameOptions = gameOptions - local scenarioFile = self.State.GameOptions.ScenarioFile - if scenarioFile then + if pathToScenarioInfo and playerOptions then self.Preview:Show() - self.Preview:UpdateScenario(scenarioFile) - else - self.Preview:Hide() + self.Preview:UpdateScenario(pathToScenarioInfo, playerOptions) end end, @@ -158,15 +152,8 @@ local AutolobbyInterface = Class(Group) { RestoreState = function(self, state) self.State = state - if state.PlayerOptions then - local ok, msg = pcall(self.UpdatePlayerOptions, self, state.PlayerOptions) - if not ok then - WARN(msg) - end - end - - if state.GameOptions then - local ok, msg = pcall(self.UpdateGameOptions, self, state.GameOptions) + if state.PathToScenarioFile and state.PlayerOptions then + local ok, msg = pcall(self.UpdateScenario, self, state.PathToScenarioFile, state.PlayerOptions) if not ok then WARN(msg) end diff --git a/lua/ui/lobby/autolobby/AutolobbyMapPreview.lua b/lua/ui/lobby/autolobby/AutolobbyMapPreview.lua index 28e6266656..82ae3896dd 100644 --- a/lua/ui/lobby/autolobby/AutolobbyMapPreview.lua +++ b/lua/ui/lobby/autolobby/AutolobbyMapPreview.lua @@ -26,19 +26,19 @@ local LayoutHelpers = import("/lua/maui/layouthelpers.lua") local Group = import("/lua/maui/group.lua").Group local MapPreview = import("/lua/ui/controls/mappreview.lua").MapPreview +local AutolobbyMapPreviewSpawn = import("/lua/ui/lobby/autolobby/AutolobbyMapPreviewSpawn.lua") ---@class UIAutolobbyMapPreview : Group ---@field Preview MapPreview ----@field Border Control +---@field Overlay Bitmap ---@field PathToScenarioFile? FileName ---@field ScenarioInfo? UILobbyScenarioInfo +---@field ScenarioSave? UIScenarioSaveFile ---@field EnergyIcon Bitmap # Acts as a pool ---@field MassIcon Bitmap # Acts as a pool ---@field WreckageIcon Bitmap # Acts as a pool ---@field IconTrash TrashBag # Trashbag that contains all icons ----@field MassIcons Bitmap[] ----@field EnergyIcons Bitmap[] ----@field WreckageIcons Bitmap[] +---@field SpawnIcons UIAutolobbyMapPreviewSpawn[] local AutolobbyMapPreview = ClassUI(Group) { ---@param self UIAutolobbyMapPreview @@ -47,23 +47,28 @@ local AutolobbyMapPreview = ClassUI(Group) { Group.__init(self, parent) self.Preview = MapPreview(self) - self.Border = UIUtil.SurroundWithBorder(self.Preview, '/scx_menu/lan-game-lobby/frame/') + + -- D:\SteamLibrary\steamapps\common\Supreme Commander Forged Alliance\gamedata\textures\textures\ui\common\game\mini-map-glow-brd ? + self.Overlay = UIUtil.CreateBitmap(self, '/scx_menu/gameselect/map-panel-glow_bmp.dds') self.EnergyIcon = UIUtil.CreateBitmap(self, "/game/build-ui/icon-energy_bmp.dds") self.MassIcon = UIUtil.CreateBitmap(self, "/game/build-ui/icon-mass_bmp.dds") self.WreckageIcon = UIUtil.CreateBitmap(self, "/scx_menu/lan-game-lobby/mappreview/wreckage.dds") + self.SpawnIcons = {} self.IconTrash = TrashBag() - self.EnergyIcons = {} - self.MassIcons = {} - self.WreckageIcons = {} end, ---@param self UIAutolobbyMapPreview ---@param parent Control __post_init = function(self, parent) - LayoutHelpers.ReusedLayoutFor(self.Preview) + LayoutHelpers.ReusedLayoutFor(self.Overlay) :Fill(self) + :DisableHitTest(true) + :End() + + LayoutHelpers.ReusedLayoutFor(self.Preview) + :FillFixedBorder(self.Overlay, 24) :End() LayoutHelpers.ReusedLayoutFor(self.EnergyIcon) @@ -83,14 +88,13 @@ local AutolobbyMapPreview = ClassUI(Group) { --- --- This function is private and should not be called from outside the class. ---@param self UIAutolobbyMapPreview + ---@param icon Control ---@param scenarioWidth number ---@param scenarioHeight number ---@param px number ---@param pz number - ---@param source Bitmap - ---@return Bitmap - _CreateIcon = function(self, scenarioWidth, scenarioHeight, px, pz, source) - local size = self.Width() + PositionIcon = function(self, icon, scenarioWidth, scenarioHeight, px, pz) + local size = self.Preview.Width() local xOffset = 0 local xFactor = 1 local yOffset = 0 @@ -105,23 +109,13 @@ local AutolobbyMapPreview = ClassUI(Group) { xFactor = ratio end - -- create an icon - local icon = UIUtil.CreateBitmapColor(self, 'ffffff') - - -- share the texture - icon:ShareTextures(source) - - local x = xOffset + (px / scenarioWidth) * (size - 2) * xFactor - 4 - local z = yOffset + (pz / scenarioHeight) * (size - 2) * yFactor - 4 + local x = xOffset + (px / scenarioWidth) * (size - 2) * xFactor + local z = yOffset + (pz / scenarioHeight) * (size - 2) * yFactor -- position it LayoutHelpers.ReusedLayoutFor(icon) - :Width(14) - :Height(14) - :AtLeftTopIn(self, x, z) - - -- make it disposable - self.IconTrash:Add(icon) + :AtLeftTopIn(self.Preview, x - 0.5 * icon.Width(), z - 0.5 * icon.Height()) + :End() return icon end, @@ -142,35 +136,40 @@ local AutolobbyMapPreview = ClassUI(Group) { --- This function is private and should not be called from outside the class. ---@param self UIAutolobbyMapPreview ---@param scenarioInfo UILobbyScenarioInfo - _UpdateMarkers = function(self, scenarioInfo) + ---@param scenarioSave UIScenarioSaveFile + _UpdateMarkers = function(self, scenarioInfo, scenarioSave) local scenarioWidth = scenarioInfo.size[1] local scenarioHeight = scenarioInfo.size[2] - -- load in the save file - self.ScenarioSave = {} - doscript('/lua/dataInit.lua', self.ScenarioSave) - doscript(scenarioInfo.save, self.ScenarioSave) - - local allmarkers = self.ScenarioSave.Scenario.MasterChain['_MASTERCHAIN_'].Markers + local allmarkers = scenarioSave.MasterChain['_MASTERCHAIN_'].Markers if not allmarkers then return end - for key, marker in allmarkers do + + for _, marker in allmarkers do if marker['type'] == "Mass" then - table.insert(self.MassIcons, - self:_CreateIcon( - scenarioWidth, scenarioHeight, - marker.position[1], marker.position[3], - self.MassIcon - ) + ---@type Bitmap + local icon = LayoutHelpers.ReusedLayoutFor(self.IconTrash:Add(UIUtil.CreateBitmapColor(self, 'ffffff'))) + :Width(12) + :Height(12) + :End() + + icon:ShareTextures(self.MassIcon) + self:PositionIcon( + icon, scenarioWidth, scenarioHeight, + marker.position[1], marker.position[3] ) + elseif marker['type'] == "Hydrocarbon" then - table.insert(self.EnergyIcons, - self:_CreateIcon( - scenarioWidth, scenarioHeight, - marker.position[1], marker.position[3], - self.EnergyIcon - ) + ---@type Bitmap + local icon = LayoutHelpers.ReusedLayoutFor(self.IconTrash:Add(UIUtil.CreateBitmapColor(self, 'ffffff'))) + :Width(12) + :Height(12) + :End() + icon:ShareTextures(self.EnergyIcon) + self:PositionIcon( + icon, scenarioWidth, scenarioHeight, + marker.position[1], marker.position[3] ) end end @@ -181,7 +180,8 @@ local AutolobbyMapPreview = ClassUI(Group) { --- This function is private and should not be called from outside the class. ---@param self UIAutolobbyMapPreview ---@param scenarioInfo UILobbyScenarioInfo - _UpdateWreckages = function(self, scenarioInfo) + ---@param scenarioSave UIScenarioSaveFile + _UpdateWreckages = function(self, scenarioInfo, scenarioSave) -- TODO end, @@ -190,25 +190,87 @@ local AutolobbyMapPreview = ClassUI(Group) { --- This function is private and should not be called from outside the class. ---@param self UIAutolobbyMapPreview ---@param scenarioInfo UILobbyScenarioInfo - _UpdateSpawnLocations = function(self, scenarioInfo) - -- TODO + ---@param scenarioSave UIScenarioSaveFile + ---@param playerOptions UIAutolobbyPlayer[] + _UpdateSpawnLocations = function(self, scenarioInfo, scenarioSave, playerOptions) + local spawnIcons = self.SpawnIcons + local positions = MapUtil.GetStartPositionsFromScenario(scenarioInfo, scenarioSave) + if not positions then + -- clean up + for id, icon in spawnIcons do + icon:Destroy() + end + + return + end + + -- clean up + for id, icon in spawnIcons do + if not positions[id] then + icon:Destroy() + end + end + + -- create/update icons + for id, position in positions do + local icon = spawnIcons[id] + if not icon then + icon = AutolobbyMapPreviewSpawn.Create(self) + end + + spawnIcons[id] = icon + + self:PositionIcon( + icon, scenarioInfo.size[1], scenarioInfo.size[2], + position[1], position[2] + ) + + local playerOptions = playerOptions[id] + if playerOptions then + icon:Update(playerOptions.Faction) + else + icon:Reset() + end + end end, + --- Updates the map preview, including the mass, energy and wreckage icons. ---@param self UIAutolobbyMapPreview ---@param pathToScenarioInfo FileName # a reference to a _scenario.lua file - UpdateScenario = function(self, pathToScenarioInfo) + ---@param playerOptions UIAutolobbyPlayer[] + UpdateScenario = function(self, pathToScenarioInfo, playerOptions) + -- -- make it idempotent + -- if self.PathToScenarioFile ~= pathToScenarioInfo then + -- return + -- end + -- clear up previous iteration self.IconTrash:Destroy() self.Preview:ClearTexture() - self.PathToScenarioFile = pathToScenarioInfo - self.ScenarioInfo = MapUtil.LoadScenario(pathToScenarioInfo) - if self.ScenarioInfo then - self:_UpdatePreview(self.ScenarioInfo) - self:_UpdateMarkers(self.ScenarioInfo) - self:_UpdateWreckages(self.ScenarioInfo) - self:_UpdateSpawnLocations(self.ScenarioInfo) + + -- try and load the scenario info + local scenarioInfo = MapUtil.LoadScenario(pathToScenarioInfo) + if not scenarioInfo then + -- TODO: show default image that indicates something is off + return end + + self.ScenarioInfo = scenarioInfo + self:_UpdatePreview(scenarioInfo) + + -- try and load the scenario save + local scenarioSave = MapUtil.LoadScenarioSaveFile(scenarioInfo.save) + if not scenarioSave then + return + end + + self.ScenarioSave = scenarioSave + self:_UpdateMarkers(scenarioInfo, scenarioSave) + self:_UpdateWreckages(scenarioInfo, scenarioSave) + + self.PlayerOptions = playerOptions + self:_UpdateSpawnLocations(scenarioInfo, scenarioSave, playerOptions) end, --------------------------------------------------------------------------- diff --git a/lua/ui/lobby/autolobby/AutolobbyMapPreviewSpawn.lua b/lua/ui/lobby/autolobby/AutolobbyMapPreviewSpawn.lua new file mode 100644 index 0000000000..94afa51766 --- /dev/null +++ b/lua/ui/lobby/autolobby/AutolobbyMapPreviewSpawn.lua @@ -0,0 +1,108 @@ +--****************************************************************************************************** +--** Copyright (c) 2024 Willem 'Jip' Wijnia +--** +--** Permission is hereby granted, free of charge, to any person obtaining a copy +--** of this software and associated documentation files (the "Software"), to deal +--** in the Software without restriction, including without limitation the rights +--** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +--** copies of the Software, and to permit persons to whom the Software is +--** furnished to do so, subject to the following conditions: +--** +--** The above copyright notice and this permission notice shall be included in all +--** copies or substantial portions of the Software. +--** +--** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +--** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +--** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +--** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +--** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +--** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +--** SOFTWARE. +--****************************************************************************************************** + +local UIUtil = import("/lua/ui/uiutil.lua") +local LayoutHelpers = import("/lua/maui/layouthelpers.lua") + +local Bitmap = import("/lua/maui/bitmap.lua").Bitmap + +---@class UIAutolobbyMapPreviewSpawn : Bitmap +---@field Icon Bitmap +---@field Faction? number +local AutolobbyMapPreviewSpawn = ClassUI(Bitmap) { + + BorderPath = "/textures/ui/common/scx_menu/gameselect/map-slot_bmp.dds", + EmptyPath = "/textures/ui/common/dialogs/mapselect02/commander_alpha.dds", + UnknownIconPath = "/textures/ui/common/faction_icon-sm/random_ico.dds", + FactionIconPaths = { + -- faction_icon-lg + -- D:\SteamLibrary\steamapps\common\Supreme Commander Forged Alliance\gamedata\textures\textures\ui\common\dialogs\logo-btn + "/textures/ui/common/faction_icon-lg/uef_med.dds", + "/textures/ui/common/faction_icon-lg/aeon_med.dds", + "/textures/ui/common/faction_icon-lg/cybran_med.dds", + "/textures/ui/common/faction_icon-lg/seraphim_med.dds", + }, + + ---@param self UIAutolobbyMapPreviewSpawn + ---@param parent Control + __init = function(self, parent) + Bitmap.__init(self, parent, self.EmptyPath) + + self.Faction = nil + self:Hide() + end, + + ---@param self UIAutolobbyMapPreviewSpawn + ---@param parent Control + __post_init = function(self, parent) + LayoutHelpers.ReusedLayoutFor(self) + :Width(32) + :Height(32) + :Over(parent, 32) + :End() + end, + + ---@param self UIAutolobbyMapPreviewSpawn + Reset = function(self) + self.Faction = nil + self:Hide() + end, + + ---@param self Control + ---@param event KeyEvent + ---@return boolean + HandleEvent = function(self, event) + if event.Type == 'MouseEnter' then + self:SetAlpha(0.25) + elseif event.Type == 'MouseExit' then + self:SetAlpha(1.0) + end + + return true + end, + + ---@param self UIAutolobbyMapPreviewSpawn + Show = function(self) + if self.Faction then + Bitmap.Show(self) + else + self:Hide() + end + end, + + ---@param self UIAutolobbyMapPreviewSpawn + ---@param faction number + Update = function(self, faction) + local factionIcon = self.FactionIconPaths[faction] + if factionIcon then + self.Faction = faction + self:SetTexture(UIUtil.UIFile(factionIcon)) + self:Show() + end + end, +} + +---@param parent Control +---@return UIAutolobbyMapPreviewSpawn +Create = function(parent) + return AutolobbyMapPreviewSpawn(parent) +end diff --git a/lua/ui/maputil.lua b/lua/ui/maputil.lua index 2b3b31285c..889d0557d5 100644 --- a/lua/ui/maputil.lua +++ b/lua/ui/maputil.lua @@ -468,3 +468,73 @@ function CheckMapHasMarkers(scenario) end return false end + +------------------------------------------------------------------------------- +--#region Efficient utility functions + +--- Retrieves all of the playable armies for a scenario. Does not allocate new memory. +---@param scenarioInfo UIScenarioInfoFile +---@return string[]? # If defined, looks like: { 'ARMY_01', 'ARMY_02', ... }. Returns nil when the scenario is malformed. +function GetArmiesFromScenario(scenarioInfo) + + -- Usually the configuration looks like the following: + -- Configurations = { + -- ['standard'] = { + -- teams = { + -- { name = 'FFA', armies = { 'ARMY_1', 'ARMY_2', 'ARMY_3', 'ARMY_4', } }, + -- }, + -- customprops = { + -- }, + -- }, + -- } + -- + -- It is clearly an unfinished design. There's not much we can do about that. We first check + -- if it looks like that and we just return that accordingly. + + if scenarioInfo.Configurations.standard and scenarioInfo.Configurations.standard.teams then + for _, teamConfig in scenarioInfo.Configurations.standard.teams do + if teamConfig.name and (teamConfig.name == 'FFA') then + return teamConfig.armies + end + end + end + + -- Scenario format is malformed, not much we can do about this. + + return nil +end + +--- Retrieves all the starting positions for a scenario. Allocates and returns new tables on each call. +---@param scenarioInfo UIScenarioInfoFile +---@param scenarioSave UIScenarioSaveFile +---@return Vector2[]? +function GetStartPositionsFromScenario(scenarioInfo, scenarioSave) + local armies = GetArmiesFromScenario(scenarioInfo) + if not armies then + return nil + end + + local markers = scenarioSave.MasterChain._MASTERCHAIN_.Markers + if not markers then + return nil + end + + local output = {} + for _, army in armies do + local marker = markers[army] + if marker then + table.insert(output, { marker.position[1], marker.position[3] }) + else + table.insert(output, { 0, 0 }) + + WARN( + "MapUtil - no initial position marker for army", army, "found in", + scenarioInfo.name, "version", tostring(scenarioInfo.map_version) + ) + end + end + + return output +end + +--#endregion diff --git a/textures/ui/common/faction_icon-lg/aeon_med.dds b/textures/ui/common/faction_icon-lg/aeon_med.dds new file mode 100644 index 0000000000000000000000000000000000000000..5332e822aa41724144db9450159e0ee4a3f8ce46 GIT binary patch literal 16512 zcmeHOSxg;O7`_)S0;Mf*X<51erG-*~($bc)6)0Q^6liPQ_pKTqToTvR)><`D>sB?! zt!i6q)w(9eM19bxG0_+`#z&(@A2iWKV|;MeOcywY*ODuN$8HKq=Dku3{i?;Q4JIkSvlHVg6 z$YK5{r9wyTXdk5V9KboZ26hkZ0PGZ4*v!W|yNxl7U1P@l-?pKl3HB-s7h;_4HMO99 zuMxw2UV$BJ#%B9AIP+$;6ZRX-)Gpg?q60B)lf?asHr^t0BtDxxc$UI=+(f743+EK7 zaBf(yJ6Cid_FRc)3}atFKRJ0^@;|>_v-ts zKqDG57fhab>b!HhJ*k)AbWwM`?YD8s0mo#tsgv{MpCoTHZ#Sa}Q)0#yTs8 zv13mRs|^nis*)we`hG04f3xa50G~YA>oAAw{H`js>6wjc%ZuZx<-8`R9x;EyTFqmJ zx)pYt72C44$=Y`U7*!pW`u;4x28);&nc4=IJurvk{O);b^K)Ysf9fx;o$a(C?r|@k zXgef0#dXEb{+oa`ud_Ku1!AGo{Ep$UJ%Uc!G#HU zojclfJeIH0_fJB-po8#%%KMcR8C%wj{Be=ZjkNZ$7An| zZR?L)?8Py^u!R&rmw;MQH%W?WzT1eU*2Tfkz-uTsrv6&=gDvU(1^~D<%Dsc znHMoDb^M8^Mm_QSZtF4ehfTbox54c>;aA#F;>pcARxgYzUc(X(@636;cp;`=;=?&% zwFMaFao4v?7RSvGp7O#Cq8_{=Fg|ZAP3HJS=OyKWXo0T(JU<@5-YWQoWd)OJXAaIS zB_95JSgv1=skQ^VTg#<3JUHOXue^1x&hIY2+`zwEa#XCG>x(~x=dQ%teyv}QskQ_E z2FW#Z{!CwPJU+&A>OH^Qz<*zI)NgTruFAP3zNm7B-UmDVa!j=yyxtXVbz`+j>FgazjKJqaz+5ie=s6ej;$zk?=$WVU`x5&ntX-gv zYbamAtk{LU^OMGR8D}(m_3>2e!Rut*k8kU8b zUy`qqecqYcx!rbgtFHgEpmYlkw0|f1+pk$YW&PyJc~R4u%T#51h2mNckKOo_Bh$ic zzj~Ff|6Ac5l!x|TM1S|Lj%jgqJATux>$-leuK(wtbQ1?Zd^S7yeY<xZ2NyBGE; zERG+?dh2fKnpE@Pef=s1o2*~f-Q1-(-!Sp*ocvDcR9GL3>&9tY1DF1=&ker6AA@}W zbGUL2+jCQ=tG=Zt)E{{_fcFEumfiZG`^FA)?o4d1kMNzV^ZkI%C6i@v;`-qAFs^f0 z{lH_l;i33-GLCVjU3}+t?Qwm;wKZc-M@-HU6XwpD)a;fU9M6F{YbDX8rE%W_81J#I zz9?H3b@cPw0b@Q#th3#6WosSyZi6|;x$TlxOCPU!s~+b)zwv1~AK!-X9T`mS1uNsn z@{GSaTXlNz0%yL8cfZwB5FeRa4G$aycG_XcUV zV^j7XGZTFVO)rYRC7bFJ^s`Rd8Jh`?smuvY`$b=k%QqujQc@XqD!F6=8`@77{hZgueb<7u9b=Av zN+vj_GAFbz75()5ZbQy}&U^GWlK}_=fFQfXTb~r literal 0 HcmV?d00001 diff --git a/textures/ui/common/faction_icon-lg/aeon_med.png b/textures/ui/common/faction_icon-lg/aeon_med.png new file mode 100644 index 0000000000000000000000000000000000000000..6a6024c3478be5192450084017b5f8c09a224e59 GIT binary patch literal 3565 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 literal 0 HcmV?d00001 diff --git a/textures/ui/common/faction_icon-lg/aeon_mini.dds b/textures/ui/common/faction_icon-lg/aeon_mini.dds new file mode 100644 index 0000000000000000000000000000000000000000..97c57462c25cbaf37693b9cf93dfd3132aba3650 GIT binary patch literal 16512 zcmeHOS!`5Q7@jg+rlq*hmbNTqN>>VPX^XT#OIw|`(@hGvMhUoq8;DOBL_?y0s2F|G z7gtoV%R! zpW9GqOK`u>=R5xbpRW%k#h1fBlf}^R_(XFkwB_1D|72x?zd^M1N7{Mp^C|hAJV6ZW zk5bBaPR{kgnYspWy#*nULw<&Q2ifHm3vIVYbf`<4pp$O4*8_V1atQJl1UF)g^jPtu zz1yNg8`>U(1g!emd=9*gkk27e-g7JGMvqZ{mlZ$i_KLnmmv)~)8muLwk-LZq&c4ZJ|pW+E#~`iXBV(9<#HZ={n%*jo?!`b8^(GTUyk_8)I53 zR+kG0-p?zXu(A!JpBJLEWsG|rAaZBgvuX9iS` zKZ^ah8y@&LuKmD0e6v=#m~zCMOtzMH=@-3|;#1@F>ORSaHYE^jLn{(SfQ-SAKc@-2PR*5uFQs@Uh~ z5jTHlvK<&ZApa2Syv4<8{N>?TyeGy~{=9tQf6^-!_&Z6Tw4Hztvk%6OhZWx`Y=`^} zS>@GlI`t5jtCZ6aeP3>PV0{cuVjg(BUvRSA=QTcc>6bF_#Km!bdyjOR&w>x3XZt?! z8ZQ++#Jo%}Ru3(W!$|&J``4KKhrPyu-(m4hALjVW@zQ}R)AuH?zSE%xY#e*e$&^3f zSHs40TJW*#*uBccxY}!M_~m`2ZTZ|ceadn8=>Z)(qW3Avj9mkWH7NM%x76C=BhKa< zSD5^3lHk8g{L_bUww869YfS$kul}d22W%&ZkMH&HQz1uujaS#3_)5ISh41;ozwxSi zNB`j`L+0G#HLw1sqX%4-5Isj*xVF*}8^@~Ynnsg3zs1des_npaiu`kna@EGyMqP0cXHi+9V0+(d zT*UZ*af%icd5Uo&;hlh^_!fuwK| z>&SD1DsOI{;QJANs;qs$I>=9qL0#I|@k~?~m>-ZBr>y>`dk$P*6I)qpY0|iexjl9* z_!WFb-_tm*75|K3<_{C`wYHTtmkR%{r_0y02PWqDbe?C~XfP%I#A?Z1{SKeUAumIi z)799kE?JVY|79aV;ZLjuCWq-9!&imib6byC$HYA6Ts4SXiTJXXeBV*Wv`OYT)%BKJ z-vDQwVC9-78BBJ%-S@3kWi6#DH;}7^_Y~{wbd#yGthdsZ9|!k#;m@2-GMF-{+rF#S zytdli#?@;(xc3VG&b_O%mj4=H6P(2Cm7kj)e2s};w>5tz$J-i z!_Q9b*PW?8V6K_5jCExchk@-k94~O=l}tPQ^+G;^aQ^luBrdK$->>Q>YLc3_d+jIg zh1bHjKW|Q4+z1xtB#Dg}`;y_7iY{VY0O^JtfW&<3F>lE{)&IzbwA~lKKDb$A>4=17?~qUlq2HacFkC|su{12+2^)i4?PFh>3oa$jl^KAjO}-L z?I&OgKqhln=m+i%+2!v>o*y!EmWto|jG``%JMxG00iPm|Fost=Go=TNyCMG(cVTIv zB43kRHJ;UMGlkD@Q)lGZpen>#GonLZyJw0|Dsh2#zwk5nNyc6ZPmL+R35t9d#BpYu z^ua>p*u);?-Kk)hNgH5hF6SNLAUUJ9+gda)&i(mZ>p$8P?T@zGTQz@6F6V9fnn^CH z#s+Tg|Sv4J6r0(VL$%7NL! z(+MXyFn6Tq&g4yQrOhSLv6p+GbY)4jZ>SToNm~xb3124a!R1(lljdl*uVIrHI+h7+ znTP@IrI5Fz5A41fsoz|y`zE48UDv&$Okht}4DgBO5Lawn9(VmWZH@U>qB31^v&9ei z*F?rve-|Y8y_79C#_V6EEq9ki{HMNY!FMsjTgv6vG?>?aTG#H`&<7ZMz6UhNx=l54 z{Fyh9cIsvW=5%8KFYbA$;9&fB!p1)N@9B=4E&jk=@9mZ}&5s-ZlE(Qm{$FGZ-gM&t ze?1rW7x-hoAKCc&Xmkw(omoHUb?P^8+XK<#+=q3aJp1|{?vb%)d54qzY_A8mR^iXw zm+A;GhW+ic?UKZl8QnE(I) literal 0 HcmV?d00001 diff --git a/textures/ui/common/faction_icon-lg/aeon_mini.png b/textures/ui/common/faction_icon-lg/aeon_mini.png new file mode 100644 index 0000000000000000000000000000000000000000..12316e2f167023de78b385a64aefb337f6db3f01 GIT binary patch literal 3563 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 l>OS2l literal 0 HcmV?d00001 diff --git a/textures/ui/common/faction_icon-lg/aeon_thick.dds b/textures/ui/common/faction_icon-lg/aeon_thick.dds new file mode 100644 index 0000000000000000000000000000000000000000..40be02033061686fe2c5d7b5f898db655e8c9a5e GIT binary patch literal 18084 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 GJ@7wdwmf|R literal 0 HcmV?d00001 diff --git a/textures/ui/common/faction_icon-lg/cybran_med.png b/textures/ui/common/faction_icon-lg/cybran_med.png new file mode 100644 index 0000000000000000000000000000000000000000..fa4367cc4e903fec33fe54b11b64870e541512d9 GIT binary patch literal 4117 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 Ih+7}{KfjI%X#fBK literal 0 HcmV?d00001 diff --git a/textures/ui/common/faction_icon-lg/cybran_thick.png b/textures/ui/common/faction_icon-lg/cybran_thick.png new file mode 100644 index 0000000000000000000000000000000000000000..f01fdc1b15b3d4f5b91c975cc848e704671cfa02 GIT binary patch literal 4129 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 + diff --git a/textures/ui/common/faction_icon-lg/seraphim_med.dds b/textures/ui/common/faction_icon-lg/seraphim_med.dds new file mode 100644 index 0000000000000000000000000000000000000000..42bf7c624448c45ed917a2f57cd0a51d0d69b97c GIT binary patch literal 16512 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 literal 0 HcmV?d00001 diff --git a/textures/ui/common/faction_icon-lg/seraphim_med.png b/textures/ui/common/faction_icon-lg/seraphim_med.png new file mode 100644 index 0000000000000000000000000000000000000000..519faad7b076d24740e44a3d5801e57659d8cead GIT binary patch literal 4733 zcmV-@5`yiCP)C@~=k0#Sr; zifj-}2)ZlR2E(Nst{ctgehZ3ARE!)!SOw*hNK`HtT)=P$1O&q=2}BM#mdte5{_%YF zOwUvwGt(J&m)}3{>+b2QSFc{ZdiCm6kBu?L7=v5*KZQNq4E(pkX0zFBwqe7Ne)}!b zvNEL0mm_W2GTeOX!FJ06m;-j~h!J@8!3WyEK6MIdObp8T{CR7+Gml#qARtVdWbAi$ z+<`Pd9~H;`o0Io%j-me!L`JIbv~1a=iOt6Qeed2#$Bna&r@(Ma0RqCogX%j;NwKkZ zJHSH^-Fq*8oX?qqmY``5Lgjj~_>R_AJu8 zJfwB&hKo;J&Q(<4)tNKu=X>`aJ0{Bj)vMoj8vwxc=_5z-EkRNewx@e{tN7Ig#R0-i z5pTUkTxrv7+mPC9!PXlbzlDWr{<5-$4HE>$7~8zr>13?3vZ%=EWbDp6n>MxCc0WfT8k5kJ<>E0$_u0za6jMc}M-cX;Yj#1wku#(xi!1{29aP)9O2?PL-GYi>yBVbW)Na z0MxHfD*;cCYGh=DnnOKQxdPaJ0bVUygtSqkUm3Dvhd=jR|NerYI9L9$cdvDv8iB75 zaPHhsKlxLD8a2=-c;=ZtefXAO`0%QZ7>3CLj2?|NIa&Q=@?B$=dcqe ztm4oZZr)Vikt8j?0n*ZX_vR}A1&Dg~43aU!s4@lc_TJ~tRX^|4scl=E4Pe%+QKK9V zfK8ir?c$I9&p)@0Q#0@#>z*#6KeTJtvZc)i(4j+A6hCx4WJoBuGlD9%0NL3{6_ZbM z{JZaFWr?B^K`SXab&4;_ty`_-GjJ%=n>L0HjgRMBf|Qi1nhGqh02@Lbo|&nBo|g95 zV}bzCvEyBL*=zunl`a*ZU3+8Z=PfsZpa^H6w36{&@H9{O%>^*7`_c z6kfVT-tOH)4|H3~;QE%ylJkezVOzA9g=9%X4~r~7r%p&I)T}K5FOp74PWIOH%301H4D53|EMwr&~Bi=60}Byb;~32@{uMa;9bfA z^o?M?_F86UFe9JPzCFeTX#1G=tKe($hDK$^fP(7U6|YD`@4HX^JM9kjMc=-^`<>km zuyW;BU-|m{$;nu6+qSqVbmWLOf24YpcR(Gh7U;2KNMmA%pbpmuFZ0;s6Cazzmcjgloxke|3X6u`A>-h6iHf>(5cr)^v9TJ?9~#*Tgb zaX|o>J^Ptww8m@E7hw9alnol1{VQ{N$h@M8GYt2>>6)bzp&0|yEMz?Ln0_sTNBC!cKF<~t>j zDc9d7?$P7^`|Wms#6<51+{TUHd?U*M+qUiB?^!pO) z(a`8 z$ApB=o%!vj!zg4C^kG<(-p*9Ldg|viXY)`|artsEr;sV7YRQsm(?k)VpkT%fQRFKg zDGr%#y#N$p@Zd)uwc7z!ta#}qQB=wqH|~u$+-`u9l9MN`JWZ`PwHOOH8Q(iFX#pss z$G59{_r_f<|3{(#PMjz!3#H(O42h4o+X43MdFP#o2xZkk7(YHWRS*CkdrWJ$jSnjC z4}ZW-s~a~k{cHUA`SacGP_@DrE|>q(Yt_P09UmVPqu(A2EC7N(#%tEZM2O^MJS3^c z&YcGjS}}5Hbx>5ad$%m(9BQ)x%$)iA-+OxLnyWrfnnR{Jn7y_A;b za46Zl8#e6RDa!!gd~@iK747-)N6d#%gDfmhLA-JOy3zVfI)Rrj^PXI}f=e(;b^7$v z?b|(Fkb;7uB1r<+ycwOjefy3bV@%l{{Oezzf8OCxmO(~QP_Su}EGr`v4`wI|nVODT>JLO50x`wL}H>K0E`=lS<16#=jTfj!29o`PY@g1uAQ0X z;OW!n&oc%vapIeAy4?V~c9oPc2GF@PMiP%4@t+DebZA0?-41Z^;?FodYforVQ#J{GIs6S#*I!VW4MgdJK>{HIyrOh-0Ia45z5ILJ=(i>mo9cYV>nkc zcIeRJ#SsyV4ILUEZ@2U7!$_V&dB1z=sl-GJbFfhyJ9hRgV~Vq- z2U$k@LKY<^1|yBrr@#BI+l>}SmK7)N*=PIp69mO)xN+n2&z(+%?%ti37ZCw4W=!9{ zf}p&&WJzJ6BmvBtm7ni+D^4dJ0J(JO>Qy7{d-mDaUw6CtBb*01W$bXEG!GxfQ^@{d zK36h81=zPwedpV6j~?}8Y{G;=gB%Ww;p?x9BEaAOK53G}p>!a{+=)`Cdf~#!lN}C#jT`YjD(Axw zzxYCy0nVI3nAnLPZ7NFG4=P{4$XE z0UeMb1tievWB>3Sn0g3M0e0_J-`TaRq(qi|ofOLbd*A^~`!`#%&H1j}Co>7n!`rLC*KIvIX%1I|^5AoGGc%1WoBTIa!Ja)w zk9z)swq@_St64KEwrMEWPrd!C*THnqMGtf5jv3={@GYgb4^dQv{X(lV;|S77u3oLa zL+k&OC;gA$j11gPC9j-^$VlIeTTc`hd;1(cdfb1%HT!ekP}j46|MBBm3ecc|_T~;Q zSNW?ZiW{t3_s@Uw72wlP^*Q&CAQfQQGNg(l33l`wE>gdt9>{25x zu2UziIsC!}@4W@>6td7k9jGcV*E+%m(4|YecK+HiXi!|7-42kSpPlWw=b*-d1*=y} zk|I}ckR@KR!b}@H5pYTanw+11#!aHUJiM!XK{GFJ%^FEk=4AD9kQ$tqhs!PsA#dHf zZ=Wm!Or5Hg2jDA^I(2H+tO^BC*WK&ccN#cgkT5 z93jqenK1(?B`Yf4{FpI0IW8A3lf^vZ;-aHH1;2IczI|1p0P1=kd8A{-Dl1F~_V{y!z_WrBTrMS3$SgK(!uO~g zLSEkF$)Xr|$5RGVzkXAuxLmxBCmS|=xK$lbusqQj8?bRBUcLCD`Z=XyB_~gwTv@4P z7MTStBPUE4G)NEtu3p8kXky}|NiG*aMn-O~+pYK_-+bZbR4G&vx*nlRSk|B*^ z^XB4WSqA9Y^N)YjT5qT~ar6Q@AAK}?wkQHT`Q*b7n^6GTK2Au$j;F7Q`^Tb1*dL=t z8LuHYg!w~}YuBouY~PLtGrD%o%&e?5@73RPkM{9%g4zP;p;04qUnnQOewL5<0 zN~G!OX6gt9s$2n3!z(NCYT!VmG({-*?mHA%Y;1aZWhKC8pP{#`2Z|6H`_P1O&~Bz4 zKKf|$W=v5VGp4dq3GDbsK>^ZX!%!|^`1X<6B*IWr1bthN1mc_YNXeKON@eV)pVqF` zO2WjsnwLCDsZ;dn6{4Fqseg~(FXY!ld|=kF(PU3rCgX?Y!z%x9Gw>@T0HFMn#-IL# zlvc*Rg2X(1#&U8pG8_)ZE?-{1-m_!yM37!b(CK0N1Pj?1<#)Nc|Sri$=JO;)ZHL6?Jcv?3t-@0`Rw>-IW4mSh33<7|1B<=0Dy+@SM zj^I)HJ{0j)M|!rlI0 z>HqAXM`OULMGL&5|Kcew#`e;#CGS0&a_AdDrKR{@X{m)QhXwxumzpC+)jv|i00000 LNkvXXu0mjfP4y(! literal 0 HcmV?d00001 diff --git a/textures/ui/common/faction_icon-lg/seraphim_mini.dds b/textures/ui/common/faction_icon-lg/seraphim_mini.dds new file mode 100644 index 0000000000000000000000000000000000000000..5266c4d8f7096ecec61feb504cd7d3f6443ad3c0 GIT binary patch literal 16512 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 literal 0 HcmV?d00001 diff --git a/textures/ui/common/faction_icon-lg/seraphim_mini.png b/textures/ui/common/faction_icon-lg/seraphim_mini.png new file mode 100644 index 0000000000000000000000000000000000000000..23e407c7343211503d0467d9ad024cdb9e97412d GIT binary patch literal 5079 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 literal 0 HcmV?d00001 diff --git a/textures/ui/common/faction_icon-lg/seraphim_thick.dds b/textures/ui/common/faction_icon-lg/seraphim_thick.dds new file mode 100644 index 0000000000000000000000000000000000000000..4eb014ed16a2a0f1e6ec0eb43e45608080b52a2b GIT binary patch literal 18084 zcmd5@39uE#6@9ZlkX=^UcNE!)KoA7s2Lwbw+(=NOC}N0+1SleI2#BB}sA!3T1QH<# ziW(#$h?XsNPCKaQW;s$ok?KeHYyQk;|3=>pE3A!k4;Aa!aw-|FB8zuQ6fLhw*S`2^>SAa-?N)Xz!m-vdEN44vSwZKQ z<;Jf`_7Ddfa!84X<1%N;d{Zo_?3|j!zua8l-##T4XanBL$C=%8h$$X_hizV>y~5sxkTs*Y(rlq0Tj$9p)~)Ka2&vQ|$gD zbu7BRJS|pMePE5%InuRz`D*5^deX%0Ko%_6q|}aLPmhJW;6CMkpq~jZEk|I{chk!{ zN9q^(r@v^mqI- z#1gUax4mP+`~dZH7~y_<75AM*KiM%mOOcDsfG|uZFw{ zp*bSep?8M*-Uwo1HE)N!#pe+$Jr>USi%i&`!Y)0>*$_|Wk>(tAFNUZw2>4MBR_DU6 zDX-x9CQhdl7sxFT`c^aPM9VCTc{lGiV+C_9!NXm&Ia*)E*$do!ni!BSexo4bnME+1 z2{U8)*@MSN^kM5W$SV-jz1T8$_U*AaA_n5Q{A-#zAlCL$MgbfBEP?bTA3#N5J-z#sj(yAnIH~d!Fv>HAQ(k-&>f37qgnQ zWHcwS8Z{5~t+w4TZ~3&SeK`i<`BPsdeFi;J#G;G--1PTVm2G$b5$2pWE%Yy9uTeAR zE!lzb_ejBKjKlN|D)QUTxasL*f$<|(x1oY*b?&2F2RsI+h{YhCJI$+d^^Xnhx-*t1 z;_A*{hiBd_tVeb!uKt!cjrI9jP=Mnu++frR+krNAQn}K;nzm-n>6a+jfnSO9(9j>J z$HJNT1167&&ZJq%c|Q3Wb1%EOzzMky@AhYk*7cS@R?x*7i2vw+it6fdvFbMteRsxu zpE>8u4X-7i66aG;=Uk~Cx8+P(z}z`^hIZ{^1tC zyuRoyeVE1MmL{g?BawV2wF7_W`z(j`ZXPG()%<&&XU?>1nS05N%#>GIV1T?_RwZrB4xGkPCU4#fT8ZOra_YWPmHI`#%IwsWA;FBOn26*P*3PPV=m-A+9!Uy5Hs`(Q{Ixt`j#iccY)nw zFxMCB>%tx#sc#y5Ea(rJP~fodOVjrN&9$tgu^>JC{-R^iypHq5dqBS@uEVu(@D<$c zKEp$O>(~}}pZ+bpCv@Z^A1kEyw}o7pe4ox~jwUdbqlGpR*^%pb*KJkGWgXMGJMSJ|&+2fZ zQqTSI#t0_y@K)`u)I%ON=hxEp4#TIWX=4%elQ;Bucv3$h{-*p1y=#PTd`?0!wa|}K z&LR0aI@QITCco!UXp`PMIxlDHRpEWYrgc|c2iKX`Sy6YHZjast!cTt0yvZ3qL&2r~ zEOclbBOAQ^f}m|u!n{3eBobbcFNg{^GRmlA&tcy5V-M$ramAG%t+9pIj z*rI)!5!>!nKS}hQ#du*gY???v^oe{%ZX<_hPh(Az+%MJjuK8JnEBI4hs-N(wlq2ly z(&IsMCi(rP_#mDg9qBm*^Pyb?kN&QQ za<@v%?NiQ7^sL3$>Rxe2B7NZ_eR~onNFBZ6NPT z9F*rx-0c)u&SG1HT*OV8AEh6OE}#UV-mPi0so<>%m|7 zTbFVGR;OO7jyit72K;K@=bI!>;6eLoo{x(Ml~aD7=RPop;<;ee7sXE+N9ntZ-mPL@ zQpEOeaqk=H7jdxxpIVSjMK~AytH$Yy8$T2w-l+##4dl}_#9coiF9@8H5vVL`@qLT+R68h#`*sNA<7ui literal 0 HcmV?d00001 diff --git a/textures/ui/common/faction_icon-lg/seraphim_thick.png b/textures/ui/common/faction_icon-lg/seraphim_thick.png new file mode 100644 index 0000000000000000000000000000000000000000..dcd902697a7516bafcbe02ec6df32c3a3be799c9 GIT binary patch literal 4863 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 literal 0 HcmV?d00001 diff --git a/textures/ui/common/faction_icon-lg/uef_med.dds b/textures/ui/common/faction_icon-lg/uef_med.dds new file mode 100644 index 0000000000000000000000000000000000000000..b44b67def6c222b257425c93bf171a001d128a49 GIT binary patch literal 16512 zcmeI3%dZqg6vq2rUY89oL0FJ*4a6N1ln?|ikIMidXoT>7L^N(h_!C?Z*zraaf}#oR zG(P?zmn_tUCW?T&=KE&)Yieq$yQ^lpd#2}d!?~v)P<4J^ovJ$BGqhUAroW7$Xz8*j z+5=CECitu44j%gO$i`Od*!jitIu~<(PvhR+q;y8|8+iMN#&H>~h zv+-nN%j)ibnEioy7WNkm6SQqS-aE{g>gpSuE!gvfTm1C@!w%OK$1wT<-#%E%K5sIL zX}Biiz5T<8t?E9(H3NGpR&sxPVO9M2vp3`UlM~_}%RI2zCnuu#8?ZmU|5n?^Sp%@| zfjxt@$C&I7K7PfXqe0{60+@Hg(wv_F`&VaIrsFPl`EkkvaC5%@2WyWz+3%gnX3x=} zad`md9k72*_PZYJIT|!(Zv^vpoBht|H!^cx%(idhTquiOS~N)mu)h!c2bRwF+kK3? z*yWp`0hqVh?6*#Z=e(G0Kgt?_`#sp-uyowZWS17Br~#PYwb^f;EN|S!F7MEQdte=t!XtJ{%bD#5#MSstezbF5{|I}~ptDm1Pu8tQabms5pg~uyPwk2{_~lY|Ef}JKi(Qw-u9$o@ z$YWPKz8Va|9+C$>?20{410Qy^Q=~x|>^>TVuq$?#1|jTfCrg7e+4D3Q#;aqFo%VFL!((34{SaTr`!8vOSo!{w?-ns%<=q=AcF zx`b+w!R~ynRm^33@Ox{J#~!Ld#vWHIyR`Jtz+9t?*7>457$tTe4MNz9@?aF%^EBYz z*l%rPK1Z_+wq>pJTziI+^J2EWJr{7^-XDr`UgK_ib1tZ@#E%@iv~X!~d3I@Ebq)34 zzikS;p#i`5*i|F>)(2(|IOaRsoKZic>=EZySl4^>5DgscY-{T%GuyEtUOq1bYmmWy z&~Cp8=iv9Qk(m1qc7E^mgC?+M#hBzlVXf?7XYLI2`E^!Ibzd*a0|$F!zVGa-WtIkW zfn6H8=7OWH8;hJ5v+eCXz`1IivCDazUFTqS{r|^V-Jc5{8qC@M|2h}Ow<(M-0qr=f ACIA2c literal 0 HcmV?d00001 diff --git a/textures/ui/common/faction_icon-lg/uef_med.png b/textures/ui/common/faction_icon-lg/uef_med.png new file mode 100644 index 0000000000000000000000000000000000000000..2c14e08b27081f74665f5e134d6b5ead40f923bb GIT binary patch literal 2605 zcmV+|3exq7P)MCb#6S5YmM&aCn@PMVE?SiNjhUH$|5aaQ zauRKAE%wvM2p@0VgYPDRNPJ}_md>3+t0lgusHLT=3!u2Twe`af0R%x5^E5Le`rd^L zc-@*B-u^oc-%J9P#DDPx+Hg3thA1NOot*&T@MDiPHEFMN>(+@AZEXOOG&6JO4t*L3 zpgnU2@8h4)kn0mJ3Jce-Z*8@HpZPK(B}$C78WXN07RqDJ$L9365rOw-VbXd0!p|BC%L|JAc3Y zCIrt306XmQ$FVf`6aVhJ&JnMMUxeTp0bmD}mt%>Z_(()q10WiG_StvdLE>M0(Rn}B z$aNvOE&%M1HEXa$37olzud3?kaNbWN?X$_}1FqXdl8f_-= z^hsUa>#qYuA{#gE-_P4C=aHQdVlm!+aT5SGVC`BgeflZd%%L7Z#4lZ1U48H%FK@g9 zYeG16O4*O>!CV07FzvM)9YwoxrMlj-Wwo`hy#}y!>E_KZzs%YT?}LO8jW#wOI;2-? z#R{~eqso3|2WA3LIyl=#3?(KfuU;J-1elpgr;|xmUgQoWX?i-90!Su*`e|Ums5bTE zxpS03e;de*OFJ0pjt_&Ye2}W@e_QCMN89<9$#& zZ{9p|Wczl2bo$!0Z@-oMh5AXmWetFb5V974S>kEyRWw>(|Hc~tB_;dz_4fk^LVkWQ z2#`+y_S<*g0mS1+j?~rpcSN8kJ|6GtYG{z#O~e-$x3`}>Dfg4Pt!nm#U|9fGiC1sE zTD|S1VaI^CSL2hzl6Ywcr|zlL3b?xFF;$I=s-mrKQNX zeD+Z%1W+#8dEyCP4-IY;PcRo6h)3-jLl&=r-Z)C;mJsxa&sFFMfYzmH=m4OzbGL4t zJh^{AKr;E`kDq+P&#_eM=br}$0pjuQ?xrR=1QZmkSW#AnPg)@0TtaEC1Wq6z`z1+t z?o3YuBofDu@7^u9gBOU0MB>DWJ$sBofOfxuMgYnfL4!O^CIDn1^!9di03?$?{Lt6O zqA3W9{fWf!<1H=Np;J?G?7#QkS6&$!!h}*^AOD1snFP*99|7EV->a{Vi~t0Kt5(Hg zvfqhBPfv3*Kq__R3hj4d0gCvDMmUghnHDbk z7Z@JKQeB<$1^{-2AQOM=SaY-W**Oqzp@$yYvW1;L^fnQXA3eHbhjvm~ScvBx{{H*YrS5M0hW;;? zWKTSS*|u=28$z7U=;@6Jvj5H|aP&q5qgJ4`PdtIyjv(VXf)aFI->Fn?AW&LbQ2~&j zUsAGamGD&g$(tOJ3W2l#+57bP*0XVr+U11C?%6)WBvUA&(Ufpi}bT^BnYPx zIJB&rNc8kP|GYM#M3Yx~yUCTGp7o3X2*yHC*LOC7W0X+R`xaX75KnOZ7edr94h>2~ zBC%LU2Y=!T6dEFt%1YzkCh{)v1kVY8pzJUj%(V%nzy7*(sjG|q#uPnJjF)ruvw{5F zvc;>!6Fe6J^njsZ-8w8$USYOv>TDBA1Z&%{tE#YH$Hv%szz3ck0n|`-D20aEwyLp7 zC=sj?Pq}*U6Hmw)0T7fq@on4Cvd=7Kd#M9)C z{UVKHAOYmMMbPVGDn+f6ttc*9BuPa@FT8N75;Z9FgN}KOn8!v`e1dX P00000NkvXXu0mjfTZG#a literal 0 HcmV?d00001 diff --git a/textures/ui/common/faction_icon-lg/uef_mini.dds b/textures/ui/common/faction_icon-lg/uef_mini.dds new file mode 100644 index 0000000000000000000000000000000000000000..f54215cc247c157ee3fe01737a0f39b32a226b48 GIT binary patch literal 16512 zcmeI3%Wf1$6oz|j5!n{LD1a3wmT(kERy+WRo25}u7D$$yAR+t$*+jwt+mX0hARCZu zvx)L3S%phBAs}8OakklIk>>m}^S95`RCibP^le5SQ9eCAGgasN&#A66)l9Rwyl^K7 zg6WwcxGWYeXy8xVT>0ke(wSy+d1ZXGeKO(ig1olac^$_=5b|I9iHu=B!Y~XD+o1+! z=YZtly0m{fZKLe*s~k_5x1`DB>}tF6yiuiJWsEJ1&&WMHH5#Jvx!Bz@e5h&+;d@Nh z6O*Iin>(L{@9)otbJN`#$a{t^4}@u(?8lF`+5i3d^AOFZK~*s-VqD>Vq}l(FxRagM zrh$BB;PXHjqMS$EZR|-J6cLZI`V{s}#(idDB>erS-nf&Uc3AR2xF2fv*BhVqzW6`ev5H0ik+I&O#|WnQM11&Ans(Rt&0Z2y;?SV zT{IBxn`ECH8wr2Ao&S6%Yn-hr4}|*%vQK=}2!CCjbFS;+ShAkCXs}h(`i6exaZTEP z;IEpUni(4WFTUL*qdvO(P$jlr{5i` zT!Sa#gZ7Dj!sk3R(CpZ=LQQ+=tMNz+jL$ffZMVNYU_blCScvbK=3ZJq8kp>V-fe$> z-``uPTmzH+@70SzPV~3>>JWr*LB-`XMb^4Yk+<>mp;yI zUC7E#Uo9H++;0@0_u+RIc8*ho2K}zj>h9(HGWuu5&(q z;l1Ej`)D@)DF1O!^4-08FAczc;mfh8E;==PBs$JgBk8&heX+*F7#PckA1uaw)}H)F zKFA}wFO7NNng=@W@Dp{hKIfYUI_~FAwPXJL`te8upFA+)4#>p17_Y-$G#3ptC)pVr z{TljnxhC4!*`KclzU;gvxN4yJOm_OL-;<_65}mO(ra`~#X&U&jGagAA_^`9TI1Tb*kJA8q`>t`Pue3G|vaqv1Qv=**RBK%7 zwT_HwR$mXe4#mxP`W9!QfnF!sUzT+ZW9W5~ec?I-pVPQ}UYGv5_Pkase=CU@%G3cCUe=cjItv_SF7qG;^vebz>Uy<{A zF8J1oZ}0Zhb%pMKD-AN$i8}6F<80SNTb#{vM%;6s?_`a$lDA%UqK-S+$>ghng+15( z?l@-iysHLD?7kY*g`HZYX<)9=N#9>}9$+mquLpHwR~lHa2R`h!JkT0g*mGUq$&%IT zJixkuHJSB@Z*7A-IDh(JF9CfS-`{J+PR+Cia@?vD%xfLvoM!c0U|$_uOLnC}R39Mz zKL790)C8Yp4t3bKIZmCe_`1TFW!+E3d^hf6t<;&>^iBBpvT9&q-!uCkgy+H^%>~yy zu(1Ck{W#ydtCKG9TUu8hB<-nM*zwtcWY=*JhORuYX<%U=n%_fp@*wv6f`xr3;;wT5 z$MWc@6TxooSwS!Jv%~P*%mdVPW?eU|>^gor59GL2CyK3gU8nue<5+lczcKp#s~ity O&gY@kVIRf)KK%m=@L&x9 literal 0 HcmV?d00001 diff --git a/textures/ui/common/faction_icon-lg/uef_mini.png b/textures/ui/common/faction_icon-lg/uef_mini.png new file mode 100644 index 0000000000000000000000000000000000000000..42ca4afdaadbacf1ed5bb1ee46c8549b29dc9608 GIT binary patch literal 2500 zcmV;#2|M|%m zQ-o3&=s3e$ONExd04gZzNEB=fl;R6K2m@OF2Y7%P3bcZK@lhzqt4WG7)3iWsrmPRg z)4RLLW;f}*`;+v0xwE@T_T2k@d+xpGp1W~Lk|aqIj3Gnew%UL(Sc{AkfXn4_xmb>gzA2}_sDYP*HEi`51QuOI*beE+?g=&Y>5!qQS! zY6-z=`-QiU)dmQw7ytl#^Gz)A1qG8PNz&xWpMBQdElE?R?ArDF?~;_2Ra8_~mU$i_ z?ZyDum+#qwM>}@V`K+w`{D~6*e7+4EE?xxic*@J?%mK*G_IhW|1Sl(OXt;C<0EM9b z)rJNvfB3Yn? z()oDAzx=ZP?`4`taI653_}#nlh`D;Vd)&CBNYu8|V0U+^Te1S*th{q>BCuiEU`SZowlWf-@-lEZAWL|4GU;}R5ibsI}otG1j zPyDoLue@TFl>pIbIDG#;Krq2yVNW2_G!fjmNf{_C%=TBW{9 zYYNAXDchAFxCubncgZ7O%B7J=qQwvZOuYgqFWZ}#lB-v+>BB!GYZId#hKXAO{Ph(xYk zYi-4Lz+7Z(m)zVLGv0g?puF7gzi~tST0_bL&?DaG!^IMc+#=@B{*vc2GyT3zWS)^0aAV9^?x_LJpymy3A{amrT~=0%d>1pM||vk z(n|=Uh*yJGs#=Ty@ME+`P8{*E_tg_Zd%NEc(ARh6iuG$~u`r8xHIfqo?3Ft9=+U`z zJ9k>=5wwW66O7oSApq8-!^8dk4<7=ALi_iB_#r?v8VdFF@G>zL^yG&^hYl@Y9y>e8 z&Mqyjs9>Le^yvKgUAxR1BhY=+#523l&cVS44{qN^4^~yh>Lm)qOEB2f^#1!=xrwol zN{j%|MbO}nN`T?vNTjzHZ?I?)z|hcNfBo?XzY3^b{@~!fd!3yC@@Zip05Ey-jvc@L zYW^5OJ91Z1@zz@a<>lMAcXa_woVao&hIB~?SdUQXz=6t2fM?JC{rB->es(dJ-_r8I z2aG2rXj;hlBs`0*%$e9ezQ4b- z^T-i^zyCgY5?hV$*nn3yvhEl_`2%dZf^`k3yktE>s>BntXAOwajuBENo={M*aN*ju z@jL=`4F?Y{T?+8*+2G*7fN{Hcr=cgE4=`GfAn)U=s?bH=yh*>TPrMd*c?40TdVP;T zg38U!&yW4SsQm{?uLY#)5kwJB7_CPTJ)P6{2(7IjeH7~vL~lG9bw-e$NAP-UYunoy z0lbOVf>Dp)ay|E)$7B9HUe6p&o1iY2L`&kPn=L+B?+uqqik1x5SA8> z22&O*+vI6>WKk<}N3Nbqf?1KpXfznSdsi=V7YHcZkv~k^9*$!K%5!IL6-4aL zJD8UcXg-}P@oMM`!PpqV?!*%;e?Ndy2=dj(#6l3g*ph0hT5^57b;J{F_l_X#F+yQs zX(>Q{{u^(sUM=5AV@?2v1(aG_mn{Pr9K3(OtBWjV^Z0>*-+nuOTyI&{KJN;$od5{h zkwqoUPQB6_EXIfZh$mRy1!zIpU@VTnfKOgYNmW%KU|!B0d6x0xmoEM4D`rQ0&^+FL z#1m{Q00K53#SLm|lvi*SZP_w_p`l>#&K>dgP#+%-x3yJQGdq-^?cDbN2NK&0fPf83 z?-^)mq4R98h*_|rgkWq(PD_i@PW!_#0w7=m)0iM$h?%btj1f<(2ljRKT5y~Il)xZw zVvms8qLa}l9?RNVNwVv3Ix!q807-#F9wE8NqS4KlST;5~m3V?}r-8{rx%Z7;rDA-o zA-N~s`NWTas*!FbBKB|M68&kE&zE_B8fCcw!YcMZVM+q!LNNBXap_9@NIO|=fN2%g z@CaJOTWzmt?P9e7rdJpMSV9U#>#3yk>HEa1%d{B;jv+NTb{aSaKKKv!U8XRa{*3DY O0000) literal 0 HcmV?d00001 diff --git a/textures/ui/common/faction_icon-lg/uef_thick.dds b/textures/ui/common/faction_icon-lg/uef_thick.dds new file mode 100644 index 0000000000000000000000000000000000000000..3dceb3e88a953ecfe5533faa8991bbed8a7a9ac4 GIT binary patch literal 18084 zcmeI4&yN&E6vumJ7M6`h;2@Zo$eNJI!R4SgJ?QEt#wdviR}+y06@vc&;lQ3WU?S@e zR9x`~7(h+bzlbY%UH%jYiq}s&sSBo zcVAV#DHiR?f&7pM%v*0Cdv9fJ?fCmkqpf49AIoxb)&2sWSC!$v?H4kJ|1ic>J8g$S zpEMvEU6FP{+7H&Q^vS<2e37^@;V(;jAWbIn!-m&5KI+1cX^$as4yFAn&1K*}zb)q% zrtNE+<692DWH^w^-13n4u`GVCQVvQUGc#W-hi_sWZbkG!4zwA4^qz<1@O6m8uF=3~Im|kblpet0=Evdu zz_?>JqmSO@56j`Jm>%kKAfF76-pm)vVI#z0G_1_eCl73`myD~fodq7k;PbXQkYAs9 zq&QqV`E(Q6#q{vghwX6aBM)KlMU2DdiFP?Cd0>4Rw!Hs<>nvWmvIcS52^gEIk+>Zi>A3M?mInZYE2mEdHdf#%mAvUOos~>bTFBrdXGd$pT zBRo^k*_s|WVOI-|#~M57N^53f3PXled%_2BB@zl~+HdStuac8{l+B=mrQ zgX@T8f$MUtyJPbTc{If%#({ondBixdAD>5R4=uwXSzpxUk>kKPay)Vz*pG(Cq#T$} zMHgy)XgM$r_xIg=s^!3bQaqaCP{PBF%gqn;H)h@UfybST&y8Jg#@_255AkT4gNDbX z`xowd^TgtCD?ed<04F!z+;ixO>q}ev7aAVOvDi<7KRKU6=dn(@dAjbNDaLPmUrWQo z{hm=M-qY&b$9(zwtiB%eb$w>_soML67&lT!L354o zgOiK&J&=a`4v%OXueJJc_2AY^em>Rg7iRR(+WirS2fm+(`%=X`VjQUZ5*`^2@UPYT zW)2V3A4NQJ97=d7I5cuUgz@IAuu${Ra?tST8wYaI@F?Yw;?Y+Q8Xm~+FWLK9b?PY8 zW4s@QeiG{i=4+?(g7Nz{r4Ko0-1UN=yVkn-6nzC+Z#a-kibrYv;c~*fm<2tM18s_j zw*K%rB)%`-J3WvCt?)qoF;P7R9+^Ce{yV837*|~@exg1N*6SL5WH_MTR=#gjH{RU) z#9wv&Q7|vE`*JzIrgh%(Q0pV3hwS~}X~n4>SIcAGIgnRszF@t~{Jq)C&I`ux+mt@! yT&RY^v0KvS^6yj&dCV;b>O{$7?m2io9{ay%Cl6S8Jof+3fy-m(c|ZRjlK%mY9ZFLG literal 0 HcmV?d00001 diff --git a/textures/ui/common/faction_icon-lg/uef_thick.png b/textures/ui/common/faction_icon-lg/uef_thick.png new file mode 100644 index 0000000000000000000000000000000000000000..77debb11a0a57db8b5b7be6af866b4fe56b20dc8 GIT binary patch literal 2708 zcmV;F3TyR=P)z^oU7s>rTeE|pW;;941_Dm^%kpt^+5yWtCIOMckFH)7#anLKwd?e01SAxixe>&(c?{vY@aWJX zv^#gIw+jS8#bVAYeVk> zo=pdM4h$&}4Gm~xG4*x?#7i$7KaM~=`K0pe`2f<~jNZ~V**o$K7*Ze(9z?rsn|eDr z5Q`VDS)=`P%>lA`GurlcuSF2ogCPatzyY*1HR|mc5XX+`2SToyHpRFW3@H#Feu%cV zR=u6yKWB~$OziAVIC{9d_jcILt>J-|F3OpaFlvRFemE#dZ3ItuO zQU?OS=kxmkii)bL4j*<72vV+1F^+?Qdh0E;ue_rES_GoFI2t{21fZ~R>C)$)x7Is7 z$`wEy0|Pt$_S<;$#vAG_C_+S|jg9UDLN2cZVmlbvQHl_HfWRpu9A3I~{d#MC+uPaz3JUJIXVoe;brcm|MG%#U@&5a0`9%mdF+iki5U&Bk8wk1h zL=ZZ^(={!Rfg~+nj7NQa^jpO@-~@z{Mo0VlKK&FR9zS|C8U+x=$;pWcfc*SB?_9PF zAQGvq{rF=5pD!=3u#n&98kN;RC~0i$!iBHC!aT^vjau^{bmo^VK@NWXmHwg-G3f4Z z=YSv;7A{%x^waK-AWi{6G8RGT{L=YmQUK$&+tl82-n`YTUw_>?Amk!I=FVNQV#f~l zdje#5xW2x;-2Oa>JP*NuK;3lHV~@pR_FWYwK<3R`wMxsCDb)1ytpLWrfZAJKzI^7) zJMREoyVl=-=8RScjB9kXxA&7z0RH>0r-$w)NtfMu5am3CR9CK?JzG}?Ff#JfPhDNs z`9q@A`sK@?fBxQkT3by&-wI%KccWFliXw`sR3ZT|JbdIxWhHZnw?;>MdrzMRh{v0n zwr*u>j8JINq7^GxorZjhu_}TnMF{$Q@`wBbn3|fL1h{mmwROi1)*s$Vzt*CEjX~#| zX&M;PMF>@CXM5|3iScpvpTjGHP=tubo0_VtrHjtx%K?guF(XJ9-K@@}s%I*zs<2D| z{fR_sYPz}rLZQ3vTDg+>KRk?!XkHN{)5W*(ajmTn9FQ+MNs3ojd8NMP_lBw|9fJO8U=b?SKFM>#x(>Q)QaILL{rN#wzA9qz$U7q;IG!HpMhH zZr-dtf|vz@#A_GBYnMU0gGeM6>+EC^gr=ByytQ@vc7TzQAAh8q2(rnp%k!!!ajOC0 z1%zA-5Pg|ctG1nH0kOMYLwBJLuPKJW;SGe6vH`ek8Oz7S<42Ef-D-UvLhBUc_lLs^ z7T90FW4MCLY&{0RARftMJ)kH5rpkB zFydHMNR(-p)6>K1F#!DjKtS)J(tJgi>C!&46vWgv-Suggv#V1jyjvzI|)fu!0?Z;#cBly0p*e8ibTg4h;17$@n7GnmmY-%oWJE7YLH;z#wJLq-sf&8QbP+mAPF8`OiO7$y=^j zW2i~`1Q}X`s6UX!Y@&?B=K&`aA>2RWu8kOSNe8sL8m)DSG7_Hx8G7Q4`+*>N1`JZ> zHHe;zb=m;o^Q2$*t|3%K9DCKG_UL~nm4VdsP>tOH O0000 Date: Thu, 31 Oct 2024 11:09:24 +0100 Subject: [PATCH 72/83] Fix issue with hot reload --- lua/ui/lobby/autolobby/AutolobbyInterface.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/ui/lobby/autolobby/AutolobbyInterface.lua b/lua/ui/lobby/autolobby/AutolobbyInterface.lua index dfbaf4e3a8..e7b1dc9497 100644 --- a/lua/ui/lobby/autolobby/AutolobbyInterface.lua +++ b/lua/ui/lobby/autolobby/AutolobbyInterface.lua @@ -130,7 +130,7 @@ local AutolobbyInterface = Class(Group) { ---@param pathToScenarioInfo FileName ---@param playerOptions UIAutolobbyPlayer[] UpdateScenario = function(self, pathToScenarioInfo, playerOptions) - self.State.ScenarioFile = pathToScenarioInfo + self.State.PathToScenarioFile = pathToScenarioInfo self.State.PlayerOptions = playerOptions if pathToScenarioInfo and playerOptions then From 577f2e11e2e7eb42e8d99933b1587b5e0b7c88bb Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Thu, 31 Oct 2024 11:10:09 +0100 Subject: [PATCH 73/83] Document `SetColorMask` --- engine/User/CMauiBitmap.lua | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/engine/User/CMauiBitmap.lua b/engine/User/CMauiBitmap.lua index 278b891143..c0356df94c 100644 --- a/engine/User/CMauiBitmap.lua +++ b/engine/User/CMauiBitmap.lua @@ -89,4 +89,10 @@ end function CMauiBitmap:UseAlphaHitTest(doHit) end +--- Defines the color mask to be applied to bitmap during rendering (white images will get this color for example). +--- Introduced by [binary patch #42](https://github.com/FAForever/FA-Binary-Patches/pull/42) +---@param color string +function CMauiBitmap:SetColorMask(color) +end + return CMauiBitmap From 70d37a59d52c7a2b751a5e75ebf25584a4ae4b9a Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Thu, 31 Oct 2024 11:10:23 +0100 Subject: [PATCH 74/83] Improve annotations of `StringSplit` --- lua/system/utils.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lua/system/utils.lua b/lua/system/utils.lua index 09bb5acbab..8d355b597e 100644 --- a/lua/system/utils.lua +++ b/lua/system/utils.lua @@ -616,6 +616,9 @@ function StringJoin(items, delimiter) end --- "explode" a string into a series of tokens, using a separator character `sep` +---@param str string +---@param sep string +---@return string[] function StringSplit(str, sep) local sep, fields = sep or ":", {} local pattern = string.format("([^%s]+)", sep) From bc3be4466281880cd7635a5d9af5847260f4de9c Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Thu, 31 Oct 2024 16:20:33 +0100 Subject: [PATCH 75/83] Add additional utility functions --- lua/ui/maputil.lua | 42 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/lua/ui/maputil.lua b/lua/ui/maputil.lua index 889d0557d5..b7da6605fc 100644 --- a/lua/ui/maputil.lua +++ b/lua/ui/maputil.lua @@ -132,20 +132,54 @@ ---@field PlayableAreaHeight number Syncs when the playable area changes ---@field PlayableRect { [1]: number, [2]: number, [3]: number, [4]: number } Coordinates `{x0, y0, x1, y1}` of the playable area Rectangle. Syncs when the playable area changes. +--- Given the path to a scenario info file, returns a path with the `_scenario.lua` bit removed. +---@param pathToScenarioInfo any +---@return string +local function GetPathToFolder(pathToScenarioInfo) + return string.sub(pathToScenarioInfo, 1, string.len(pathToScenarioInfo) - string.len("scenario.lua")) +end + +--- Given the path to a scenario info file, returns the path to the folder it resides in. +---@param pathToScenarioInfo any +---@return string +local function GetPathToScenario(pathToScenarioInfo) + local splits = StringSplit(pathToScenarioInfo, "/") + return string.sub(pathToScenarioInfo, 1, string.len(pathToScenarioInfo) - string.len(splits[table.getn(splits)])) +end + --- Given the path to a scenario info file, returns the path to the scenario options file. The reference to this file is not stored in the _scenario.lua file. ---@param pathToScenarioInfo FileName ---@return FileName function GetPathToScenarioOptions(pathToScenarioInfo) - return string.sub(pathToScenarioInfo, 1, string.len(pathToScenarioInfo) - string.len("scenario.lua")) .. - "options.lua" --[[@as FileName]] + return GetPathToScenario(pathToScenarioInfo) .. "options.lua" --[[@as FileName]] end --- Given the path to a scenario info file, returns the path to the scenario strings file. The reference to this file is not stored in the _scenario.lua file. ---@param pathToScenarioInfo FileName ---@return FileName function GetPathToScenarioStrings(pathToScenarioInfo) - return string.sub(pathToScenarioInfo, 1, string.len(pathToScenarioInfo) - string.len("scenario.lua")) .. - "strings.lua" --[[@as FileName]] + return GetPathToScenario(pathToScenarioInfo) .. "strings.lua" --[[@as FileName]] +end + +--- Given the path to a scenario info file, returns the path to the scenario water mask. The water mask can help players understand where water is. +---@param pathToScenarioInfo string +---@return FileName +function GetPathToWaterMask(pathToScenarioInfo) + return GetPathToFolder(pathToScenarioInfo) .. "/lobby/preview-water.dds" --[[@as FileName]] +end + +--- Given the path to a scenario info file, returns the path to the scenario cliff mask. The cliffs mask can help players understand where units can go. +---@param pathToScenarioInfo string +---@return FileName +function GetPathToCliffMask(pathToScenarioInfo) + return GetPathToFolder(pathToScenarioInfo) .. "/lobby/preview-cliffs.dds" --[[@as FileName]] +end + +--- Given the path to a scenario info file, returns the path to the scenario buildable mask. The buildable mask can help players understand where they have large, buildable areas. +---@param pathToScenarioInfo string +---@return FileName +function GetPathToBuildableMask(pathToScenarioInfo) + return GetPathToFolder(pathToScenarioInfo) .. "/lobby/preview-buildable.dds" --[[@as FileName]] end --- Loads in the scenario save. This function is expensive and should be used sparingly. From 92ab7f4ea01050cb7f059638315e29ed0fe8a75b Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Thu, 31 Oct 2024 17:15:49 +0100 Subject: [PATCH 76/83] Setup for a panel with brackets --- lua/ui/lobby/autolobby/AutolobbyInterface.lua | 2 -- .../lobby/autolobby/AutolobbyMapPreview.lua | 24 +++++++++--------- .../scx_menu/autolobby/map_preview_panel.dds | Bin 0 -> 1582524 bytes 3 files changed, 12 insertions(+), 14 deletions(-) create mode 100644 textures/ui/common/scx_menu/autolobby/map_preview_panel.dds diff --git a/lua/ui/lobby/autolobby/AutolobbyInterface.lua b/lua/ui/lobby/autolobby/AutolobbyInterface.lua index e7b1dc9497..860856b6eb 100644 --- a/lua/ui/lobby/autolobby/AutolobbyInterface.lua +++ b/lua/ui/lobby/autolobby/AutolobbyInterface.lua @@ -88,8 +88,6 @@ local AutolobbyInterface = Class(Group) { LayoutHelpers.ReusedLayoutFor(self.Preview) :AtCenterIn(self, -100, 0) - :Width(400) - :Height(400) :Hide() :End() diff --git a/lua/ui/lobby/autolobby/AutolobbyMapPreview.lua b/lua/ui/lobby/autolobby/AutolobbyMapPreview.lua index 82ae3896dd..e322448733 100644 --- a/lua/ui/lobby/autolobby/AutolobbyMapPreview.lua +++ b/lua/ui/lobby/autolobby/AutolobbyMapPreview.lua @@ -25,10 +25,11 @@ local MapUtil = import("/lua/ui/maputil.lua") local LayoutHelpers = import("/lua/maui/layouthelpers.lua") local Group = import("/lua/maui/group.lua").Group +local Bitmap = import("/lua/maui/bitmap.lua").Bitmap local MapPreview = import("/lua/ui/controls/mappreview.lua").MapPreview local AutolobbyMapPreviewSpawn = import("/lua/ui/lobby/autolobby/AutolobbyMapPreviewSpawn.lua") ----@class UIAutolobbyMapPreview : Group +---@class UIAutolobbyMapPreview : Bitmap ---@field Preview MapPreview ---@field Overlay Bitmap ---@field PathToScenarioFile? FileName @@ -38,13 +39,13 @@ local AutolobbyMapPreviewSpawn = import("/lua/ui/lobby/autolobby/AutolobbyMapPre ---@field MassIcon Bitmap # Acts as a pool ---@field WreckageIcon Bitmap # Acts as a pool ---@field IconTrash TrashBag # Trashbag that contains all icons ----@field SpawnIcons UIAutolobbyMapPreviewSpawn[] -local AutolobbyMapPreview = ClassUI(Group) { +---@field ArmyIcons UIAutolobbyMapPreviewSpawn[] +local AutolobbyMapPreview = ClassUI(Bitmap) { ---@param self UIAutolobbyMapPreview ---@param parent Control __init = function(self, parent) - Group.__init(self, parent) + Bitmap.__init(self, parent, UIUtil.UIFile('/scx_menu/autolobby/map_preview_panel.dds'), 'UIAutolobbyMapPreview') self.Preview = MapPreview(self) @@ -54,7 +55,9 @@ local AutolobbyMapPreview = ClassUI(Group) { self.EnergyIcon = UIUtil.CreateBitmap(self, "/game/build-ui/icon-energy_bmp.dds") self.MassIcon = UIUtil.CreateBitmap(self, "/game/build-ui/icon-mass_bmp.dds") self.WreckageIcon = UIUtil.CreateBitmap(self, "/scx_menu/lan-game-lobby/mappreview/wreckage.dds") - self.SpawnIcons = {} + self.ArmyIcons = {} + + UIUtil.CreateDialogBrackets(self, 40, 32, 40, 32) self.IconTrash = TrashBag() end, @@ -63,7 +66,9 @@ local AutolobbyMapPreview = ClassUI(Group) { ---@param parent Control __post_init = function(self, parent) LayoutHelpers.ReusedLayoutFor(self.Overlay) - :Fill(self) + :Width(442) + :Height(442) + :AtLeftTopIn(self, 1, 1) :DisableHitTest(true) :End() @@ -193,7 +198,7 @@ local AutolobbyMapPreview = ClassUI(Group) { ---@param scenarioSave UIScenarioSaveFile ---@param playerOptions UIAutolobbyPlayer[] _UpdateSpawnLocations = function(self, scenarioInfo, scenarioSave, playerOptions) - local spawnIcons = self.SpawnIcons + local spawnIcons = self.ArmyIcons local positions = MapUtil.GetStartPositionsFromScenario(scenarioInfo, scenarioSave) if not positions then -- clean up @@ -239,11 +244,6 @@ local AutolobbyMapPreview = ClassUI(Group) { ---@param pathToScenarioInfo FileName # a reference to a _scenario.lua file ---@param playerOptions UIAutolobbyPlayer[] UpdateScenario = function(self, pathToScenarioInfo, playerOptions) - -- -- make it idempotent - -- if self.PathToScenarioFile ~= pathToScenarioInfo then - -- return - -- end - -- clear up previous iteration self.IconTrash:Destroy() self.Preview:ClearTexture() diff --git a/textures/ui/common/scx_menu/autolobby/map_preview_panel.dds b/textures/ui/common/scx_menu/autolobby/map_preview_panel.dds new file mode 100644 index 0000000000000000000000000000000000000000..056afbd1266464f0d3fd7e51633dc5340d639072 GIT binary patch literal 1582524 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 In}65;2LdtRDF6Tf literal 0 HcmV?d00001 From 8df32fb02418df5000126317be57717044f5ffe1 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Tue, 19 Nov 2024 20:04:00 +0100 Subject: [PATCH 77/83] Revert "Setup for a panel with brackets" This reverts commit 92ab7f4ea01050cb7f059638315e29ed0fe8a75b. --- lua/ui/lobby/autolobby/AutolobbyInterface.lua | 2 ++ .../lobby/autolobby/AutolobbyMapPreview.lua | 24 +++++++++--------- .../scx_menu/autolobby/map_preview_panel.dds | Bin 1582524 -> 0 bytes 3 files changed, 14 insertions(+), 12 deletions(-) delete mode 100644 textures/ui/common/scx_menu/autolobby/map_preview_panel.dds diff --git a/lua/ui/lobby/autolobby/AutolobbyInterface.lua b/lua/ui/lobby/autolobby/AutolobbyInterface.lua index 860856b6eb..e7b1dc9497 100644 --- a/lua/ui/lobby/autolobby/AutolobbyInterface.lua +++ b/lua/ui/lobby/autolobby/AutolobbyInterface.lua @@ -88,6 +88,8 @@ local AutolobbyInterface = Class(Group) { LayoutHelpers.ReusedLayoutFor(self.Preview) :AtCenterIn(self, -100, 0) + :Width(400) + :Height(400) :Hide() :End() diff --git a/lua/ui/lobby/autolobby/AutolobbyMapPreview.lua b/lua/ui/lobby/autolobby/AutolobbyMapPreview.lua index e322448733..82ae3896dd 100644 --- a/lua/ui/lobby/autolobby/AutolobbyMapPreview.lua +++ b/lua/ui/lobby/autolobby/AutolobbyMapPreview.lua @@ -25,11 +25,10 @@ local MapUtil = import("/lua/ui/maputil.lua") local LayoutHelpers = import("/lua/maui/layouthelpers.lua") local Group = import("/lua/maui/group.lua").Group -local Bitmap = import("/lua/maui/bitmap.lua").Bitmap local MapPreview = import("/lua/ui/controls/mappreview.lua").MapPreview local AutolobbyMapPreviewSpawn = import("/lua/ui/lobby/autolobby/AutolobbyMapPreviewSpawn.lua") ----@class UIAutolobbyMapPreview : Bitmap +---@class UIAutolobbyMapPreview : Group ---@field Preview MapPreview ---@field Overlay Bitmap ---@field PathToScenarioFile? FileName @@ -39,13 +38,13 @@ local AutolobbyMapPreviewSpawn = import("/lua/ui/lobby/autolobby/AutolobbyMapPre ---@field MassIcon Bitmap # Acts as a pool ---@field WreckageIcon Bitmap # Acts as a pool ---@field IconTrash TrashBag # Trashbag that contains all icons ----@field ArmyIcons UIAutolobbyMapPreviewSpawn[] -local AutolobbyMapPreview = ClassUI(Bitmap) { +---@field SpawnIcons UIAutolobbyMapPreviewSpawn[] +local AutolobbyMapPreview = ClassUI(Group) { ---@param self UIAutolobbyMapPreview ---@param parent Control __init = function(self, parent) - Bitmap.__init(self, parent, UIUtil.UIFile('/scx_menu/autolobby/map_preview_panel.dds'), 'UIAutolobbyMapPreview') + Group.__init(self, parent) self.Preview = MapPreview(self) @@ -55,9 +54,7 @@ local AutolobbyMapPreview = ClassUI(Bitmap) { self.EnergyIcon = UIUtil.CreateBitmap(self, "/game/build-ui/icon-energy_bmp.dds") self.MassIcon = UIUtil.CreateBitmap(self, "/game/build-ui/icon-mass_bmp.dds") self.WreckageIcon = UIUtil.CreateBitmap(self, "/scx_menu/lan-game-lobby/mappreview/wreckage.dds") - self.ArmyIcons = {} - - UIUtil.CreateDialogBrackets(self, 40, 32, 40, 32) + self.SpawnIcons = {} self.IconTrash = TrashBag() end, @@ -66,9 +63,7 @@ local AutolobbyMapPreview = ClassUI(Bitmap) { ---@param parent Control __post_init = function(self, parent) LayoutHelpers.ReusedLayoutFor(self.Overlay) - :Width(442) - :Height(442) - :AtLeftTopIn(self, 1, 1) + :Fill(self) :DisableHitTest(true) :End() @@ -198,7 +193,7 @@ local AutolobbyMapPreview = ClassUI(Bitmap) { ---@param scenarioSave UIScenarioSaveFile ---@param playerOptions UIAutolobbyPlayer[] _UpdateSpawnLocations = function(self, scenarioInfo, scenarioSave, playerOptions) - local spawnIcons = self.ArmyIcons + local spawnIcons = self.SpawnIcons local positions = MapUtil.GetStartPositionsFromScenario(scenarioInfo, scenarioSave) if not positions then -- clean up @@ -244,6 +239,11 @@ local AutolobbyMapPreview = ClassUI(Bitmap) { ---@param pathToScenarioInfo FileName # a reference to a _scenario.lua file ---@param playerOptions UIAutolobbyPlayer[] UpdateScenario = function(self, pathToScenarioInfo, playerOptions) + -- -- make it idempotent + -- if self.PathToScenarioFile ~= pathToScenarioInfo then + -- return + -- end + -- clear up previous iteration self.IconTrash:Destroy() self.Preview:ClearTexture() diff --git a/textures/ui/common/scx_menu/autolobby/map_preview_panel.dds b/textures/ui/common/scx_menu/autolobby/map_preview_panel.dds deleted file mode 100644 index 056afbd1266464f0d3fd7e51633dc5340d639072..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1582524 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 In}65;2LdtRDF6Tf From 523072bc2e9efbdfe5dc68639016ba06fd25d17a Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Tue, 19 Nov 2024 20:05:53 +0100 Subject: [PATCH 78/83] Add brackets --- lua/ui/lobby/autolobby/AutolobbyMapPreview.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lua/ui/lobby/autolobby/AutolobbyMapPreview.lua b/lua/ui/lobby/autolobby/AutolobbyMapPreview.lua index 82ae3896dd..381150d500 100644 --- a/lua/ui/lobby/autolobby/AutolobbyMapPreview.lua +++ b/lua/ui/lobby/autolobby/AutolobbyMapPreview.lua @@ -56,6 +56,8 @@ local AutolobbyMapPreview = ClassUI(Group) { self.WreckageIcon = UIUtil.CreateBitmap(self, "/scx_menu/lan-game-lobby/mappreview/wreckage.dds") self.SpawnIcons = {} + UIUtil.CreateDialogBrackets(self, 30, 24, 30, 24) + self.IconTrash = TrashBag() end, From e991a71f46571ca5f6928d32c91ee636356c8b00 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Tue, 19 Nov 2024 20:06:45 +0100 Subject: [PATCH 79/83] Disable the rejoin functionality It requires more testing. Eventually it should likely be the host that informs a client to rejoin. --- lua/ui/lobby/autolobby/AutolobbyController.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index e006c88b84..4c55cc1ebb 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -859,7 +859,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC -- occasionally send data over the network to create pings on screen self.Trash:Add(ForkThread(self.ShareLaunchStatusThread, self)) - self.Trash:Add(ForkThread(self.CheckForRejoinThread, self)) + -- self.Trash:Add(ForkThread(self.CheckForRejoinThread, self)) -- disabled, for now self:SendData(self.HostID, { Type = "AddPlayer", PlayerOptions = self:CreateLocalPlayer() }) end, From 35d23f73770f38d9c68ca18dee71cbc5734bf3bb Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Tue, 19 Nov 2024 20:07:31 +0100 Subject: [PATCH 80/83] Remove debug code to not launch the game --- lua/ui/lobby/autolobby/AutolobbyController.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/ui/lobby/autolobby/AutolobbyController.lua b/lua/ui/lobby/autolobby/AutolobbyController.lua index 4c55cc1ebb..94d3bf59e0 100644 --- a/lua/ui/lobby/autolobby/AutolobbyController.lua +++ b/lua/ui/lobby/autolobby/AutolobbyController.lua @@ -473,7 +473,7 @@ AutolobbyCommunications = Class(MohoLobbyMethods, AutolobbyServerCommunicationsC ---@param self UIAutolobbyCommunications LaunchThread = function(self) - while not IsDestroyed(self) and false do + while not IsDestroyed(self) do if self:CanLaunch(self.LaunchStatutes) then WaitSeconds(5.0) From cca7d0babceafa23a98cfc02de3249414c7f5651 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Tue, 19 Nov 2024 20:07:55 +0100 Subject: [PATCH 81/83] Removing logging and the like --- lua/ui/lobby/autolobby.lua | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/lua/ui/lobby/autolobby.lua b/lua/ui/lobby/autolobby.lua index 37b708c9d1..2e31c407d0 100644 --- a/lua/ui/lobby/autolobby.lua +++ b/lua/ui/lobby/autolobby.lua @@ -90,10 +90,6 @@ function HostGame(gameName, scenarioFileName, singlePlayer) "_scenario.lua") AutolobbyCommunicationsInstance:HostGame() end - - -- -- start with a loading dialog - -- import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() - -- :CreateLoadingDialog() end local rejoinTest = false @@ -116,26 +112,6 @@ function JoinGame(address, asObserver, playerName, uid) AutolobbyCommunicationsInstance.JoinParameters.DesiredPeerId = uid AutolobbyCommunicationsInstance:JoinGame(address, playerName, uid) end - - if rejoinTest then - rejoinTest = false - ForkThread( - function() - local startSpot = tonumber(GetCommandLineArg("/startspot", 1)[1]) or 1 - if startSpot == 2 then - WaitSeconds(3) - LOG(" -- REJOIN TEST -- ") - if AutolobbyCommunicationsInstance then - AutolobbyCommunicationsInstance:DisconnectFromPeer("3") - end - end - end - ) - end - - -- -- start with a loading dialog - -- import("/lua/ui/lobby/autolobby/AutolobbyInterface.lua").GetSingleton() - -- :CreateLoadingDialog() end --- Called by the engine. From b62a26a2152e65ccab851698baf4067dd636190f Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Tue, 19 Nov 2024 20:35:00 +0100 Subject: [PATCH 82/83] Remove debugging code --- lua/lazyvar.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/lazyvar.lua b/lua/lazyvar.lua index 14a6f5d390..ec623eed6c 100644 --- a/lua/lazyvar.lua +++ b/lua/lazyvar.lua @@ -9,7 +9,7 @@ local setmetatable = setmetatable -- Set this true to get tracebacks in error messages. It slows down lazyvars a lot, -- so don't use except when debugging. -local ExtendedErrorMessages = true +local ExtendedErrorMessages = false local EvalContext = nil local WeakKeyMeta = { __mode = 'k' } From 10a916d30f538450330483a0618d08452558dcd5 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Tue, 19 Nov 2024 20:43:43 +0100 Subject: [PATCH 83/83] Add a snippet --- changelog/snippets/features.6479.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 changelog/snippets/features.6479.md diff --git a/changelog/snippets/features.6479.md b/changelog/snippets/features.6479.md new file mode 100644 index 0000000000..8e5556e10c --- /dev/null +++ b/changelog/snippets/features.6479.md @@ -0,0 +1,7 @@ +- (#6479) Rework the in-game matchmaker lobby from the ground up + +From a user perspective the matchmaker lobby now has a map preview and a connection matrix. The map preview can help the players to understand what they'll be gating into. The connection matrix can help players to understand what source (a player) is connected to what source (another player). The diagonal represents what sources (other players) the local client is connected to. When you receive a message from a peer then it blinks the corresponding box in the matrix. + +From a developers perspective the matchmaker lobby is now maintainable. You can now start the matchmaker lobby locally through your development environment. This allows you to run the matchmaker lobby as if you would find a match in the client. The matchmaker lobby is build from the ground up with maintainability in mind. It now supports a hot reload-like functionality for the interface. This allows you to change the interface on the go, without having to relaunch the game. + +All taken together this is still very much a work in progress and we would love to hear the feedback of the community. We welcome you on Discord in the dev-talk channel.