From 6780001ed8a649cd4c9af492c7e892c6e3e17877 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Mon, 14 Oct 2024 10:29:03 -0700 Subject: [PATCH] Implement command line launch of the auto lobby for LAN games (#6474) --- changelog/snippets/other.6474.md | 2 + engine/User.lua | 8 +-- lua/system/utils.lua | 9 +-- lua/ui/lobby/autolobby.lua | 11 +-- lua/ui/uimain.lua | 44 +++++++++--- scripts/LaunchFAInstances.ps1 | 116 +++++++++++++++++++++++++++++++ 6 files changed, 169 insertions(+), 21 deletions(-) create mode 100644 changelog/snippets/other.6474.md create mode 100644 scripts/LaunchFAInstances.ps1 diff --git a/changelog/snippets/other.6474.md b/changelog/snippets/other.6474.md new file mode 100644 index 0000000000..e0944a855c --- /dev/null +++ b/changelog/snippets/other.6474.md @@ -0,0 +1,2 @@ +- (#6474) Implement command line launch of the auto lobby for LAN games. This allows rapidly testing multiplayer in a local environment. + Further details on the command line switches used and an example script for launching multiple instances are in the linked pull request. diff --git a/engine/User.lua b/engine/User.lua index ce06129cc0..63c82e050a 100644 --- a/engine/User.lua +++ b/engine/User.lua @@ -355,13 +355,13 @@ end function GetCamera(name) end ---- Gets the following arguments to a commandline option. For example, if `/arg -flag key:value drop` ---- was passed to the commandline, then `GetCommandLineArg("/arg", 2)` would return ---- `{"-flag", "key:value"}` +--- Gets the "arguments" (tokens split by spaces) that follow a commandline option, +--- disregarding if they start with `/` like other commandline options. +--- Returns `false` if there are not `maxArgs` tokens after the `option`. ---@see GetCommandLineArgTable(option) for parsing key-values ---@param option string ---@param maxArgs number ----@return string[]? +---@return string[] | false function GetCommandLineArg(option, maxArgs) end diff --git a/lua/system/utils.lua b/lua/system/utils.lua index 990d6e2451..8db7ebcd2a 100644 --- a/lua/system/utils.lua +++ b/lua/system/utils.lua @@ -731,12 +731,13 @@ end -- GetCommandLineArgTable("/arg") -> {key1="value1", key2="value2"} function GetCommandLineArgTable(option) -- Find total number of args - local next = 1 + local nextMax = 1 local args, nextArgs = nil, nil repeat - nextArgs, args = GetCommandLineArg(option, next), nextArgs - next = next + 1 - until not nextArgs + nextArgs, args = GetCommandLineArg(option, nextMax), nextArgs + nextMax = nextMax + 1 + -- GetCommandLineArg tokenizes without being limited by `/`, so we need to stop manually + until not nextArgs or nextArgs[nextMax - 1]:sub(0,1) == "/" -- Construct result table local result = {} diff --git a/lua/ui/lobby/autolobby.lua b/lua/ui/lobby/autolobby.lua index 2328daa213..d20479e4e7 100644 --- a/lua/ui/lobby/autolobby.lua +++ b/lua/ui/lobby/autolobby.lua @@ -92,8 +92,9 @@ 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 @@ -104,9 +105,9 @@ local function MakeLocalPlayerInfo(name) result.Team = tonumber(GetCommandLineArg("/team", 1)[1]) result.StartSpot = tonumber(GetCommandLineArg("/startspot", 1)[1]) or false - result.DEV = tonumber(GetCommandLineArg("/deviation", 1)[1]) or "" - result.MEAN = tonumber(GetCommandLineArg("/mean", 1)[1]) or "" - result.NG = tonumber(GetCommandLineArg("/numgames", 1)[1]) or "" + 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) @@ -447,7 +448,7 @@ end -- join an already existing lobby function JoinGame(address, asObserver, playerName, uid) - LOG("Joingame (name=" .. playerName .. ", uid=" .. uid .. ", address=" .. address ..")") + 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 diff --git a/lua/ui/uimain.lua b/lua/ui/uimain.lua index 68bccf10f9..e401dc5b80 100644 --- a/lua/ui/uimain.lua +++ b/lua/ui/uimain.lua @@ -63,18 +63,46 @@ function StartFrontEndUI() end end - ---Used by command line to host a game -function StartHostLobbyUI(protocol, port, playerName, gameName, mapName, natTraversalProvider) - LOG("Command line hostlobby") - local lobby = import("/lua/ui/lobby/lobby.lua") +--- Hosts a multiplayer LAN lobby. +--- Called by the engine with the command line argument `/hostgame ` +--- Add the argument `/players ` to use the auto lobby. `/` to choose a faction. +---@param protocol UILobbyProtocols +---@param port number +---@param playerName string +---@param gameName string +---@param mapFile FileName +---@param natTraversalProvider userdata? +function StartHostLobbyUI(protocol, port, playerName, gameName, mapFile, natTraversalProvider) + LOG("Hosting lobby from the command line") + local lobby + -- auto lobby only works with 2+ players + local autoStart = GetCommandLineArg("/players", 1)[1] >= 2 + if autoStart then + lobby = import("/lua/ui/lobby/autolobby.lua") + else + lobby = import("/lua/ui/lobby/lobby.lua") + end lobby.CreateLobby(protocol, port, playerName, nil, natTraversalProvider, GetFrame(0), StartFrontEndUI) - lobby.HostGame(gameName, mapName, false) + lobby.HostGame(gameName, mapFile, false) end ---Used by command line to join a game +--- Joins a multiplayer lobby. +--- Called by the engine with the command line argument `/joingame
` +--- Add the argument `/players ` to use the auto lobby. `/` to choose a faction. +---@param protocol UILobbyProtocols +---@param address string +---@param playerName string +---@param natTraversalProvider userdata? function StartJoinLobbyUI(protocol, address, playerName, natTraversalProvider) - local lobby = import("/lua/ui/lobby/lobby.lua") + LOG("Joining lobby from the command line") -- can also be from lobby.lua ReturnToMenu(true), but that never gets called + local lobby + -- auto lobby only works with 2+ players + local autoStart = GetCommandLineArg("/players", 1)[1] >= 2 + if autoStart then + lobby = import("/lua/ui/lobby/autolobby.lua") + else + lobby = import("/lua/ui/lobby/lobby.lua") + end local port = 0 lobby.CreateLobby(protocol, port, playerName, nil, natTraversalProvider, GetFrame(0), StartFrontEndUI) lobby.JoinGame(address, false) diff --git a/scripts/LaunchFAInstances.ps1 b/scripts/LaunchFAInstances.ps1 new file mode 100644 index 0000000000..8a22fca4a6 --- /dev/null +++ b/scripts/LaunchFAInstances.ps1 @@ -0,0 +1,116 @@ +param ( + [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 +) + +# Base path to the bin directory +$binPath = "C:\ProgramData\FAForever\bin" + +# Paths to the potential executables within the base path +$debuggerExecutable = Join-Path $binPath "FAFDebugger.exe" +$regularExecutable = Join-Path $binPath "ForgedAlliance.exe" + +# Check for the existence of the executables and choose accordingly +if (Test-Path $debuggerExecutable) { + $gameExecutable = $debuggerExecutable + Write-Output "Using debugger executable: $gameExecutable" +} elseif (Test-Path $regularExecutable) { + $gameExecutable = $regularExecutable + Write-Output "Debugger not found, using regular executable: $gameExecutable" +} else { + Write-Output "Neither debugger nor regular executable found in $binPath. Exiting script." + exit 1 +} + +# Command-line arguments common for all instances +$baseArguments = '/init "init_dev.lua" /EnableDiskWatch /nomovie /RunWithTheWind /gameoptions CheatsEnabled:true GameSpeed:adjustable ' + +# Game-specific settings +$hostProtocol = "udp" +$hostPlayerName = "HostPlayer_1" +$gameName = "MyGame" + +# Array of factions to choose from +$factions = @("UEF", "Seraphim", "Cybran", "Aeon") + +# Get the screen resolution (for placing and resizing the windows) +Add-Type -AssemblyName System.Windows.Forms +$screenWidth = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Width +$screenHeight = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Height + +# Calculate the number of rows and columns for the grid layout +$columns = [math]::Ceiling([math]::Sqrt($players)) +$rows = [math]::Ceiling($players / $columns) + +# Calculate the size of each window based on the grid +# Limit the window size to 1024x768 as the game session will not launch if it is smaller +$windowWidth = [math]::Max([math]::Floor($screenWidth / $columns), 1024) +$windowHeight = [math]::Max([math]::Floor($screenHeight / $rows), 768) + +# Function to launch a single game instance +function Launch-GameInstance { + param ( + [int]$instanceNumber, + [int]$xPos, + [int]$yPos, + [string]$arguments + ) + + # Add window position and size arguments + $arguments += " /position $xPos $yPos /size $windowWidth $windowHeight" + + try { + Start-Process -FilePath $gameExecutable -ArgumentList $arguments -NoNewWindow + Write-Host "Launched instance $instanceNumber at position ($xPos, $yPos) with size ($windowWidth, $windowHeight) and arguments: $arguments" + } catch { + Write-Host "Failed to launch instance ${instanceNumber}: $_" + } +} + +# Function to calculate team argument based on instance number and team configuration +function Get-TeamArgument { + param ( + [int]$instanceNumber + ) + + if ($teams -eq 0) { + return "" # No team argument for FFA + } + + # Calculate team number; additional +1 because player team indices start at 2 + return "/team $((($instanceNumber % $teams) + 1 + 1))" +} + +# Prepare arguments and launch instances +if ($players -eq 1) { + $logFile = "dev.log" + Launch-GameInstance -instanceNumber 1 -xPos 0 -yPos 0 -arguments "/log $logFile /showlog /map $map $baseArguments" +} else { + $hostLogFile = "host_dev_1.log" + $hostFaction = $factions | Get-Random + $hostTeamArgument = Get-TeamArgument -instanceNumber 0 + $hostArguments = "/log $hostLogFile /showlog /hostgame $hostProtocol $port $hostPlayerName $gameName $map /startspot 1 /players $players /$hostFaction $hostTeamArgument $baseArguments" + + # Launch host game instance + Launch-GameInstance -instanceNumber 1 -xPos 0 -yPos 0 -arguments $hostArguments + + # Client game instances + for ($i = 1; $i -lt $players; $i++) { + $row = [math]::Floor($i / $columns) + $col = $i % $columns + $xPos = $col * $windowWidth + $yPos = $row * $windowHeight + + $clientLogFile = "client_dev_$($i + 1).log" + $clientPlayerName = "ClientPlayer_$($i + 1)" + $clientFaction = $factions | Get-Random + $clientTeamArgument = Get-TeamArgument -instanceNumber $i + $clientArguments = "/log $clientLogFile /joingame $hostProtocol localhost:$port $clientPlayerName /startspot $($i + 1) /players $players /$clientFaction $clientTeamArgument $baseArguments" + + Launch-GameInstance -instanceNumber ($i + 1) -xPos $xPos -yPos $yPos -arguments $clientArguments + } +} + +Write-Host "$players instance(s) of the game launched. Host is running at port $port."