Skip to content

refactor: extract lobby submodules for better maintainability"#7140

Open
TimMasalme wants to merge 2 commits into
FAForever:developfrom
TimMasalme:refactor/lobby-module-extraction
Open

refactor: extract lobby submodules for better maintainability"#7140
TimMasalme wants to merge 2 commits into
FAForever:developfrom
TimMasalme:refactor/lobby-module-extraction

Conversation

@TimMasalme

@TimMasalme TimMasalme commented Jun 17, 2026

Copy link
Copy Markdown

Summary

lobby.lua had grown to ~7 400 lines, making it very hard to navigate,
review, and test individual features. This PR extracts its major subsystems
into focused modules under lua/ui/lobby/lobby_modules/, reducing the main
file to ~2 400 lines while keeping it as the single orchestration entry point.

Total lines before: ~7 400 in one file
Total lines after: ~2 400 (lobby.lua) + ~5 000 across 12 modules

Extracted modules

File Lines Responsibility
createui.lua 1 666 Slot/player UI construction (CreateUI, CreateSlotsUI, faction selector)
hostutils.lua 555 Host-only slot/team/AI management helpers
messagehandlers.lua 522 All incoming lobbyComm message handlers
launchlogic.lua 475 Launch checks and game-start sequencing
autobalance.lua 396 Auto-balance algorithm
optionsdialog.lua 355 Game options dialog and formatted option lists
maputils.lua 212 Map preview, start-spot, and resource calculations
chathandler.lua 193 Chat input, history, and chat-line rendering
cpubenchmark.lua 173 CPU benchmark scoring and bar UI
pingutils.lua 157 Connection status and ping display
aiutils.lua 155 AI name, key, and tooltip lookups
slotmenu.lua 125 Right-click slot context menu

Architecture

Each module follows the same Init(deps) injection pattern already
established in the codebase:

  • lobby.lua imports each module and owns all shared state (locals).
  • Each module declares its dependencies as module-level locals.
  • SyncModuleDeps() in lobby.lua calls every module's Init{...} to
    (re-)inject the current values before any cross-module call.
  • rawset(getfenv(1), k, v) is used inside Init to bypass SupCom's
    strict-global guard without hitting the 32-upvalue-per-function limit.

InitLobbyComm and its lobbyComm.* callbacks are deliberately not
extracted — they directly reassign lobby.lua's own locals (lobbyComm,
GUI, hostID, localPlayerID, localPlayerName) and KeepAliveThreadFunc
loops on while lobbyComm do, which requires a live upvalue. Extracting them
would add significant complexity for no functional gain.

Testing

Tested in a local FAF session (host + join). No regressions observed in:

  • Slot UI rendering and faction selector
  • Chat, pinging, and CPU benchmark
  • Map preview and game options dialog
  • AI slot assignment and auto-balance
  • Game launch sequence

Summary by CodeRabbit

  • New Features
    • Enhanced lobby auto-balance for teams/start spots, plus automatic team and AI name assignment.
    • Expanded lobby chat with improved editor behavior, public chat, and private messages/whispers.
    • Added CPU benchmarking and live per-player CPU rating tooltips.
    • Upgraded map preview interactions for slot placement, swapping, and team assignment.
    • Improved lobby settings dialog with richer option formatting and scenario-driven option display.
    • Added connection/ping status monitoring utilities and ping tooltips.
    • Introduced a clearer lobby launch/update flow with scenario/mod handling.
  • Refactor
    • Split core lobby functionality into focused modules for better separation of concerns.

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Eleven new Lua modules are added under lua/ui/lobby/lobby_modules/, extracting distinct subsystems from the monolithic lobby.lua. Each module follows a shared Init(deps) dependency-injection pattern and covers: AI utilities, team autobalancing, chat handling, CPU benchmarking, lobby UI construction, host-only operations, launch orchestration, map preview interaction, network message dispatch, options dialog, ping/connection status, and slot context menus.

Changes

Lobby modularization

Layer / File(s) Summary
Pure utility modules: slotmenu, pingutils, aiutils
lobby_modules/slotmenu.lua, lobby_modules/pingutils.lua, lobby_modules/aiutils.lua
slotmenu builds context-menu key/string/tooltip arrays for slot states via GetData, Build, and GetStrings. pingutils computes peer connection status codes and updates UI titles for newly-connected peers; CalcConnectionStatus and EveryoneHasEstablishedConnections validate connectivity across the lobby. aiutils loads AI type metadata, computes AI ratings from map size and cheat multipliers, and constructs PlayerData payloads with color assignment for AI players.
Chat handler and CPU benchmark
lobby_modules/chathandler.lua, lobby_modules/cpubenchmark.lua
chathandler implements AddChatText with faction-colored styling, PublicChat/PrivateChat broadcast over lobbyComm, whisper parsing via ParseWhisper, and the full chat input editor with max-character enforcement, history navigation, and slash-command dispatch. cpubenchmark runs CPUBenchmark work with score skew adjustment, StressCPU multi-pass benchmarking, bar UI updates per slot, and broadcasts scores via lobbyComm.
Options dialog and map preview
lobby_modules/optionsdialog.lua, lobby_modules/maputils.lua
optionsdialog rebuilds formatted game option display data via RefreshOptionDisplayData incorporating scenario/mod state with "Enabled mods" and "Build restriction(s)" special entries, and constructs ShowLobbyOptionsDialog popup with sliders, checkboxes, and Prefs persistence. maputils renders slot markers via RefreshMapPosition and wires rollover/click/right-click handlers via ConfigureMapListeners for fixed-spawn player assignment, click-to-swap state machine, and manual auto-team reassignment.
Autobalance algorithms
lobby_modules/autobalance.lua
Implements multiple candidate-generator strategies (alternating best/worst, average-threshold, round-robin, random), ComputeQuality via TrueSkill, AssignRandomStartSpots with mirrored-slot reordering and best-candidate selection, AssignAutoTeams for axis/parity/manual modes, and AssignAINames from faction name lists without duplicates.
Host-only lobby operations
lobby_modules/hostutils.lua
Slot state APIs (SetPlayerNotReady, SetSlotClosed, SetSlotClosedSpawnMex), bidirectional player/observer conversion (ConvertPlayerToObserver, ConvertObserverToPlayer), AI removal, slot move/swap with sanity checks, observer and player join flows (TryAddObserver, TryAddPlayer) with color assignment and fallback to observer, army/player settings sync (SendArmySettingsToServer, SendPlayerSettingsToServer), button enabled-state refresh, mod compatibility enforcement with peer ejection (UpdateMods), and observer kicking (KickObservers).
Network message dispatch
lobby_modules/messagehandlers.lua
Defines a Handlers table covering all lobby message types (player/observer transitions, game-info sync, color/faction/mod updates, CPU benchmark, disconnect, launch) each with optional Accept/Reject predicates. Dispatch routes incoming messages with WARN on unknown or rejected types.
Launch orchestration and UpdateGame
lobby_modules/launchlogic.lua
TryLaunch performs pre-launch validation, victory-sandbox fallback, observer enforcement, and the full LaunchGame sequence (team/faction/rating prep, gameInfo flattening, scenario defaults, mod application, lobbyComm:LaunchGame). UpdateGame loads scenario state, recomputes AI ratings, assembles preGameData with mod icon replacements via pcall, and refreshes all lobby UI elements (faction/slot enablement, slot backgrounds, lobby background, map name, rule text).
Full lobby UI construction
lobby_modules/createui.lua
CreateSlotsUI builds the per-player slot grid (name/color/faction/team controls, CPU/ping bars, ready checkbox) with all event handlers and host swapping workflow. CreateUI assembles the complete lobby screen: title/rules/mod panels, map preview with zoom, chat display, scrollable options list, launch/load/presets/unit-manager buttons, host observer/admin controls, random map and auto-teams toggles, and observer kick flow. Includes host-only PenguinAutoBalance for 2-team assignment enumeration and player swapping. CreateUI_Faction_Selector creates faction radio-button UI with LastFaction persistence and skin refresh.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐇 Hop hop, the lobby grows tidy and neat,
Each module now carries its own little beat!
Init(deps) the magic that binds them with care,
From chat to launch logic, subsystems laid bare.
No more one giant file — the warren is clean,
The most modular lobby the codebase has seen! ✨

