Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/snippets/fix.7057.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- (#7057) Remove game options of unused mods from game launch info to try to get live replays to work.
109 changes: 106 additions & 3 deletions lua/ui/lobby/lobby.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@
--*
--* Copyright © 2005 Gas Powered Games, Inc. All rights reserved.
--*****************************************************************************

-- This file implements the main lobby screen
-- To see the options/map selection screen, see mapselect.lua
-- For mods, modsmanager.lua
-- For unit restrictions, unitsmanager.lua
-- For "patchnotes" screen, changelog.lua
-- For load game screen, saveload.lua CreateLoadDialog

local GameVersion = import("/lua/version.lua").GetVersion
local UIUtil = import("/lua/ui/uiutil.lua")
local MenuCommon = import("/lua/ui/menus/menucommon.lua")
Expand Down Expand Up @@ -58,6 +66,8 @@ if versionType != "FAF" then
ConExecute("net_DebugLevel 10")
end

local DebugComponent = import("/lua/shared/components/DebugComponent.lua").DebugComponent

function GetAITypes()
AIKeys = {}
AIStrings = {}
Expand All @@ -82,11 +92,32 @@ if HasCommandLineArg("/syncreplay") and HasCommandLineArg("/gpgnet") then
IsSyncReplayServer = true
end

local globalOpts = import("/lua/ui/lobby/lobbyoptions.lua").globalOpts
local teamOpts = import("/lua/ui/lobby/lobbyoptions.lua").teamOptions
local AIOpts = import("/lua/ui/lobby/lobbyoptions.lua").AIOpts
local lobbyOptions = import("/lua/ui/lobby/lobbyoptions.lua")
local globalOpts = lobbyOptions.globalOpts
local teamOpts = lobbyOptions.teamOptions
local AIOpts = lobbyOptions.AIOpts
local gameColors = import("/lua/gamecolors.lua").GameColors

-- Table mapping option keys to mods that use them
-- Format: { [optionKey] = { modName1 = true, modName2 = true, ... } }
---@type table<string, table<string, true>>
local ModOptionMapping = {}

-- Set of option keys from the original/default lobbyOptions.lua
-- Used to distinguish default options from mod-added options
---@type table<string, true>
local DefaultOptionKeys = {}

-- Initialize DefaultOptionKeys with the original lobbyOptions.lua options
local function initOptionKeys(...)
for _, optionTable in ipairs(arg) do
for _, option in optionTable do
DefaultOptionKeys[option.key] = true
end
end
end
initOptionKeys(globalOpts, teamOpts, AIOpts)

local numOpenSlots = LobbyComm.maxPlayerSlots

-- Add lobby options from AI mods
Expand All @@ -102,13 +133,25 @@ function ImportModAIOptions()
alreadyStored = false
for k, v in AIOpts do
if v.key == t.key then
if DebugComponent.EnabledLogging then
LOG(string.format(
'Found duplicate mod option "%s" in mod "%s"'
, t.key
, ModData.name
))
end
alreadyStored = true
break
end
end
if not alreadyStored then
table.insert(AIOpts, t)
end

-- Initialize the option's mod set
ModOptionMapping[t.key] = ModOptionMapping[t.key] or {}
-- Track that this option is used by this mod
ModOptionMapping[t.key][ModData.name] = true
end
end
end
Expand All @@ -118,6 +161,28 @@ ImportModAIOptions()
-- Maps faction identifiers to their names.
local FACTION_NAMES = {[1] = "uef", [2] = "aeon", [3] = "cybran", [4] = "seraphim", [5] = "random" }

--- Helper function: Returns true if the given option key exists in the default lobbyOptions.lua
---@param optionKey string
---@return boolean
local function IsDefaultOption(optionKey)
return DefaultOptionKeys[optionKey] ~= nil
end

--- Helper function: Returns true if the given option is used by any of the given mods
---@param optionKey string
---@param enabledModNames table<string, true>
---@return boolean
local function IsOptionUsedByGivenMods(optionKey, enabledModNames)
local modNamesUsingOpt = ModOptionMapping[optionKey]
if not modNamesUsingOpt then return false end
for modName, _ in modNamesUsingOpt do
if enabledModNames[modName] then
return true
end
end
return false
end

local rehostPlayerOptions = {} -- Player options loaded from preset, used for rehosting

local formattedOptions = {}
Expand Down Expand Up @@ -2285,6 +2350,44 @@ local function TryLaunch(skipNoObserversCheck)
-- set the mods
gameInfo.GameMods = Mods.GetGameMods(gameInfo.GameMods)

--#region Filter GameOptions to remove options from disabled mods
-- Build set of enabled mod names for quick lookup
local enabledModNames = {}
for _, modInfo in gameInfo.GameMods do
enabledModNames[modInfo.name] = true
end

-- Remove options from disabled mods
-- Only remove options that are not in the default lobbyOptions.lua
local keysToRemove = {}
for optionKey, _ in gameInfo.GameOptions do
-- Skip if this is a default option (always keep default options)
if not IsDefaultOption(optionKey) and ModOptionMapping[optionKey] then
-- Check if this option is used by an enabled mod
local isUsed = IsOptionUsedByGivenMods(optionKey, enabledModNames)

-- If NOT used, mark for removal
if not isUsed then
table.insert(keysToRemove, optionKey)
if DebugComponent.EnabledSpewing then
SPEW(string.format('Option "%s" marked for removal because none of these mods are enabled: "%s"'
, optionKey
, table.concatkeys(ModOptionMapping[optionKey], '", "')
))
end
end
end
end
if DebugComponent.EnabledLogging then
LOG(string.format("%d options marked for removal", table.getsize(keysToRemove)))
end

-- Remove the marked keys from GameOptions
for _, key in keysToRemove do
gameInfo.GameOptions[key] = nil
end
--#endregion
Comment on lines +2353 to +2389
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Filter is executed too late to affect clients’ launch payload.

At Line 2348, launch data is already broadcast before this filtering runs, so clients can still receive unfiltered GameOptions. Move the filtering (and mod resolution it depends on) before the broadcast.

💡 Proposed fix
-        lobbyComm:BroadcastData({ Type = 'Launch', GameInfo = gameInfo })
-
-        -- set the mods
-        gameInfo.GameMods = Mods.GetGameMods(gameInfo.GameMods)
+        -- set the mods
+        gameInfo.GameMods = Mods.GetGameMods(gameInfo.GameMods)

         --#region Filter GameOptions to remove options from disabled mods
         -- Build set of enabled mod names for quick lookup
         local enabledModNames = {}
         for _, modInfo in gameInfo.GameMods do
             enabledModNames[modInfo.name] = true
         end
@@
         for _, key in keysToRemove do
             gameInfo.GameOptions[key] = nil
         end
         --#endregion
+
+        -- Broadcast the already-filtered launch payload
+        lobbyComm:BroadcastData({ Type = 'Launch', GameInfo = gameInfo })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lua/ui/lobby/lobby.lua` around lines 2353 - 2389, The GameOptions filtering
runs too late; move the whole filtering block (the code that builds
enabledModNames, iterates gameInfo.GameOptions, uses IsDefaultOption,
ModOptionMapping and IsOptionUsedByGivenMods, collects keysToRemove and nils
them from gameInfo.GameOptions) so it executes before the launch data broadcast
is created/sent (i.e. before the function that prepares/broadcasts the launch
payload to clients). Also ensure any mod resolution this filtering depends on
(the population of gameInfo.GameMods and ModOptionMapping) is performed
beforehand so enabledModNames is accurate; after moving, confirm the broadcast
uses the now-filtered gameInfo.GameOptions.


SetWindowedLobby(false)

Presets.SaveLastGamePreset()
Expand Down
Loading