From d73afd3ed7140db8a4a5ecf61c8f8f95b53e83d5 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Wed, 16 Jul 2025 19:50:12 +0200 Subject: [PATCH 01/18] Add annotations --- engine/User.lua | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/engine/User.lua b/engine/User.lua index 4f23c091965..1fa16989716 100644 --- a/engine/User.lua +++ b/engine/User.lua @@ -124,6 +124,19 @@ --- | 'NUMPAD_DECIMAL' --- | 'NUMPAD_DIVIDE' +---@class UISinglePlayerArmy +---@field PlayerName string +---@field Faction number +---@field Human boolean + +---@class UISinglePlayerSessionConfiguration +---@field scenarioInfo UILobbyScenarioInfo +---@field scenarioMods ModInfo[] +---@field teamInfo table +---@field RandomSeed number +---@field createReplay boolean +---@field playerName string + --- Repeatedly the selection box of the unit to the hovered-over state to create a blinking effect ---@param entityId EntityId ---@param onTime number @@ -903,7 +916,7 @@ function LaunchReplaySession(filename) end --- Launch a new single player session ----@param sessionInfo UIScenarioInfo +---@param sessionInfo UISinglePlayerSessionConfiguration function LaunchSinglePlayerSession(sessionInfo) end From ab6c36be9f6ef08d0f6461ac4a98c03ad351c945 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Wed, 16 Jul 2025 19:50:37 +0200 Subject: [PATCH 02/18] Remove logging --- lua/SinglePlayerLaunch.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/lua/SinglePlayerLaunch.lua b/lua/SinglePlayerLaunch.lua index 7bc493a168d..169ec8e851c 100644 --- a/lua/SinglePlayerLaunch.lua +++ b/lua/SinglePlayerLaunch.lua @@ -4,7 +4,6 @@ local MapUtils = import("/lua/ui/maputil.lua") local aiTypes = import("/lua/ui/lobby/aitypes.lua").aitypes function GetRandomName(faction, aiKey) - WARN('GRN: ',faction) local aiNames = import("/lua/ui/lobby/ainames.lua").ainames local factions = import("/lua/factions.lua").Factions From c9811431401f3f5e876071ca5328e805cfb19a35 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Wed, 16 Jul 2025 19:51:36 +0200 Subject: [PATCH 03/18] Add annotations --- lua/SinglePlayerLaunch.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lua/SinglePlayerLaunch.lua b/lua/SinglePlayerLaunch.lua index 169ec8e851c..05e7bb3511d 100644 --- a/lua/SinglePlayerLaunch.lua +++ b/lua/SinglePlayerLaunch.lua @@ -290,6 +290,9 @@ local function SetupCommandLineSkirmish(scenario, isPerfTest) return sessionInfo end +--- Called by the engine when the `/map` or `/scenario` command line switch is detected. +---@param mapName FileName # Full path to a scenario, e.g. /maps/SCMP_007/SCMP_007_scenario.lua for Open Palms. +---@param isPerfTest boolean function StartCommandLineSession(mapName, isPerfTest) if not mapName then error("SetupCommandLineSession - mapName required") From 10e88711f88fe71c94d5753f36f0b02cb8068ea1 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Wed, 16 Jul 2025 20:01:24 +0200 Subject: [PATCH 04/18] Dynamically compute default options --- lua/SinglePlayerLaunch.lua | 43 +++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/lua/SinglePlayerLaunch.lua b/lua/SinglePlayerLaunch.lua index 05e7bb3511d..9b053b23c54 100644 --- a/lua/SinglePlayerLaunch.lua +++ b/lua/SinglePlayerLaunch.lua @@ -3,6 +3,31 @@ local Prefs = import("/lua/user/prefs.lua") local MapUtils = import("/lua/ui/maputil.lua") local aiTypes = import("/lua/ui/lobby/aitypes.lua").aitypes +--- Assigns the default value of all options to the defaultOptions table. +---@param defaultOptions table +---@param options ScenarioOption[] +local function AssignDefaultOptions (defaultOptions, options) + ---@param option ScenarioOption + for index, option in options do + if option.key and option.values and option.default then + defaultOptions[option.key] = option.values[option.default].key + end + end +end + +--- Generates a table of all default lobby options +--- @return table +local function GetDefaultOptions () + local allLobbyOptions = import("/lua/ui/lobby/lobbyOptions.lua") + + local defaultOptions = {} + AssignDefaultOptions(defaultOptions, allLobbyOptions.teamOptions) + AssignDefaultOptions(defaultOptions, allLobbyOptions.globalOpts) + AssignDefaultOptions(defaultOptions, allLobbyOptions.AIOpts) + + return defaultOptions +end + function GetRandomName(faction, aiKey) local aiNames = import("/lua/ui/lobby/ainames.lua").ainames local factions = import("/lua/factions.lua").Factions @@ -119,22 +144,12 @@ function FixupMapName(mapName) end -local defaultOptions = { - FogOfWar = 'explored', - NoRushOption = 'Off', - PrebuiltUnits = 'Off', - Difficulty = 2, - DoNotShareUnitCap = true, - Timeouts = -1, - GameSpeed = 'normal', - UnitCap = '500', - Victory = 'sandbox', - CheatsEnabled = 'true', - CivilianAlliance = 'enemy', -} local function GetCommandLineOptions(isPerfTest) - local options = table.copy(defaultOptions) + + local options = GetDefaultOptions() + + reprsl(options) if isPerfTest then options.FogOfWar = 'none' From cfec78742562c8c019a4ebcc73f6d0ab0a8d253f Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Wed, 16 Jul 2025 20:11:20 +0200 Subject: [PATCH 05/18] Annotate and adjust location of utility functions --- lua/SinglePlayerLaunch.lua | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/lua/SinglePlayerLaunch.lua b/lua/SinglePlayerLaunch.lua index 9b053b23c54..b2ed817ffe6 100644 --- a/lua/SinglePlayerLaunch.lua +++ b/lua/SinglePlayerLaunch.lua @@ -28,7 +28,11 @@ local function GetDefaultOptions () return defaultOptions end -function GetRandomName(faction, aiKey) +--- Generates a random, thematic name used by AIs. +---@param faction number +---@param aiKey string +---@return string +local function GetRandomName(faction, aiKey) local aiNames = import("/lua/ui/lobby/ainames.lua").ainames local factions = import("/lua/factions.lua").Factions @@ -38,7 +42,7 @@ function GetRandomName(faction, aiKey) if aiKey then local aiName = "AI" - for index, value in aiTypes do + for _, value in aiTypes do if aiKey == value.key then aiName = value.name end @@ -49,11 +53,15 @@ function GetRandomName(faction, aiKey) return name end -function GetRandomFaction() +--- Generates a random faction. +---@return number +local function GetRandomFaction() return math.random(table.getn(import("/lua/factions.lua").Factions)) end -function VerifyScenarioConfiguration(scenarioInfo) +--- Validates the scenario file. +---@param scenarioInfo UIScenarioInfoFile +local function VerifyScenarioConfiguration(scenarioInfo) if scenarioInfo == nil then error("VerifyScenarioConfiguration - no scenarioInfo") end @@ -71,7 +79,15 @@ function VerifyScenarioConfiguration(scenarioInfo) end end - +--- Transforms a map name into a path to the scenario file. This is based on an educated guess - there's no guarantee. +---@param mapName FileName | string +---@return FileName +local function FixupMapName(mapName) + if (not string.find(mapName, "/")) and (not string.find(mapName, "\\")) then + mapName = "/maps/" .. mapName .. "/" .. mapName .. "_scenario.lua" + end + return mapName --[[@as FileName]] +end -- Note that the map name must include the full path, it won't try to guess the path based on name function SetupCampaignSession(scenario, difficulty, inFaction, campaignFlowInfo, isTutorial) @@ -136,12 +152,6 @@ end -function FixupMapName(mapName) - if (not string.find(mapName, "/")) and (not string.find(mapName, "\\")) then - mapName = "/maps/" .. mapName .. "/" .. mapName .. "_scenario.lua" - end - return mapName -end From 3e4263dbdcd9a44adf70b6400cf44dd162f11455 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Wed, 16 Jul 2025 20:23:26 +0200 Subject: [PATCH 06/18] Add annotations --- lua/SinglePlayerLaunch.lua | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/lua/SinglePlayerLaunch.lua b/lua/SinglePlayerLaunch.lua index b2ed817ffe6..71cee99e1d3 100644 --- a/lua/SinglePlayerLaunch.lua +++ b/lua/SinglePlayerLaunch.lua @@ -89,7 +89,13 @@ local function FixupMapName(mapName) return mapName --[[@as FileName]] end --- Note that the map name must include the full path, it won't try to guess the path based on name +--- Populates a session to launch a campaign scenario. +---@param scenario UIScenarioInfoFile # Map name must be a full path to a campaign scenario, it won't try to guess it based on just a name. +---@param difficulty number +---@param inFaction? number +---@param campaignFlowInfo? any +---@param isTutorial? boolean +---@return UISinglePlayerSessionConfiguration function SetupCampaignSession(scenario, difficulty, inFaction, campaignFlowInfo, isTutorial) local factions = import("/lua/factions.lua").Factions local faction = inFaction or 1 @@ -150,11 +156,6 @@ function SetupCampaignSession(scenario, difficulty, inFaction, campaignFlowInfo, end - - - - - local function GetCommandLineOptions(isPerfTest) local options = GetDefaultOptions() @@ -189,8 +190,10 @@ local function GetCommandLineOptions(isPerfTest) return options end - -function SetupBotSession(mapName) +--- Populates a session where all armies are AI. +---@param mapName any +---@return table +local function SetupBotSession(mapName) if not mapName then error("SetupBotSession - mapName required") end @@ -245,8 +248,11 @@ function SetupBotSession(mapName) return sessionInfo end - -local function SetupCommandLineSkirmish(scenario, isPerfTest) +--- Populates a session designed for a simple skirmish. The player is put into the first slot. All other slots are populated by AIs. +---@param scenario UIScenarioInfoFile +---@param isPerfTest boolean +---@return table +local function SetupSkirmishSession(scenario, isPerfTest) local faction if HasCommandLineArg("/faction") then @@ -342,7 +348,7 @@ function StartCommandLineSession(mapName, isPerfTest) end sessionInfo = SetupCampaignSession(scenario, difficulty, faction) else - sessionInfo = SetupCommandLineSkirmish(scenario, isPerfTest) + sessionInfo = SetupSkirmishSession(scenario, isPerfTest) end LaunchSinglePlayerSession(sessionInfo) end From 3a29fbbf09b4d0cc64ef9e29f87f9e3e185e1dca Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Wed, 16 Jul 2025 20:33:41 +0200 Subject: [PATCH 07/18] Add utility function to support the `/gameptions` argument --- lua/SinglePlayerLaunch.lua | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/lua/SinglePlayerLaunch.lua b/lua/SinglePlayerLaunch.lua index 71cee99e1d3..66533f06fbd 100644 --- a/lua/SinglePlayerLaunch.lua +++ b/lua/SinglePlayerLaunch.lua @@ -28,6 +28,24 @@ local function GetDefaultOptions () return defaultOptions end +--- Generates a table of all set lobby options. +--- +--- Lobby options can be set using the `/gameoptions` argument. The format is `/gameoptions key:value key:value`. The correct key-value pairs can be found in `lua\ui\lobby\lobbyOptions.lua`. As an example: `/gameoptions AllowObservers:true CivilianAlliance:Enemy` +local function GetOptions() + local options = GetDefaultOptions() + local parsedOptions = import("/lua/system/utils.lua").GetCommandLineArgTable("/gameoptions") + + for key, value in parsedOptions do + if options[key] then + options[key] = value + else + WARN("Unknown option: " .. tostring(key) .. " with value " .. tostring(value)) + end + end + + return options +end + --- Generates a random, thematic name used by AIs. ---@param faction number ---@param aiKey string @@ -191,9 +209,9 @@ local function GetCommandLineOptions(isPerfTest) end --- Populates a session where all armies are AI. ----@param mapName any +---@param scenario UIScenarioInfoFile ---@return table -local function SetupBotSession(mapName) +local function SetupBotSession(scenario) if not mapName then error("SetupBotSession - mapName required") end @@ -348,7 +366,11 @@ function StartCommandLineSession(mapName, isPerfTest) end sessionInfo = SetupCampaignSession(scenario, difficulty, faction) else - sessionInfo = SetupSkirmishSession(scenario, isPerfTest) + if HasCommandLineArg("/observe") then + sessionInfo = SetupBotSession(scenario) + else + sessionInfo = SetupSkirmishSession(scenario, isPerfTest) + end end LaunchSinglePlayerSession(sessionInfo) end From 0d69d757dbdeed0ab34bb7c13144ba59ca8c72d6 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Wed, 16 Jul 2025 20:43:08 +0200 Subject: [PATCH 08/18] Add utility function to get mods from command line --- lua/SinglePlayerLaunch.lua | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lua/SinglePlayerLaunch.lua b/lua/SinglePlayerLaunch.lua index 66533f06fbd..f93f584ead6 100644 --- a/lua/SinglePlayerLaunch.lua +++ b/lua/SinglePlayerLaunch.lua @@ -46,6 +46,22 @@ local function GetOptions() return options end +--- Generates a table of all set game mods. +--- +--- Lobby options can be set using the `/gamemods` argument. The format is `/gamemods name:uid name:uid`. The name is not used but it is useful to document what mod the UID is supposed to represent. As an example: `/gamemods m27ai:f27c55b4-v075-55b4-92b6-64398e75e23f rngai:faf0863e-94a0-b0b0-9ba583e9feb4` +local function GetMods() + local mods = {} + local parsedMods = import("/lua/system/utils.lua").GetCommandLineArgTable("/gamemods") + + ---@param name string + ---@param uid string + for name, uid in parsedMods do + mods[uid]=true + end + + return mods +end + --- Generates a random, thematic name used by AIs. ---@param faction number ---@param aiKey string From 37407d26f4e0f1fddf104f160b601fa13beacd33 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Wed, 16 Jul 2025 22:16:02 +0200 Subject: [PATCH 09/18] Fix incorrect type of mod The game expects a list of (parsed) mod_info files. --- lua/SinglePlayerLaunch.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lua/SinglePlayerLaunch.lua b/lua/SinglePlayerLaunch.lua index f93f584ead6..f030d81b0d3 100644 --- a/lua/SinglePlayerLaunch.lua +++ b/lua/SinglePlayerLaunch.lua @@ -49,6 +49,7 @@ end --- Generates a table of all set game mods. --- --- Lobby options can be set using the `/gamemods` argument. The format is `/gamemods name:uid name:uid`. The name is not used but it is useful to document what mod the UID is supposed to represent. As an example: `/gamemods m27ai:f27c55b4-v075-55b4-92b6-64398e75e23f rngai:faf0863e-94a0-b0b0-9ba583e9feb4` +---@return ModInfo[] local function GetMods() local mods = {} local parsedMods = import("/lua/system/utils.lua").GetCommandLineArgTable("/gamemods") @@ -59,7 +60,7 @@ local function GetMods() mods[uid]=true end - return mods + return import("/lua/MODS.LUA").GetGameMods(mods) end --- Generates a random, thematic name used by AIs. From c78b03d6ff753045d07ff61ae02ebd2003eefb0d Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Wed, 16 Jul 2025 22:17:02 +0200 Subject: [PATCH 10/18] Add support for mod selection via arguments for bot session --- lua/SinglePlayerLaunch.lua | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/lua/SinglePlayerLaunch.lua b/lua/SinglePlayerLaunch.lua index f030d81b0d3..4c4ee5fb4be 100644 --- a/lua/SinglePlayerLaunch.lua +++ b/lua/SinglePlayerLaunch.lua @@ -229,30 +229,17 @@ end ---@param scenario UIScenarioInfoFile ---@return table local function SetupBotSession(scenario) - if not mapName then - error("SetupBotSession - mapName required") - end - - mapName = FixupMapName(mapName) - - local sessionInfo = {} + ---@type UISinglePlayerSessionConfiguration + sessionInfo = { } sessionInfo.playerName = Prefs.GetFromCurrentProfile('Name') or 'Player' - sessionInfo.createReplay = false - - sessionInfo.scenarioInfo = import("/lua/ui/maputil.lua").LoadScenario(mapName) - if not sessionInfo.scenarioInfo then - error("Unable to load map " .. mapName) - end + sessionInfo.createReplay = true + sessionInfo.scenarioInfo = scenario + sessionInfo.scenarioInfo.Options = GetOptions() + sessionInfo.scenarioMods = GetMods() VerifyScenarioConfiguration(sessionInfo.scenarioInfo) - local armies = sessionInfo.scenarioInfo.Configurations.standard.teams[1].armies - - sessionInfo.teamInfo = {} - - local numColors = table.getn(import("/lua/gamecolors.lua").GameColors.PlayerColors) - local ai local aiopt = GetCommandLineArg("/ai", 1) if aiopt then @@ -261,6 +248,10 @@ local function SetupBotSession(scenario) ai = aitypes[1].key end + sessionInfo.teamInfo = { } + + local armies = sessionInfo.scenarioInfo.Configurations.standard.teams[1].armies + local numColors = table.getn(import("/lua/gamecolors.lua").GameColors.PlayerColors) for index, name in armies do sessionInfo.teamInfo[index] = import("/lua/ui/lobby/lobbycomm.lua").GetDefaultPlayerOptions(sessionInfo.playerName) sessionInfo.teamInfo[index].PlayerName = name @@ -272,9 +263,6 @@ local function SetupBotSession(scenario) sessionInfo.teamInfo[index].AIPersonality = ai end - sessionInfo.scenarioInfo.Options = GetCommandLineOptions(false) - sessionInfo.scenarioMods = import("/lua/mods.lua").GetCampaignMods(sessionInfo.scenarioInfo) - local seed = GetCommandLineArg("/seed", 1) if seed then sessionInfo.RandomSeed = tonumber(seed[1]) From 254d79925aed544e9009627264d4ef8e226a12a7 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Thu, 17 Jul 2025 15:27:47 +0200 Subject: [PATCH 11/18] Add annotations --- lua/system/utils.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lua/system/utils.lua b/lua/system/utils.lua index df07239d01e..0784e57c389 100644 --- a/lua/system/utils.lua +++ b/lua/system/utils.lua @@ -750,6 +750,8 @@ end -- Example: -- command line args: /arg key1:value1 key2:value2 -- GetCommandLineArgTable("/arg") -> {key1="value1", key2="value2"} +---@param option string +---@return table function GetCommandLineArgTable(option) -- Find total number of args local nextMax = 1 From 25244c495d131550da6d881126614a8995d3856b Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Thu, 17 Jul 2025 15:28:03 +0200 Subject: [PATCH 12/18] Add support for bot assignment --- lua/SinglePlayerLaunch.lua | 114 +++++++++++++++++++++++++++---------- 1 file changed, 83 insertions(+), 31 deletions(-) diff --git a/lua/SinglePlayerLaunch.lua b/lua/SinglePlayerLaunch.lua index 4c4ee5fb4be..367d8650a98 100644 --- a/lua/SinglePlayerLaunch.lua +++ b/lua/SinglePlayerLaunch.lua @@ -3,10 +3,14 @@ local Prefs = import("/lua/user/prefs.lua") local MapUtils = import("/lua/ui/maputil.lua") local aiTypes = import("/lua/ui/lobby/aitypes.lua").aitypes +---@class UISessionBotInfo +---@field Key string +---@field StartSlot number + --- Assigns the default value of all options to the defaultOptions table. ---@param defaultOptions table ---@param options ScenarioOption[] -local function AssignDefaultOptions (defaultOptions, options) +local function AssignDefaultOptions(defaultOptions, options) ---@param option ScenarioOption for index, option in options do if option.key and option.values and option.default then @@ -17,7 +21,7 @@ end --- Generates a table of all default lobby options --- @return table -local function GetDefaultOptions () +local function GetDefaultOptions() local allLobbyOptions = import("/lua/ui/lobby/lobbyOptions.lua") local defaultOptions = {} @@ -28,9 +32,10 @@ local function GetDefaultOptions () return defaultOptions end ---- Generates a table of all set lobby options. ---- ---- Lobby options can be set using the `/gameoptions` argument. The format is `/gameoptions key:value key:value`. The correct key-value pairs can be found in `lua\ui\lobby\lobbyOptions.lua`. As an example: `/gameoptions AllowObservers:true CivilianAlliance:Enemy` +--- Generates a table of all set lobby options. +--- +--- Lobby options can be set using the `/gameoptions` argument. The format is `/gameoptions key:value key:value ...`. The correct key-value pairs can be found in `lua\ui\lobby\lobbyOptions.lua`. As an example: `/gameoptions AllowObservers:true CivilianAlliance:Enemy` +--- @return table local function GetOptions() local options = GetDefaultOptions() local parsedOptions = import("/lua/system/utils.lua").GetCommandLineArgTable("/gameoptions") @@ -47,8 +52,8 @@ local function GetOptions() end --- Generates a table of all set game mods. ---- ---- Lobby options can be set using the `/gamemods` argument. The format is `/gamemods name:uid name:uid`. The name is not used but it is useful to document what mod the UID is supposed to represent. As an example: `/gamemods m27ai:f27c55b4-v075-55b4-92b6-64398e75e23f rngai:faf0863e-94a0-b0b0-9ba583e9feb4` +--- +--- Lobby options can be set using the `/gamemods` argument. The format is `/gamemods uid:name uid:name ...`. The name is not used but it is useful to document what mod the UID is supposed to represent. As an example: `/gamemods f27c55b4-v075-55b4-92b6-64398e75e23f:m27ai faf0863e-94a0-b0b0-9ba583e9feb4:rngai` ---@return ModInfo[] local function GetMods() local mods = {} @@ -56,13 +61,36 @@ local function GetMods() ---@param name string ---@param uid string - for name, uid in parsedMods do - mods[uid]=true + for uid, name in parsedMods do + mods[uid] = true end return import("/lua/MODS.LUA").GetGameMods(mods) end +--- Generates a table of all defined bots. +--- +--- Lobby options can be set using the `/gameais` argument. The format is `/gameais slot:bot slot:bot ...`. The name is not used but it is useful to document what mod the UID is supposed to represent. As an example: `/gameais 1:m27ai 4:rngai` +---@return UISessionBotInfo[] +local function GetBots() + ---@type UISessionBotInfo[] + local bots = {} + local parsedBots = import("/lua/system/utils.lua").GetCommandLineArgTable("/gameais") + + ---@param key string + ---@param slot string + for slot, key in parsedBots do + local parsedSlot = tonumber(slot) + if parsedSlot then + table.insert(bots, { Key = key, StartSlot = parsedSlot }) + else + WARN("Invalid slot for AI: " .. tostring(id) .. " with value " .. tostring(slot)) + end + end + + return bots +end + --- Generates a random, thematic name used by AIs. ---@param faction number ---@param aiKey string @@ -125,7 +153,7 @@ local function FixupMapName(mapName) end --- Populates a session to launch a campaign scenario. ----@param scenario UIScenarioInfoFile # Map name must be a full path to a campaign scenario, it won't try to guess it based on just a name. +---@param scenario UIScenarioInfoFile # Map name must be a full path to a campaign scenario, it won't try to guess it based on just a name. ---@param difficulty number ---@param inFaction? number ---@param campaignFlowInfo? any @@ -190,7 +218,6 @@ function SetupCampaignSession(scenario, difficulty, inFaction, campaignFlowInfo, return sessionInfo end - local function GetCommandLineOptions(isPerfTest) local options = GetDefaultOptions() @@ -225,33 +252,63 @@ local function GetCommandLineOptions(isPerfTest) return options end ---- Populates a session where all armies are AI. +--- Populates a session where all defined armies are AI. ---@param scenario UIScenarioInfoFile ---@return table local function SetupBotSession(scenario) + VerifyScenarioConfiguration(scenario) + ---@type UISinglePlayerSessionConfiguration - sessionInfo = { } + sessionInfo = {} sessionInfo.playerName = Prefs.GetFromCurrentProfile('Name') or 'Player' sessionInfo.createReplay = true sessionInfo.scenarioInfo = scenario sessionInfo.scenarioInfo.Options = GetOptions() sessionInfo.scenarioMods = GetMods() - VerifyScenarioConfiguration(sessionInfo.scenarioInfo) - - local ai - local aiopt = GetCommandLineArg("/ai", 1) - if aiopt then - ai = aiopt[1] - else - ai = aitypes[1].key + local seed = tonumber(GetCommandLineArg("/seed", 1)) + if seed then + sessionInfo.RandomSeed = seed end - sessionInfo.teamInfo = { } - + sessionInfo.teamInfo = {} local armies = sessionInfo.scenarioInfo.Configurations.standard.teams[1].armies local numColors = table.getn(import("/lua/gamecolors.lua").GameColors.PlayerColors) + + -- advanced bot assignment. Useful if you want specific bots at specific slots. + local bots = GetBots() + if not table.empty(bots) then + + ---@param index number + ---@param botInfo UISessionBotInfo + for index, botInfo in bots do + local faction = GetRandomFaction() + local name = GetRandomName(faction, botInfo.Key) + + sessionInfo.teamInfo[index] = import("/lua/ui/lobby/lobbycomm.lua").GetDefaultPlayerOptions(sessionInfo.playerName) + sessionInfo.teamInfo[index].Faction = faction + sessionInfo.teamInfo[index].PlayerName = name + sessionInfo.teamInfo[index].ArmyName = armies[botInfo.StartSlot] + + sessionInfo.teamInfo[index].AIPersonality = botInfo.Key + sessionInfo.teamInfo[index].StartSlot = botInfo.StartSlot + + sessionInfo.teamInfo[index].Human = false + sessionInfo.teamInfo[index].PlayerColor = math.mod(index, numColors) + sessionInfo.teamInfo[index].ArmyColor = math.mod(index, numColors) + end + + return sessionInfo + end + + -- simple bot assignment. Allows you to quickly populate all slots with a given bot type. + local ai = "rush" + local aiType = GetCommandLineArg("/ai", 1) + if aiType then + ai = aiType[1] + end + for index, name in armies do sessionInfo.teamInfo[index] = import("/lua/ui/lobby/lobbycomm.lua").GetDefaultPlayerOptions(sessionInfo.playerName) sessionInfo.teamInfo[index].PlayerName = name @@ -263,11 +320,6 @@ local function SetupBotSession(scenario) sessionInfo.teamInfo[index].AIPersonality = ai end - local seed = GetCommandLineArg("/seed", 1) - if seed then - sessionInfo.RandomSeed = tonumber(seed[1]) - end - return sessionInfo end @@ -282,7 +334,7 @@ local function SetupSkirmishSession(scenario, isPerfTest) faction = tonumber(GetCommandLineArg("/faction", 1)[1]) local maxFaction = table.getn(import("/lua/factions.lua").Factions) if faction < 1 or faction > maxFaction then - error("SetupCommandLineSession - selected faction index " .. faction .. " must be between 1 and " .. maxFaction) + error("SetupCommandLineSession - selected faction index " .. faction .. " must be between 1 and " .. maxFaction) end else faction = GetRandomFaction() @@ -292,7 +344,7 @@ local function SetupSkirmishSession(scenario, isPerfTest) scenario.Options = GetCommandLineOptions(isPerfTest) - sessionInfo = { } + sessionInfo = {} sessionInfo.playerName = Prefs.GetFromCurrentProfile('Name') or 'Player' sessionInfo.createReplay = true sessionInfo.scenarioInfo = scenario @@ -329,7 +381,7 @@ local function SetupSkirmishSession(scenario, isPerfTest) local extras = MapUtils.GetExtraArmies(sessionInfo.scenarioInfo) if extras then - for k,armyName in extras do + for k, armyName in extras do local index = table.getn(sessionInfo.teamInfo) + 1 sessionInfo.teamInfo[index] = import("/lua/ui/lobby/lobbycomm.lua").GetDefaultPlayerOptions("civilian") sessionInfo.teamInfo[index].PlayerName = 'civilian' From 1abb863e559fbe7b990f888125a9e76ef0cf4c50 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sun, 20 Jul 2025 09:10:33 +0200 Subject: [PATCH 13/18] Fix not being able to default all game options --- lua/SinglePlayerLaunch.lua | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lua/SinglePlayerLaunch.lua b/lua/SinglePlayerLaunch.lua index 367d8650a98..43298641998 100644 --- a/lua/SinglePlayerLaunch.lua +++ b/lua/SinglePlayerLaunch.lua @@ -14,7 +14,12 @@ local function AssignDefaultOptions(defaultOptions, options) ---@param option ScenarioOption for index, option in options do if option.key and option.values and option.default then - defaultOptions[option.key] = option.values[option.default].key + local value = option.values[option.default] + if type(value) == "table" then + defaultOptions[option.key] = value.key + else + defaultOptions[option.key] = value + end end end end From cb6e11c0f9daded5c300b3ce7fc9c21f2b4fa53a Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sun, 20 Jul 2025 09:11:02 +0200 Subject: [PATCH 14/18] Add command line to run the game as fast as possible --- lua/SinglePlayerLaunch.lua | 6 ++++ scripts/LaunchBotSession.ps1 | 68 ++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 scripts/LaunchBotSession.ps1 diff --git a/lua/SinglePlayerLaunch.lua b/lua/SinglePlayerLaunch.lua index 43298641998..56eafbaccf4 100644 --- a/lua/SinglePlayerLaunch.lua +++ b/lua/SinglePlayerLaunch.lua @@ -434,5 +434,11 @@ function StartCommandLineSession(mapName, isPerfTest) sessionInfo = SetupSkirmishSession(scenario, isPerfTest) end end + + -- feature: run the skirmish as fast as possible + if HasCommandLineArg("/runWithTheWind") then + ConExecute("wld_RunWithTheWind 1") + end + LaunchSinglePlayerSession(sessionInfo) end diff --git a/scripts/LaunchBotSession.ps1 b/scripts/LaunchBotSession.ps1 new file mode 100644 index 00000000000..2fd1945b7f7 --- /dev/null +++ b/scripts/LaunchBotSession.ps1 @@ -0,0 +1,68 @@ +param ( + [string]$map = "/maps/scmp_009/SCMP_009_scenario.lua", # Default map: Seton's Clutch + [string]$ai = "rush" # Same keys as used in lua\ui\lobby\aitypes.lua (and in lua\aibrains\index.lua) +) + +# 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 +} + +# Build argument list +$args = @( + "/init", "init_local_development.lua", + "/nobugreport", + "/EnableDiskWatch", + + "/log", "dev-bot-session.log", + "/showlog", + + # Seed to use for randomness + "/seed", "1", + + # Enable cheats + "/cheats", + + # Indicates to the engine that we want to quickly start a scenario, skipping the lobby. + "/scenario", $map, + + # Indicates to the startup sequence that we want to observe as a player. You start with the focus army set to a bot. + "/observe", + + # List of all available (sim) mods. There should be no spaces. + # Format: "/gamemods", "mod_id:mod_name" + + # "/gamemods", "joe-ai-01:AverageJoeAI" + + # Bot configuration. There should be no spaces. + # Format: "/gameais", "1:ai_name", "2:ai_name" + + "/gameais", "1:$ai", "2:$ai" + + # Lobby option configuration. There should be no spaces. + # Format: "/gameoptions", "option_key:option_value" + + "/gameoptions", "UnitCap:750" + + # Game option to run the skirmish as fast as possible. + # the `wld_RunWithTheWind` console command in the background. + + "/runWithTheWind" +) + +Write-Host "Launching map: $map with AIs: $ai" +Start-Process -FilePath $gameExecutable -ArgumentList $args From 97f24f59b3f20771f6539744b921c61c4161d269 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sun, 20 Jul 2025 09:19:09 +0200 Subject: [PATCH 15/18] Make game speed adjustable by default --- scripts/LaunchBotSession.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/LaunchBotSession.ps1 b/scripts/LaunchBotSession.ps1 index 2fd1945b7f7..85b9e552af8 100644 --- a/scripts/LaunchBotSession.ps1 +++ b/scripts/LaunchBotSession.ps1 @@ -51,12 +51,12 @@ $args = @( # Bot configuration. There should be no spaces. # Format: "/gameais", "1:ai_name", "2:ai_name" - "/gameais", "1:$ai", "2:$ai" + "/gameais", "1:$ai", "2:$ai", # Lobby option configuration. There should be no spaces. # Format: "/gameoptions", "option_key:option_value" - "/gameoptions", "UnitCap:750" + "/gameoptions", "UnitCap:750", "GameSpeed:adjustable", # Game option to run the skirmish as fast as possible. # the `wld_RunWithTheWind` console command in the background. From 341ce5b453dd8387c0b09a198f33253596465bfb Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sun, 20 Jul 2025 09:22:55 +0200 Subject: [PATCH 16/18] Add license and introduction comments --- scripts/LaunchBotSession.ps1 | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/scripts/LaunchBotSession.ps1 b/scripts/LaunchBotSession.ps1 index 85b9e552af8..daaca94bab9 100644 --- a/scripts/LaunchBotSession.ps1 +++ b/scripts/LaunchBotSession.ps1 @@ -1,3 +1,32 @@ +# ****************************************************************************************************** +# ** Copyright (c) 2025 FAForever +# ** +# ** 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. +# ****************************************************************************************************** + +# The purpose of this script is to launch a bot session through the command line. It is intended to be +# an example of how to launch a bot session. For more information, see also: +# +# - lua\SinglePlayerLaunch.lua +# +# And specifically the `StartCommandLineSession` and `SetupBotSession` functions. + param ( [string]$map = "/maps/scmp_009/SCMP_009_scenario.lua", # Default map: Seton's Clutch [string]$ai = "rush" # Same keys as used in lua\ui\lobby\aitypes.lua (and in lua\aibrains\index.lua) @@ -32,7 +61,7 @@ $args = @( "/showlog", # Seed to use for randomness - "/seed", "1", + # "/seed", "1", # Enable cheats "/cheats", From 1a19dca6f6d3750d459660118bc993c3568bad9c Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sun, 20 Jul 2025 09:31:42 +0200 Subject: [PATCH 17/18] Add a changelog snippet --- changelog/snippets/other.6888.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog/snippets/other.6888.md diff --git a/changelog/snippets/other.6888.md b/changelog/snippets/other.6888.md new file mode 100644 index 00000000000..85cc6ed14c2 --- /dev/null +++ b/changelog/snippets/other.6888.md @@ -0,0 +1,3 @@ +- (#6888) Make it more convenient to launch a (bot) session through the command line + +With these changes we shorten the workflow to test (sim) mods and in particular AI mods. There is an example PowerShell script file in the `scripts` folder to help you get started. From 13d2a658bbd8adeaf295646bb4d38c7c4a32305c Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Fri, 10 Oct 2025 06:21:38 +0200 Subject: [PATCH 18/18] Add support for custom armies --- lua/SinglePlayerLaunch.lua | 26 ++++++++++++++++++++++++++ lua/ui/maputil.lua | 8 +++++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/lua/SinglePlayerLaunch.lua b/lua/SinglePlayerLaunch.lua index 56eafbaccf4..98ef28e5dfc 100644 --- a/lua/SinglePlayerLaunch.lua +++ b/lua/SinglePlayerLaunch.lua @@ -304,6 +304,19 @@ local function SetupBotSession(scenario) sessionInfo.teamInfo[index].ArmyColor = math.mod(index, numColors) end + -- add all additional, non-player armies + local extras = MapUtils.GetExtraArmies(sessionInfo.scenarioInfo) + if extras then + for k,armyName in extras do + local index = table.getn(sessionInfo.teamInfo) + 1 + sessionInfo.teamInfo[index] = import("/lua/ui/lobby/lobbycomm.lua").GetDefaultPlayerOptions("civilian") + sessionInfo.teamInfo[index].PlayerName = 'civilian' + sessionInfo.teamInfo[index].Civilian = true + sessionInfo.teamInfo[index].ArmyName = armyName + sessionInfo.teamInfo[index].Human = false + end + end + return sessionInfo end @@ -325,6 +338,19 @@ local function SetupBotSession(scenario) sessionInfo.teamInfo[index].AIPersonality = ai end + -- add all additional, non-player armies + local extras = MapUtils.GetExtraArmies(sessionInfo.scenarioInfo) + if extras then + for k,armyName in extras do + local index = table.getn(sessionInfo.teamInfo) + 1 + sessionInfo.teamInfo[index] = import("/lua/ui/lobby/lobbycomm.lua").GetDefaultPlayerOptions("civilian") + sessionInfo.teamInfo[index].PlayerName = 'civilian' + sessionInfo.teamInfo[index].Civilian = true + sessionInfo.teamInfo[index].ArmyName = armyName + sessionInfo.teamInfo[index].Human = false + end + end + return sessionInfo end diff --git a/lua/ui/maputil.lua b/lua/ui/maputil.lua index 158d1bee4de..968e684b3e3 100644 --- a/lua/ui/maputil.lua +++ b/lua/ui/maputil.lua @@ -431,12 +431,14 @@ end ---@return string[] function GetExtraArmies(scenario) if scenario.Configurations.standard and scenario.Configurations.standard.teams then - local teams = scenario.Configurations.standard.teams - if teams.ExtraArmies then - local armies = STR_GetTokens(teams.ExtraArmies, ' ') + local properties = scenario.Configurations.standard.customprops + if properties.ExtraArmies then + local armies = StringSplit(properties.ExtraArmies, ' ') return armies end end + + return {} end --- Validate options provided by the scenario file.