Suggested labels

area: lobby

Suggested reviewers

  • KionX
  • lL1l1
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.04% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: extracting lobby subsystems into modules for improved maintainability.
Description check ✅ Passed The description comprehensively covers objectives, architecture, testing, and includes a detailed module breakdown with line counts and responsibilities.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 11

🧹 Nitpick comments (7)
lua/ui/lobby/lobby_modules/maputils.lua (1)

160-174: ⚡ Quick win

Unused variable requestedFaction.

requestedFaction is assigned at line 162 but never used. Either remove the assignment or pass it to ConvertObserverToPlayer if faction information is needed.

♻️ Proposed fix to remove unused variable
                     elseif IsObserver(localPlayerID) then
                         if lobbyComm:IsHost() then
-                            local requestedFaction = GetSanitisedLastFaction()
                             HostUtils.ConvertObserverToPlayer(FindObserverSlotForID(localPlayerID), slot)
                         else
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lua/ui/lobby/lobby_modules/maputils.lua` around lines 160 - 174, The variable
requestedFaction is assigned the result of GetSanitisedLastFaction() but is
never used in the subsequent code. Remove the assignment of requestedFaction on
the line where it gets assigned from GetSanitisedLastFaction(), as it serves no
purpose in the current logic and only clutters the code. If this faction
information is actually needed by HostUtils.ConvertObserverToPlayer, pass
requestedFaction as a parameter to that function call instead.
lua/ui/lobby/lobby_modules/optionsdialog.lua (1)

313-334: 💤 Low value

Unnecessary if true then wrapper.

This block always executes. If it was for feature-flagging during development, consider removing the wrapper now.

♻️ Suggested removal
-    if true then
-        --snowflakes count
-        local currentSnowFlakesCount = Prefs.GetFromCurrentProfile('SnowFlakesCount') or 100
-        ...
-    end
+    --snowflakes count
+    local currentSnowFlakesCount = Prefs.GetFromCurrentProfile('SnowFlakesCount') or 100
+    ...
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lua/ui/lobby/lobby_modules/optionsdialog.lua` around lines 313 - 334, Remove
the unnecessary `if true then` wrapper and its corresponding `end` statement
from the snowflakes option block. The condition always evaluates to true and
serves no purpose. Keep all the content inside intact, including the
`currentSnowFlakesCount` variable initialization, the
`slider_SnowFlakes_Count_TEXT` text creation, the `slider_SnowFlakes_Count`
slider initialization, and the `OnValueChanged` callback function. Simply
unindent the code and remove both the opening and closing conditional
statements.
lua/ui/lobby/lobby_modules/hostutils.lua (1)

497-510: 💤 Low value

Consider using LobbyComm.maxPlayerSlots instead of magic number 16.

The hardcoded MAXSLOT = 16 duplicates the constant already available via the injected LobbyComm.maxPlayerSlots. Using the injected constant ensures consistency if the value ever changes.

♻️ Suggested change
 function SendArmySettingsToServer()
     local armyIdx = 1
-    local MAXSLOT = 16
-    for slotNum = 1, MAXSLOT do
+    for slotNum = 1, LobbyComm.maxPlayerSlots do
         local playerInfo = gameInfo.PlayerOptions[slotNum]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lua/ui/lobby/lobby_modules/hostutils.lua` around lines 497 - 510, The
SendArmySettingsToServer function uses a hardcoded magic number MAXSLOT with
value 16 instead of leveraging the injected LobbyComm.maxPlayerSlots constant.
Replace the local MAXSLOT variable declaration with a direct reference to
LobbyComm.maxPlayerSlots in the for loop condition to ensure consistency and
eliminate the duplicate constant definition.
lua/ui/lobby/lobby_modules/createui.lua (1)

1789-1791: 💤 Low value

Dead code: snowflake feature is disabled.

The if false then block will never execute. If this is intentional (seasonal/debug feature), consider adding a comment explaining when it should be enabled, or remove it entirely.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lua/ui/lobby/lobby_modules/createui.lua` around lines 1789 - 1791, The if
false then block containing the CreateSnowFlakes(GUI) function call is dead code
that will never execute. Either remove this entire block entirely if the
snowflake feature is no longer needed, or if this is an intentional seasonal or
debug feature that should be enabled at certain times, replace the false
condition with a meaningful comment explaining when and how this feature should
be activated (such as a configuration flag, seasonal check, or debug mode
variable).
lua/ui/lobby/lobby_modules/cpubenchmark.lua (2)

93-101: ⚡ Quick win

Add nil guard for slot structure access.

The CPUText closure (line 95) accesses GUI.slots[slotNumber].CPUSpeedBar.CPUActualValue without checking if GUI.slots[slotNumber] exists. While the tooltip is initially attached during slot creation (per createui.lua:305-324), the closure may be invoked later if the slot structure changes. Consider adding a nil guard.

🛡️ Suggested nil check
     local CPUText = function()
         local CPUInfo
-        if GUI.slots[slotNumber].CPUSpeedBar.CPUActualValue then
+        if GUI.slots[slotNumber] and GUI.slots[slotNumber].CPUSpeedBar and GUI.slots[slotNumber].CPUSpeedBar.CPUActualValue then
             CPUInfo = GUI.slots[slotNumber].CPUSpeedBar.CPUActualValue
         else
             CPUInfo = LOC('<LOC lobui_0458>UnKnown')
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lua/ui/lobby/lobby_modules/cpubenchmark.lua` around lines 93 - 101, The
CPUText closure accesses GUI.slots[slotNumber].CPUSpeedBar.CPUActualValue
without verifying that GUI.slots[slotNumber] exists, which could cause errors if
the slot structure is modified after initial creation. Add a nil guard to check
if GUI.slots[slotNumber] exists before attempting to access the nested
CPUSpeedBar and CPUActualValue properties; if the slot is nil, return the
fallback LOC('<LOC lobui_0458>UnKnown') message to ensure the function remains
safe.

149-152: ⚡ Quick win

Add nil guards for GUI.rerunBenchmark structure.

Lines 149 and 151 access GUI.rerunBenchmark and .label without checking existence. While these controls should be initialized during UI setup, adding guards makes the module more resilient to partial initialization or UI lifecycle changes.

🛡️ Suggested nil checks
 function StressCPU(waitTime)
-    GUI.rerunBenchmark:Disable()
+    if not GUI.rerunBenchmark then
+        WARN('StressCPU: GUI.rerunBenchmark not initialized')
+        return nil
+    end
+    GUI.rerunBenchmark:Disable()
     for i = waitTime, 1, -1 do
-        GUI.rerunBenchmark.label:SetText(i..'s')
+        if GUI.rerunBenchmark.label then
+            GUI.rerunBenchmark.label:SetText(i..'s')
+        end
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lua/ui/lobby/lobby_modules/cpubenchmark.lua` around lines 149 - 152, Add nil
guards before accessing GUI.rerunBenchmark and its label property. Before the
Disable() call on GUI.rerunBenchmark at line 149 and before accessing
GUI.rerunBenchmark.label for the SetText() call at line 151, add checks to
verify that GUI.rerunBenchmark exists and that GUI.rerunBenchmark.label exists.
This makes the code more resilient to cases where these UI elements may not be
fully initialized or available during the UI lifecycle.
lua/ui/lobby/lobby_modules/chathandler.lua (1)

58-60: ⚡ Quick win

Add nil guards for the nested property access chain.

The expression GetLocalPlayerData():AsTable().Faction chains multiple calls without nil checks. If any intermediate value (GetLocalPlayerData(), :AsTable(), or .Faction) is nil, this will crash. Consider guarding each step or using a safer accessor pattern.

🛡️ Suggested defensive check
             if not chatPlayerColor then
                 nameFont = UIUtil.bodyFont
                 if Prefs.GetOption('faction_font_color') then
-                    nameColor = import("/lua/skins/skins.lua").skins[ FACTION_NAMES[GetLocalPlayerData():AsTable().Faction] ].fontColor
+                    local localData = GetLocalPlayerData()
+                    if localData then
+                        local dataTable = localData:AsTable()
+                        if dataTable and dataTable.Faction and FACTION_NAMES[dataTable.Faction] then
+                            nameColor = import("/lua/skins/skins.lua").skins[ FACTION_NAMES[dataTable.Faction] ].fontColor
+                        end
+                    end
                     textColor = nameColor
                 else
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lua/ui/lobby/lobby_modules/chathandler.lua` around lines 58 - 60, The nested
property access chain in the line assigning nameColor from skins lacks nil
guards and will crash if GetLocalPlayerData() returns nil, if AsTable() returns
nil, or if the Faction property is nil. Add defensive nil checks for each step
of the chain: first verify GetLocalPlayerData() is not nil, then verify the
result of AsTable() is not nil, and finally verify that the Faction property
exists and is not nil before attempting to access the skins array and retrieve
the fontColor property. Only assign nameColor and textColor if all intermediate
values are valid.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lua/ui/lobby/lobby_modules/aiutils.lua`:
- Around line 136-179: The generic for loop iterating over the aitypes table in
the GetAIPlayerData function is missing the required iterator function. Change
the loop statement from `for k, entry in aitypes do` to `for k, entry in
pairs(aitypes) do` to properly iterate over the table with both key and value
pairs.

In `@lua/ui/lobby/lobby_modules/autobalance.lua`:
- Around line 463-470: The repeat-until loop starting with ranNum =
math.random(1, table.getn(factionNames)) can infinitely loop if all faction
names are exhausted (all slots in nameSlotsTaken[playerFaction] are populated).
Add a safeguard by either checking if available slots remain before entering the
loop, or adding a maximum iteration counter to break out gracefully if no unused
names are available. Reference the condition
nameSlotsTaken[playerFaction][ranNum] == nil to determine when an available slot
exists.

In `@lua/ui/lobby/lobby_modules/createui.lua`:
- Around line 1483-1484: The inequality operator `!=` is used on line 1483 in
the conditional expression checking manual teams, but standard Lua syntax
requires `~=` for inequality comparisons. Replace both occurrences of `!=` in
the manualTeams condition (comparing numPlayersTeam1 != teamSize and
numPlayersTeam2 != teamSize) with the correct `~=` operator to match Lua syntax
standards.

In `@lua/ui/lobby/lobby_modules/hostutils.lua`:
- Around line 569-575: The function IsModAvailable is being called within the
UpdateMods() function in hostutils.lua but is not available in the scope of that
file since it's defined only in lobby.lua. To fix this, you need to inject
IsModAvailable as a dependency from lobby.lua into hostutils.lua. Add
IsModAvailable to the _syncHostUtils() function call in lobby.lua to pass it as
an injected function, then ensure it is declared in the injected functions list
at the top of hostutils.lua and properly initialized in the appropriate Init
function so that UpdateMods() can access and call IsModAvailable when checking
if mods are available.

In `@lua/ui/lobby/lobby_modules/launchlogic.lua`:
- Line 502: The assignment of gameInfo.AdaptiveMap from scenarioInfo.AdaptiveMap
on line 502 will fail if scenarioInfo is nil, which occurs when the conditional
block defining scenarioInfo (based on gameInfo.GameOptions.ScenarioFile) is not
executed. Add a nil check before this assignment to verify that scenarioInfo
exists before attempting to access scenarioInfo.AdaptiveMap. Only assign
gameInfo.AdaptiveMap if scenarioInfo is not nil.

In `@lua/ui/lobby/lobby_modules/maputils.lua`:
- Around line 186-214: In the manual auto-teams mode block where AutoTeams ==
'manual', there is a nil access risk at lines 209-211. The condition on line 189
can be satisfied even when gameInfo.PlayerOptions[slot] is nil (for open
unoccupied slots) because of the or clause checking TeamSpawn != 'fixed'. Add a
guard to ensure gameInfo.PlayerOptions[slot] is not nil before attempting to
access gameInfo.PlayerOptions[slot]['Team'] and calling SetSlotInfo, otherwise
the code will error when clicking an empty slot with TeamSpawn set to a
non-fixed value.

In `@lua/ui/lobby/lobby_modules/messagehandlers.lua`:
- Around line 304-306: The function FindRehostSlotForID is currently being
called as a global function in the messagehandlers.lua file, but for consistency
with the module's dependency injection pattern, it should be explicitly injected
like the similar functions FindSlotForID and FindObserverSlotForID. Add
FindRehostSlotForID to the dependencies table that is passed to
MessageHandlers.Init(), then update the call to FindRehostSlotForID in
messagehandlers.lua to use the injected dependency instead of the global
function reference.
- Around line 387-391: The SystemMessage handler is calling PrintSystemMessage
directly as a global function instead of using dependency injection. Update the
module's InitB function to accept PrintSystemMessage as a parameter (similar to
how SendSystemMessage, SendPersonalSystemMessage, and AddChatText are injected),
and then modify the Handle function within the SystemMessage table to use the
injected PrintSystemMessage instead of accessing it globally.
- Around line 374-377: The `availableMods` variable is used in the `Handle`
function within the `SetAvailableMods` message handler but is never assigned
from the dependencies object passed to the module during initialization. To fix
this, add an assignment statement `availableMods = deps.availableMods` in either
the `InitA` or `InitB` function so that the variable is properly captured from
the dependencies before it is accessed in the handler function.

In `@lua/ui/lobby/lobby_modules/optionsdialog.lua`:
- Around line 96-101: In the elseif block that handles the case where modNum
equals 0 and modNumUI is greater than 0, the singular form check on line 98 is
using the wrong variable. Change the condition from `modNum == 1` to `modNumUI
== 1` so that the singular "UI Mod" text is correctly used when there is exactly
one UI mod instead of checking a variable that is guaranteed to be zero in this
branch.

In `@lua/ui/lobby/lobby_modules/slotmenu.lua`:
- Line 104: The generic for loop iterating over menuData[stateKey][hostKey] is
missing an iterator function. Since this table appears to be a sequential array
of action keys, wrap the table reference with the ipairs() iterator function to
properly iterate over the indexed values. Change the for loop to use
ipairs(menuData[stateKey][hostKey]) instead of iterating directly over the table
without an iterator.

---

Nitpick comments:
In `@lua/ui/lobby/lobby_modules/chathandler.lua`:
- Around line 58-60: The nested property access chain in the line assigning
nameColor from skins lacks nil guards and will crash if GetLocalPlayerData()
returns nil, if AsTable() returns nil, or if the Faction property is nil. Add
defensive nil checks for each step of the chain: first verify
GetLocalPlayerData() is not nil, then verify the result of AsTable() is not nil,
and finally verify that the Faction property exists and is not nil before
attempting to access the skins array and retrieve the fontColor property. Only
assign nameColor and textColor if all intermediate values are valid.

In `@lua/ui/lobby/lobby_modules/cpubenchmark.lua`:
- Around line 93-101: The CPUText closure accesses
GUI.slots[slotNumber].CPUSpeedBar.CPUActualValue without verifying that
GUI.slots[slotNumber] exists, which could cause errors if the slot structure is
modified after initial creation. Add a nil guard to check if
GUI.slots[slotNumber] exists before attempting to access the nested CPUSpeedBar
and CPUActualValue properties; if the slot is nil, return the fallback LOC('<LOC
lobui_0458>UnKnown') message to ensure the function remains safe.
- Around line 149-152: Add nil guards before accessing GUI.rerunBenchmark and
its label property. Before the Disable() call on GUI.rerunBenchmark at line 149
and before accessing GUI.rerunBenchmark.label for the SetText() call at line
151, add checks to verify that GUI.rerunBenchmark exists and that
GUI.rerunBenchmark.label exists. This makes the code more resilient to cases
where these UI elements may not be fully initialized or available during the UI
lifecycle.

In `@lua/ui/lobby/lobby_modules/createui.lua`:
- Around line 1789-1791: The if false then block containing the
CreateSnowFlakes(GUI) function call is dead code that will never execute. Either
remove this entire block entirely if the snowflake feature is no longer needed,
or if this is an intentional seasonal or debug feature that should be enabled at
certain times, replace the false condition with a meaningful comment explaining
when and how this feature should be activated (such as a configuration flag,
seasonal check, or debug mode variable).

In `@lua/ui/lobby/lobby_modules/hostutils.lua`:
- Around line 497-510: The SendArmySettingsToServer function uses a hardcoded
magic number MAXSLOT with value 16 instead of leveraging the injected
LobbyComm.maxPlayerSlots constant. Replace the local MAXSLOT variable
declaration with a direct reference to LobbyComm.maxPlayerSlots in the for loop
condition to ensure consistency and eliminate the duplicate constant definition.

In `@lua/ui/lobby/lobby_modules/maputils.lua`:
- Around line 160-174: The variable requestedFaction is assigned the result of
GetSanitisedLastFaction() but is never used in the subsequent code. Remove the
assignment of requestedFaction on the line where it gets assigned from
GetSanitisedLastFaction(), as it serves no purpose in the current logic and only
clutters the code. If this faction information is actually needed by
HostUtils.ConvertObserverToPlayer, pass requestedFaction as a parameter to that
function call instead.

In `@lua/ui/lobby/lobby_modules/optionsdialog.lua`:
- Around line 313-334: Remove the unnecessary `if true then` wrapper and its
corresponding `end` statement from the snowflakes option block. The condition
always evaluates to true and serves no purpose. Keep all the content inside
intact, including the `currentSnowFlakesCount` variable initialization, the
`slider_SnowFlakes_Count_TEXT` text creation, the `slider_SnowFlakes_Count`
slider initialization, and the `OnValueChanged` callback function. Simply
unindent the code and remove both the opening and closing conditional
statements.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dafdcfac-0dc6-4088-8637-7b977898e921

📥 Commits

Reviewing files that changed from the base of the PR and between c35a745 and 744f150.

📒 Files selected for processing (13)
  • lua/ui/lobby/lobby.lua
  • lua/ui/lobby/lobby_modules/aiutils.lua
  • lua/ui/lobby/lobby_modules/autobalance.lua
  • lua/ui/lobby/lobby_modules/chathandler.lua
  • lua/ui/lobby/lobby_modules/cpubenchmark.lua
  • lua/ui/lobby/lobby_modules/createui.lua
  • lua/ui/lobby/lobby_modules/hostutils.lua
  • lua/ui/lobby/lobby_modules/launchlogic.lua
  • lua/ui/lobby/lobby_modules/maputils.lua
  • lua/ui/lobby/lobby_modules/messagehandlers.lua
  • lua/ui/lobby/lobby_modules/optionsdialog.lua
  • lua/ui/lobby/lobby_modules/pingutils.lua
  • lua/ui/lobby/lobby_modules/slotmenu.lua

Comment on lines +136 to +179
function GetAIPlayerData(name, AIPersonality, slot)
local AIColor
-- gets the color of the player/AI occupying the slot directly prior if available
for i = 1, LobbyComm.maxPlayerSlots do
if gameInfo.PlayerOptions[i].StartSpot == slot then
if IsColorFree(gameInfo.PlayerOptions[i].PlayerColor, slot) then
AIColor = gameInfo.PlayerOptions[i].PlayerColor
end
break
end
end
if not AIColor then
AIColor = GetAvailableColor()
end

-- retrieve properties from AI table
---@type AILobbyProperties | nil
local aiLobbyProperties = nil
for k, entry in aitypes do
if entry.key == AIPersonality then
aiLobbyProperties = entry
end
end
local iRating = ComputeAIRating(gameInfo.GameOptions, aiLobbyProperties)

return PlayerData(
{
OwnerID = hostID,
PlayerName = name,
Ready = true,
Human = false,
AIPersonality = AIPersonality,
PlayerColor = AIColor,
ArmyColor = AIColor,

PL = iRating,
MEAN = iRating,
DEV = 0,

-- keep track of the AI lobby properties for easier access
AILobbyProperties = aiLobbyProperties,
}
)
end

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 | 🔴 Critical | ⚡ Quick win

Missing pairs() iterator in generic for loop.

Line 154 uses for k, entry in aitypes without an iterator function. This should use pairs(aitypes).

🐛 Proposed fix
     ---@type AILobbyProperties | nil
     local aiLobbyProperties = nil
-    for k, entry in aitypes do
+    for k, entry in pairs(aitypes) do
         if entry.key == AIPersonality then
             aiLobbyProperties = entry
         end
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function GetAIPlayerData(name, AIPersonality, slot)
local AIColor
-- gets the color of the player/AI occupying the slot directly prior if available
for i = 1, LobbyComm.maxPlayerSlots do
if gameInfo.PlayerOptions[i].StartSpot == slot then
if IsColorFree(gameInfo.PlayerOptions[i].PlayerColor, slot) then
AIColor = gameInfo.PlayerOptions[i].PlayerColor
end
break
end
end
if not AIColor then
AIColor = GetAvailableColor()
end
-- retrieve properties from AI table
---@type AILobbyProperties | nil
local aiLobbyProperties = nil
for k, entry in aitypes do
if entry.key == AIPersonality then
aiLobbyProperties = entry
end
end
local iRating = ComputeAIRating(gameInfo.GameOptions, aiLobbyProperties)
return PlayerData(
{
OwnerID = hostID,
PlayerName = name,
Ready = true,
Human = false,
AIPersonality = AIPersonality,
PlayerColor = AIColor,
ArmyColor = AIColor,
PL = iRating,
MEAN = iRating,
DEV = 0,
-- keep track of the AI lobby properties for easier access
AILobbyProperties = aiLobbyProperties,
}
)
end
function GetAIPlayerData(name, AIPersonality, slot)
local AIColor
-- gets the color of the player/AI occupying the slot directly prior if available
for i = 1, LobbyComm.maxPlayerSlots do
if gameInfo.PlayerOptions[i].StartSpot == slot then
if IsColorFree(gameInfo.PlayerOptions[i].PlayerColor, slot) then
AIColor = gameInfo.PlayerOptions[i].PlayerColor
end
break
end
end
if not AIColor then
AIColor = GetAvailableColor()
end
-- retrieve properties from AI table
---@type AILobbyProperties | nil
local aiLobbyProperties = nil
for k, entry in pairs(aitypes) do
if entry.key == AIPersonality then
aiLobbyProperties = entry
end
end
local iRating = ComputeAIRating(gameInfo.GameOptions, aiLobbyProperties)
return PlayerData(
{
OwnerID = hostID,
PlayerName = name,
Ready = true,
Human = false,
AIPersonality = AIPersonality,
PlayerColor = AIColor,
ArmyColor = AIColor,
PL = iRating,
MEAN = iRating,
DEV = 0,
-- keep track of the AI lobby properties for easier access
AILobbyProperties = aiLobbyProperties,
}
)
end
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lua/ui/lobby/lobby_modules/aiutils.lua` around lines 136 - 179, The generic
for loop iterating over the aitypes table in the GetAIPlayerData function is
missing the required iterator function. Change the loop statement from `for k,
entry in aitypes do` to `for k, entry in pairs(aitypes) do` to properly iterate
over the table with both key and value pairs.

Comment on lines +463 to +470
local ranNum
repeat
ranNum = math.random(1, table.getn(factionNames))
until nameSlotsTaken[playerFaction][ranNum] == nil
nameSlotsTaken[playerFaction][ranNum] = true
player.PlayerName = factionNames[ranNum] .. " (" .. player.PlayerName .. ")"
end
end

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 | 🟡 Minor | ⚡ Quick win

Potential infinite loop if all faction names are exhausted.

If the number of AI players of a single faction exceeds the number of available names for that faction, the repeat-until loop will never terminate.

Consider adding a safeguard:

🛡️ Proposed defensive fix
 for index, player in gameInfo.PlayerOptions do
     if not player.Human then
         local playerFaction = player.Faction
         local factionNames = aiNames[FactionData.Factions[playerFaction].Key]
         local ranNum
+        local attempts = 0
+        local maxAttempts = table.getn(factionNames) * 2
         repeat
             ranNum = math.random(1, table.getn(factionNames))
-        until nameSlotsTaken[playerFaction][ranNum] == nil
+            attempts = attempts + 1
+        until nameSlotsTaken[playerFaction][ranNum] == nil or attempts >= maxAttempts
         nameSlotsTaken[playerFaction][ranNum] = true
         player.PlayerName = factionNames[ranNum] .. " (" .. player.PlayerName .. ")"
     end
 end
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
local ranNum
repeat
ranNum = math.random(1, table.getn(factionNames))
until nameSlotsTaken[playerFaction][ranNum] == nil
nameSlotsTaken[playerFaction][ranNum] = true
player.PlayerName = factionNames[ranNum] .. " (" .. player.PlayerName .. ")"
end
end
local ranNum
local attempts = 0
local maxAttempts = table.getn(factionNames) * 2
repeat
ranNum = math.random(1, table.getn(factionNames))
attempts = attempts + 1
until nameSlotsTaken[playerFaction][ranNum] == nil or attempts >= maxAttempts
nameSlotsTaken[playerFaction][ranNum] = true
player.PlayerName = factionNames[ranNum] .. " (" .. player.PlayerName .. ")"
end
end
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lua/ui/lobby/lobby_modules/autobalance.lua` around lines 463 - 470, The
repeat-until loop starting with ranNum = math.random(1,
table.getn(factionNames)) can infinitely loop if all faction names are exhausted
(all slots in nameSlotsTaken[playerFaction] are populated). Add a safeguard by
either checking if available slots remain before entering the loop, or adding a
maximum iteration counter to break out gracefully if no unused names are
available. Reference the condition nameSlotsTaken[playerFaction][ranNum] == nil
to determine when an available slot exists.

Comment on lines +1483 to +1484
if (manualTeams and (numPlayersTeam1 != teamSize or numPlayersTeam2 != teamSize))
or (not manualTeams and (table.getn(sortedTeam1Slots) < teamSize or table.getn(sortedTeam2Slots) < teamSize)) then

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 | 🔴 Critical | ⚡ Quick win

Same != operator issue as in messagehandlers.lua.

Line 1483 uses != which Luacheck flags as invalid Lua syntax. This is the same issue as flagged in messagehandlers.lua. Standard Lua uses ~= for inequality. If SupCom Lua supports != as an extension, this is fine; otherwise both conditions on line 1483 need to use ~=.

🐛 Proposed fix (if != is not supported)
-            if (manualTeams and (numPlayersTeam1 != teamSize or numPlayersTeam2 != teamSize))
-             or (not manualTeams and (table.getn(sortedTeam1Slots) < teamSize or table.getn(sortedTeam2Slots) < teamSize)) then
+            if (manualTeams and (numPlayersTeam1 ~= teamSize or numPlayersTeam2 ~= teamSize))
+             or (not manualTeams and (table.getn(sortedTeam1Slots) < teamSize or table.getn(sortedTeam2Slots) < teamSize)) then
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (manualTeams and (numPlayersTeam1 != teamSize or numPlayersTeam2 != teamSize))
or (not manualTeams and (table.getn(sortedTeam1Slots) < teamSize or table.getn(sortedTeam2Slots) < teamSize)) then
if (manualTeams and (numPlayersTeam1 ~= teamSize or numPlayersTeam2 ~= teamSize))
or (not manualTeams and (table.getn(sortedTeam1Slots) < teamSize or table.getn(sortedTeam2Slots) < teamSize)) then
🧰 Tools
🪛 Luacheck (1.2.0)

[error] 1483-1483: expected ')' near '!'

(E011)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lua/ui/lobby/lobby_modules/createui.lua` around lines 1483 - 1484, The
inequality operator `!=` is used on line 1483 in the conditional expression
checking manual teams, but standard Lua syntax requires `~=` for inequality
comparisons. Replace both occurrences of `!=` in the manualTeams condition
(comparing numPlayersTeam1 != teamSize and numPlayersTeam2 != teamSize) with the
correct `~=` operator to match Lua syntax standards.

Comment on lines +569 to +575
for modId, _ in selectedSimMods do
if IsModAvailable(modId) then
newmods[modId] = true
else
table.insert(missingmods, modId)
end
end

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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for IsModAvailable definition to understand where it comes from
ast-grep --pattern 'function IsModAvailable($_) { $$$ }'
rg -n 'IsModAvailable\s*=' --type=lua
rg -n 'function\s+IsModAvailable' --type=lua

Repository: FAForever/fa

Length of output: 120


🏁 Script executed:

# Check how hostutils.lua is structured and what's injected
head -100 lua/ui/lobby/lobby_modules/hostutils.lua

Repository: FAForever/fa

Length of output: 3568


🏁 Script executed:

# Find where hostutils.lua is imported/initialized
rg -n 'hostutils' --type=lua | head -20

Repository: FAForever/fa

Length of output: 239


🏁 Script executed:

# Check the InitA and InitB structure mentioned in the scratchpad
rg -n 'InitA\|InitB' lua/ui/lobby/lobby_modules/hostutils.lua | head -20

Repository: FAForever/fa

Length of output: 38


🏁 Script executed:

# Get the full InitB function and any module exports
sed -n '55,150p' lua/ui/lobby/lobby_modules/hostutils.lua

Repository: FAForever/fa

Length of output: 3769


🏁 Script executed:

# Look at the UpdateMods function where IsModAvailable is called
sed -n '560,580p' lua/ui/lobby/lobby_modules/hostutils.lua

Repository: FAForever/fa

Length of output: 704


🏁 Script executed:

# Check how lobby.lua initializes HostUtils with dependencies
rg -n 'HostUtils\.Init\|InitHostUtils' lua/ui/lobby/lobby.lua -A 10

Repository: FAForever/fa

Length of output: 38


🏁 Script executed:

# Search for where HostUtils Init is called in lobby.lua
rg -n 'HostUtils_Module\.Init\|HostUtils\.Init' lua/ui/lobby/lobby.lua -A 5

Repository: FAForever/fa

Length of output: 38


🏁 Script executed:

# Check if IsModAvailable is passed in the Init call or if it's a global function
rg -n 'IsModAvailable' lua/ui/lobby/lobby.lua -B 2 -A 2

Repository: FAForever/fa

Length of output: 179


🏁 Script executed:

# Search more carefully for how HostUtils is initialized
rg -n 'HostUtils' lua/ui/lobby/lobby.lua -B 2 -A 5

Repository: FAForever/fa

Length of output: 10877


🏁 Script executed:

# Check if IsModAvailable might be injected differently or if there's another definition
rg -n 'IsModAvailable' lua/ui/lobby/ --type=lua -l

Repository: FAForever/fa

Length of output: 120


🏁 Script executed:

# Look at the complete UpdateMods function to understand its full scope
sed -n '540,600p' lua/ui/lobby/lobby_modules/hostutils.lua

Repository: FAForever/fa

Length of output: 2157


🏁 Script executed:

# Check the complete SyncModuleDeps function to see all dependencies passed to HostUtils_Module.Init
rg -n 'SyncModuleDeps' lua/ui/lobby/lobby.lua -A 100 | head -150

Repository: FAForever/fa

Length of output: 7034


🏁 Script executed:

# Look for InitHostUtils and check what happens there
sed -n '2661,2670p' lua/ui/lobby/lobby.lua

Repository: FAForever/fa

Length of output: 347


🏁 Script executed:

# Get the full SyncModuleDeps function definition
sed -n '1135,1250p' lua/ui/lobby/lobby.lua

Repository: FAForever/fa

Length of output: 5363


🏁 Script executed:

# Also check if there's any global access to IsModAvailable or if it's somehow accessible through upvalues
grep -n 'IsModAvailable' lua/ui/lobby/lobby_modules/hostutils.lua

Repository: FAForever/fa

Length of output: 98


🏁 Script executed:

# Find the _syncHostUtils function
rg -n '_syncHostUtils' lua/ui/lobby/lobby.lua -A 50

Repository: FAForever/fa

Length of output: 5412


IsModAvailable function must be injected into hostutils.lua.

The function IsModAvailable(modId) is called on line 570 in UpdateMods(), but it is not included in the dependencies injected via _syncHostUtils() in lobby.lua. Since IsModAvailable is defined locally in lobby.lua (line 364), it is not accessible to hostutils.lua. This will cause a runtime error when UpdateMods() is called on the host.

Add IsModAvailable = IsModAvailable to the _syncHostUtils() initialization in lobby.lua (around line 1449) and to both the injected functions list (top of hostutils.lua) and the appropriate Init function.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lua/ui/lobby/lobby_modules/hostutils.lua` around lines 569 - 575, The
function IsModAvailable is being called within the UpdateMods() function in
hostutils.lua but is not available in the scope of that file since it's defined
only in lobby.lua. To fix this, you need to inject IsModAvailable as a
dependency from lobby.lua into hostutils.lua. Add IsModAvailable to the
_syncHostUtils() function call in lobby.lua to pass it as an injected function,
then ensure it is declared in the injected functions list at the top of
hostutils.lua and properly initialized in the appropriate Init function so that
UpdateMods() can access and call IsModAvailable when checking if mods are
available.

UIUtil.setEnabled(GUI.factionSelector, false)
end

gameInfo.AdaptiveMap = scenarioInfo.AdaptiveMap

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 | ⚡ Quick win

Potential nil dereference: scenarioInfo may be undefined.

Line 502 accesses scenarioInfo.AdaptiveMap outside the conditional block (lines 354-477) that assigns scenarioInfo. If gameInfo.GameOptions.ScenarioFile is nil or empty, scenarioInfo remains nil and this line will error.

🐛 Proposed fix
-    gameInfo.AdaptiveMap = scenarioInfo.AdaptiveMap
+    gameInfo.AdaptiveMap = scenarioInfo and scenarioInfo.AdaptiveMap
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
gameInfo.AdaptiveMap = scenarioInfo.AdaptiveMap
gameInfo.AdaptiveMap = scenarioInfo and scenarioInfo.AdaptiveMap
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lua/ui/lobby/lobby_modules/launchlogic.lua` at line 502, The assignment of
gameInfo.AdaptiveMap from scenarioInfo.AdaptiveMap on line 502 will fail if
scenarioInfo is nil, which occurs when the conditional block defining
scenarioInfo (based on gameInfo.GameOptions.ScenarioFile) is not executed. Add a
nil check before this assignment to verify that scenarioInfo exists before
attempting to access scenarioInfo.AdaptiveMap. Only assign gameInfo.AdaptiveMap
if scenarioInfo is not nil.

Comment on lines +304 to +306
if argv.isRehost then
local rehostSlot = FindRehostSlotForID(data.SenderID) or 0
if rehostSlot ~= 0 and gameInfo.PlayerOptions[rehostSlot] then

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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for FindRehostSlotForID definition
rg -n 'FindRehostSlotForID' --type=lua -C2

Repository: FAForever/fa

Length of output: 1180


🏁 Script executed:

# Check the context around InitA/InitB in messagehandlers.lua
rg -n 'InitA|InitB' lua/ui/lobby/lobby_modules/messagehandlers.lua -A 5 -B 2

Repository: FAForever/fa

Length of output: 1085


🏁 Script executed:

# Check the top of messagehandlers.lua for dependency setup
head -100 lua/ui/lobby/lobby_modules/messagehandlers.lua

Repository: FAForever/fa

Length of output: 3521


🏁 Script executed:

# Check if FindRehostSlotForID is defined as a global function in lobby.lua
rg -n 'function FindRehostSlotForID' lua/ui/lobby/lobby.lua -B 5 -A 10

Repository: FAForever/fa

Length of output: 435


🏁 Script executed:

# Search for where messagehandlers is required/loaded
rg -n 'messagehandlers|MessageHandlers' lua/ui/lobby/lobby.lua | head -20

Repository: FAForever/fa

Length of output: 569


🏁 Script executed:

# Check if FindRehostSlotForID is referenced in the deps being passed to Init
rg -n 'FindRehostSlotForID' lua/ui/lobby/lobby.lua

Repository: FAForever/fa

Length of output: 163


🏁 Script executed:

# Check what deps are passed to MessageHandlers.Init in _syncMessageHandlers
rg -n '_syncMessageHandlers' lua/ui/lobby/lobby.lua -A 50 | head -80

Repository: FAForever/fa

Length of output: 4092


Remove injection claim; this is a pattern inconsistency, not a runtime error.

FindRehostSlotForID is defined as a global function in lobby.lua (line 947), so it will be accessible from messagehandlers.lua without explicit injection. However, this creates an inconsistency: similar functions like FindSlotForID and FindObserverSlotForID are explicitly injected in the dependency table passed to MessageHandlers.Init() at line 1336. For consistency with the module's dependency injection pattern, FindRehostSlotForID should be added to the deps table.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lua/ui/lobby/lobby_modules/messagehandlers.lua` around lines 304 - 306, The
function FindRehostSlotForID is currently being called as a global function in
the messagehandlers.lua file, but for consistency with the module's dependency
injection pattern, it should be explicitly injected like the similar functions
FindSlotForID and FindObserverSlotForID. Add FindRehostSlotForID to the
dependencies table that is passed to MessageHandlers.Init(), then update the
call to FindRehostSlotForID in messagehandlers.lua to use the injected
dependency instead of the global function reference.

Comment on lines +374 to +377
Handle = function(data)
availableMods[data.SenderID] = data.Mods
HostUtils.UpdateMods(data.SenderID, data.Name)
end

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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify availableMods is a global or check if it should be injected
rg -n 'availableMods\s*=' --type=lua | head -20

Repository: FAForever/fa

Length of output: 503


🏁 Script executed:

cat -n lua/ui/lobby/lobby_modules/messagehandlers.lua | sed -n '360,390p'

Repository: FAForever/fa

Length of output: 1214


🏁 Script executed:

rg -n "local\s+\w+\s*=\s*function\|function\s+\w+\(" lua/ui/lobby/lobby_modules/messagehandlers.lua | grep -A5 -B5 "SetAvailableMods"

Repository: FAForever/fa

Length of output: 38


🏁 Script executed:

rg -n "SetAvailableMods" lua/ui/lobby/lobby_modules/messagehandlers.lua -A 15 -B 5

Repository: FAForever/fa

Length of output: 586


🏁 Script executed:

# Check how dependencies are injected into messagehandlers module
head -100 lua/ui/lobby/lobby_modules/messagehandlers.lua | tail -50

Repository: FAForever/fa

Length of output: 2266


🏁 Script executed:

rg -n "availableMods" lua/ui/lobby/lobby_modules/messagehandlers.lua

Repository: FAForever/fa

Length of output: 113


🏁 Script executed:

# Get the complete InitA and InitB functions
sed -n '1,150p' lua/ui/lobby/lobby_modules/messagehandlers.lua | grep -n "availableMods\|function Init"

Repository: FAForever/fa

Length of output: 140


🏁 Script executed:

# Check the full extent of InitA and InitB
awk '/^local function InitA/,/^end/ {print NR": "$0}' lua/ui/lobby/lobby_modules/messagehandlers.lua

Repository: FAForever/fa

Length of output: 1129


🏁 Script executed:

sed -n '82,115p' lua/ui/lobby/lobby_modules/messagehandlers.lua

Repository: FAForever/fa

Length of output: 1547


🏁 Script executed:

# Check how messagehandlers is initialized in lobby.lua
rg -n "messagehandlers" lua/ui/lobby/lobby.lua | head -20

Repository: FAForever/fa

Length of output: 141


🏁 Script executed:

# Find where MessageHandlers.Init is called
rg -n "MessageHandlers\.Init" lua/ui/lobby/lobby.lua -A 30 -B 5

Repository: FAForever/fa

Length of output: 1976


🏁 Script executed:

sed -n '1336,1430p' lua/ui/lobby/lobby.lua | grep -n "availableMods"

Repository: FAForever/fa

Length of output: 119


🏁 Script executed:

# Get complete _syncMessageHandlers function
sed -n '1335,1450p' lua/ui/lobby/lobby.lua

Repository: FAForever/fa

Length of output: 6130


availableMods is not assigned from dependencies in messagehandlers.lua.

The variable availableMods is passed in the dependency object when MessageHandlers.Init is called from lobby.lua:1414, but the messagehandlers module does not assign it in either the InitA or InitB functions. This means line 375 in the SetAvailableMods handler will attempt to access an undefined availableMods, resulting in a nil access error.

Add availableMods = deps.availableMods to either the InitA or InitB function in messagehandlers.lua to resolve this.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lua/ui/lobby/lobby_modules/messagehandlers.lua` around lines 374 - 377, The
`availableMods` variable is used in the `Handle` function within the
`SetAvailableMods` message handler but is never assigned from the dependencies
object passed to the module during initialization. To fix this, add an
assignment statement `availableMods = deps.availableMods` in either the `InitA`
or `InitB` function so that the variable is properly captured from the
dependencies before it is accessed in the handler function.

Comment on lines +387 to +391
SystemMessage = {
Handle = function(data)
PrintSystemMessage(data.Id, data.Args)
end
},

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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for PrintSystemMessage definition
rg -n 'function\s+PrintSystemMessage' --type=lua
rg -n 'PrintSystemMessage\s*=' --type=lua

Repository: FAForever/fa

Length of output: 128


🏁 Script executed:

cat -n lua/ui/lobby/lobby_modules/messagehandlers.lua | head -50

Repository: FAForever/fa

Length of output: 1661


🏁 Script executed:

cat -n lua/ui/lobby/lobby_modules/messagehandlers.lua | grep -A 10 -B 10 "function InitA\|function InitB"

Repository: FAForever/fa

Length of output: 2106


🏁 Script executed:

cat -n lua/ui/lobby/lobby.lua | sed -n '2010,2030p'

Repository: FAForever/fa

Length of output: 753


🏁 Script executed:

rg -n "PrintSystemMessage" lua/ui/lobby/lobby_modules/messagehandlers.lua -B 5 -A 5

Repository: FAForever/fa

Length of output: 310


🏁 Script executed:

cat -n lua/ui/lobby/lobby_modules/messagehandlers.lua | sed -n '82,120p'

Repository: FAForever/fa

Length of output: 2066


🏁 Script executed:

rg -n "PrintSystemMessage" lua/ui/lobby/lobby.lua -B 2 -A 2

Repository: FAForever/fa

Length of output: 341


🏁 Script executed:

# Check if PrintSystemMessage is accessible as a global or if it needs to be injected
rg -n "local PrintSystemMessage\|_G\[.*PrintSystemMessage\|function PrintSystemMessage" lua/ui/lobby/

Repository: FAForever/fa

Length of output: 38


PrintSystemMessage function is not included in the dependency injection pattern.

The function PrintSystemMessage is called on line 389 but is not injected via InitB deps like other similar functions (SendSystemMessage, SendPersonalSystemMessage, AddChatText). While PrintSystemMessage is globally accessible from lobby.lua, it should be injected for consistency with the module's dependency injection pattern.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lua/ui/lobby/lobby_modules/messagehandlers.lua` around lines 387 - 391, The
SystemMessage handler is calling PrintSystemMessage directly as a global
function instead of using dependency injection. Update the module's InitB
function to accept PrintSystemMessage as a parameter (similar to how
SendSystemMessage, SendPersonalSystemMessage, and AddChatText are injected), and
then modify the Handle function within the SystemMessage table to use the
injected PrintSystemMessage instead of accessing it globally.

Comment on lines +96 to +101
elseif modNum == 0 and modNumUI > 0 then
modStr = modNumUI..' UI Mods'
if modNum == 1 then
modStr = modNumUI..' UI Mod'
end
end

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 | 🟡 Minor | ⚡ Quick win

Bug: Wrong variable checked for singular form.

Line 98 checks modNum == 1 but we're in the modNum == 0 branch, so this condition is always false. The singular "UI Mod" text will never be used.

🐛 Proposed fix
     elseif modNum == 0 and modNumUI > 0 then
         modStr = modNumUI..' UI Mods'
-        if modNum == 1 then
+        if modNumUI == 1 then
             modStr = modNumUI..' UI Mod'
         end
     end
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
elseif modNum == 0 and modNumUI > 0 then
modStr = modNumUI..' UI Mods'
if modNum == 1 then
modStr = modNumUI..' UI Mod'
end
end
elseif modNum == 0 and modNumUI > 0 then
modStr = modNumUI..' UI Mods'
if modNumUI == 1 then
modStr = modNumUI..' UI Mod'
end
end
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lua/ui/lobby/lobby_modules/optionsdialog.lua` around lines 96 - 101, In the
elseif block that handles the case where modNum equals 0 and modNumUI is greater
than 0, the singular form check on line 98 is using the wrong variable. Change
the condition from `modNum == 1` to `modNumUI == 1` so that the singular "UI
Mod" text is correctly used when there is exactly one UI mod instead of checking
a variable that is guaranteed to be zero in this branch.

return nil
end

for _, key in menuData[stateKey][hostKey] do

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 | 🔴 Critical | ⚡ Quick win

Missing pairs() iterator in generic for loop.

Line 104 iterates menuData[stateKey][hostKey] without an iterator function. This should use ipairs() since the table appears to be a sequential array of action keys.

🐛 Proposed fix
-    for _, key in menuData[stateKey][hostKey] do
+    for _, key in ipairs(menuData[stateKey][hostKey]) do
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lua/ui/lobby/lobby_modules/slotmenu.lua` at line 104, The generic for loop
iterating over menuData[stateKey][hostKey] is missing an iterator function.
Since this table appears to be a sequential array of action keys, wrap the table
reference with the ipairs() iterator function to properly iterate over the
indexed values. Change the for loop to use ipairs(menuData[stateKey][hostKey])
instead of iterating directly over the table without an iterator.

@TimMasalme TimMasalme changed the title refactor: extract lobby submodules for better maintanability" refactor: extract lobby submodules for better maintainability" Jun 17, 2026

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lua/ui/lobby/lobby_modules/chathandler.lua`:
- Around line 59-62: The code on line 62 accesses skins[factionKey].fontColor
without first checking if skins[factionKey] exists, which will cause an error if
a faction has no corresponding skin entry (such as modded factions). Add a nil
guard to check that skins[factionKey] exists before accessing its fontColor
property. You can either add the check directly in the assignment statement for
factionKey, or better yet, cache the skin lookup in a local variable (like
skinTable) and use that to safely access fontColor, avoiding redundant import
calls and making the code more readable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0901eeb7-690d-463f-b2b8-25f303bbb6df

📥 Commits

Reviewing files that changed from the base of the PR and between 744f150 and 1665690.

📒 Files selected for processing (5)
  • lua/ui/lobby/lobby_modules/chathandler.lua
  • lua/ui/lobby/lobby_modules/cpubenchmark.lua
  • lua/ui/lobby/lobby_modules/createui.lua
  • lua/ui/lobby/lobby_modules/maputils.lua
  • lua/ui/lobby/lobby_modules/optionsdialog.lua
💤 Files with no reviewable changes (1)
  • lua/ui/lobby/lobby_modules/maputils.lua
🚧 Files skipped from review as they are similar to previous changes (3)
  • lua/ui/lobby/lobby_modules/optionsdialog.lua
  • lua/ui/lobby/lobby_modules/createui.lua
  • lua/ui/lobby/lobby_modules/cpubenchmark.lua

Comment on lines +59 to +62
local localData = GetLocalPlayerData()
local localTable = localData and localData:AsTable()
local factionKey = localTable and FACTION_NAMES[localTable.Faction]
nameColor = factionKey and import("/lua/skins/skins.lua").skins[factionKey].fontColor or nameColor

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate FACTION_NAMES definitions/usages:"
rg -n --type=lua -C2 '\bFACTION_NAMES\b'

echo
echo "Locate skins.lua and inspect available keys/usages:"
fd -i 'skins.lua' | while read -r f; do
  echo "---- $f ----"
  rg -n -C2 'skins\[' "$f"
done

echo
echo "Find direct skin fontColor dereferences (potentially unsafe):"
rg -n --type=lua -C2 'skins\[[^]]+\]\.fontColor'

Repository: FAForever/fa

Length of output: 9328


🏁 Script executed:

# Get file size and read skins.lua to see actual skin key definitions
wc -l lua/skins/skins.lua
echo "---"
head -100 lua/skins/skins.lua

Repository: FAForever/fa

Length of output: 3832


🏁 Script executed:

# Search for where skins table is populated with keys
rg -n --type=lua 'skins\s*=\s*\{' lua/skins/skins.lua -A 50

Repository: FAForever/fa

Length of output: 2596


🏁 Script executed:

# Search for "random" skin entry specifically
rg -n --type=lua 'random' lua/skins/skins.lua

Repository: FAForever/fa

Length of output: 125


🏁 Script executed:

# Read skins.lua from line 130 onwards to see if seraphim and other entries exist
sed -n '130,220p' lua/skins/skins.lua

Repository: FAForever/fa

Length of output: 5311


🏁 Script executed:

# Search specifically for seraphim skin entry
rg -n 'seraphim\s*=' lua/skins/skins.lua -A 5

Repository: FAForever/fa

Length of output: 373


🏁 Script executed:

# Check if uef has fontColor
rg -n 'uef\s*=\s*{' lua/skins/skins.lua -A 20 | grep -i fontcolor

Repository: FAForever/fa

Length of output: 142


🏁 Script executed:

# Check seraphim fontColor
rg -n 'seraphim\s*=\s*{' lua/skins/skins.lua -A 30 | grep -i fontcolor

Repository: FAForever/fa

Length of output: 152


🏁 Script executed:

# Check random fontColor
rg -n 'random\s*=\s*{' lua/skins/skins.lua -A 30 | grep -i fontcolor

Repository: FAForever/fa

Length of output: 148


🏁 Script executed:

# Check how localData.Faction is populated/validated
rg -n 'localData\.Faction\|GetLocalPlayerData' lua/ui/lobby/lobby_modules/chathandler.lua -B 3 -A 3

Repository: FAForever/fa

Length of output: 38


🏁 Script executed:

# Read the uef skin section
sed -n '59,68p' lua/skins/skins.lua | grep -i fontcolor
echo "---"
# Read seraphim section
sed -n '111,132p' lua/skins/skins.lua | grep -i fontcolor
echo "---"
# Read random section
sed -n '133,153p' lua/skins/skins.lua | grep -i fontcolor

Repository: FAForever/fa

Length of output: 96


🏁 Script executed:

# Search how Faction value is obtained and if it's validated before use
rg -n 'Faction' lua/ui/lobby/lobby_modules/chathandler.lua -B 5 -A 2

Repository: FAForever/fa

Length of output: 606


🏁 Script executed:

# Check GetLocalPlayerData to understand what values Faction can have
fd -i 'playerdata' | xargs rg -l 'Faction' 2>/dev/null | head -5

Repository: FAForever/fa

Length of output: 89


🏁 Script executed:

# Check when skins flattening happens and if it's guaranteed to run before chathandler use
rg -n 'assimilate\|default chain' lua/skins/skins.lua -B 3 -A 5

Repository: FAForever/fa

Length of output: 38


🏁 Script executed:

# Verify all faction skins have fontColor after checking default chain logic
# Read full uef entry
sed -n '59,68p' lua/skins/skins.lua
echo "---full seraphim---"
sed -n '111,132p' lua/skins/skins.lua

Repository: FAForever/fa

Length of output: 1578


🏁 Script executed:

# Check if localTable.Faction could be anything other than integer 1-5
rg -n 'Faction' lua/ui/lobby/data/playerdata.lua -B 2 -A 2

Repository: FAForever/fa

Length of output: 382


Add nil guard before dereferencing skins[factionKey].fontColor on line 62.

Line 62 dereferences skins[factionKey].fontColor without checking if skins[factionKey] exists. While all five standard factions (uef, aeon, cybran, seraphim, random) have skin entries in the skins table (with properties inherited via the default chain), the code does not guard against modded factions or Faction values outside the standard set. If a faction lacks a corresponding skin entry, this will error when posting chat messages.

Change:

nameColor = factionKey and import("/lua/skins/skins.lua").skins[factionKey] and import("/lua/skins/skins.lua").skins[factionKey].fontColor or nameColor

Or cache the import and skin lookup to avoid redundant calls:

local skinTable = factionKey and import("/lua/skins/skins.lua").skins[factionKey]
nameColor = skinTable and skinTable.fontColor or nameColor
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lua/ui/lobby/lobby_modules/chathandler.lua` around lines 59 - 62, The code on
line 62 accesses skins[factionKey].fontColor without first checking if
skins[factionKey] exists, which will cause an error if a faction has no
corresponding skin entry (such as modded factions). Add a nil guard to check
that skins[factionKey] exists before accessing its fontColor property. You can
either add the check directly in the assignment statement for factionKey, or
better yet, cache the skin lookup in a local variable (like skinTable) and use
that to safely access fontColor, avoiding redundant import calls and making the
code more readable.

- CreateUI, OptionsDialog, ChatHandler, etc. extracted into separate modules
- Each module uses Init(deps) pattern with rawset injection
- Avoids 32-upvalue limit and strict-global guard
- InitLobbyComm deliberately kept in lobby.lua (see comment)
@speed2CZ

Copy link
Copy Markdown
Member

I'd say this actually makes worse maintainability, everything stayed tightly coupled exactly as it was before, its just spread among more files now, which makes is harder to track and know what affects what.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants