Skip to content

Conversation

@Garanas
Copy link
Member

@Garanas Garanas commented Jan 11, 2026

Description of the proposed changes

Union mode is a game mode where players can switch focus army to issue commands to units that are not theirs. It is originally designed to be used in a co-op or tutorial setting. It enables players to collaborate. It requires a certain level of trust.

The last year or so union mode is being (ab)used as a measurement to reduce the impact of disconnecting players. This is a much broader scope, and as a result there are complaints about players abusing union mode to forcefully share resources, units or worse.

With this pull request we introduce a step between union mode being completely enabled or disabled: union mode is enabled for the players that end up disconnecting from the game. Upon a disconnect the defeat conditions are not applied. Instead, all allied players, defeated or playing, can then switch focus to the abandoned army and issue commands accordingly.

A new lobby option that is in between the regular experience and flat out applying union control to all armies
image

Testing done on the proposed changes

  • When defeated and then abandoned, the control is not shared (army is already defeated).
  • When defeated, but an ally abandons the control is shared with the player.
  • When playing, but an ally abandons the control is shared with the player.
  • When the last player of a team leaves the game the whole team is defeated.
union-control-upon-disconnect-01.mp4

Other details

Checklist

Summary by CodeRabbit

  • New Features
    • Added "Union control upon disconnect" lobby option — disconnected armies remain active and allied players can switch focus to issue commands.
  • Localization
    • Added English and Russian strings for the new lobby option and description.
    • AI chat now attempts localization before sending messages.
  • UI
    • Mode display now clears on focus change and tabs update when focus army changes.

✏️ Tip: You can customize this high-level summary in your review settings.

@github-actions github-actions bot marked this pull request as draft January 11, 2026 10:26
@coderabbitai
Copy link

coderabbitai bot commented Jan 11, 2026

📝 Walkthrough

Walkthrough

Adds a "union when disconnected" lobby option and localization strings, implements AIBrain.ShareControlWithAllies and uses it on abandon when enabled, localizes AI chat text, wires focus-change UI hooks to clear mode text, and adds minor sim/init logging and docs clarification.

Changes

Cohort / File(s) Summary of changes (attention areas)
AIBrain control flow
lua/aibrain.lua
Added ShareControlWithAllies(self) and updated AbandonedByPlayer to call it when ScenarioInfo.Options.CommonArmy == "UnionWhenDisconnected". Check index base (0-based) for SetCommandSource, SyncAIChat side-effects, and SPEW/logging.
Lobby options & localization
lua/ui/lobby/lobbyOptions.lua, loc/US/strings_db.lua, loc/RU/strings_db.lua
New UnionWhenDisconnected scenario option added and corresponding localization keys lobui_CAUponDisconnectTitle / lobui_CAUponDisconnectDescription added. Verify locale keys and UI wiring.
AI chat localization
lua/AIChatSorian.lua
AI chat message text now uses LOC(text) with fallback. Confirm LOC availability and no behavior regressions.
Focus sync & UI tabs
lua/UserSync.lua, lua/ui/game/tabs.lua
Sync.FocusArmyChanged now triggers tabs.focusArmyChanged(). tabs gained ClearModeText() and focusArmyChanged handler to clear mode text and refresh display. Check import timing and UI refresh.
Sim docs / init logging
engine/Sim.lua, lua/simInit.lua
Clarified SetCommandSource parameter indices in docs (0-based) and added non-functional logging messages during union/common army init. No signature changes.

Sequence Diagram(s)

sequenceDiagram
  participant AIBrain as AIBrain (abandoned)
  participant Ally as Allied Human Brain
  participant Sim as Sim / SetCommandSource
  participant Net as Sync (SyncAIChat)
  participant UI as Tabs/UI

  AIBrain->>AIBrain: AbandonedByPlayer()
  AIBrain->>AIBrain: check CommonArmy == "UnionWhenDisconnected"
  alt option enabled
    AIBrain->>Ally: iterate allied human brains
    loop for each eligible ally
      AIBrain->>Sim: SetCommandSource(targetArmyIndex, sourceHumanIndex) (0-based)
      AIBrain->>Net: SyncAIChat(share notification)
      AIBrain->>AIBrain: SPEW log
    end
    AIBrain->>AIBrain: return true (share occurred)
  else
    AIBrain->>AIBrain: existing defeat handling
  end
  Note over Net,UI: Focus change -> UserSync -> tabs.focusArmyChanged -> ClearModeText -> UpdateModeDisplay
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • Simplify defeat backend #6965 — Overlaps refactor/handling of AIBrain defeat/abandonment logic; likely related to ShareControlWithAllies changes.

Suggested labels

area: matchmaker/autolobby

Suggested reviewers

  • lL1l1

Poem

🐇
I nudged the keys, then gave a twitch,
When armies sleep, allies switch—
A union blooms where one hops out,
I log, I share, I shout about!
Happy hops for merged command.

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description provides context, use cases, and testing details, but the checklist shows the changelog snippet and reviewer requests are incomplete, preventing a full pass. Complete the checklist by adding a changelog snippet and requesting 2-3 reviewers as specified in the template guidelines.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding a new lobby option for union control when an army is abandoned, which is directly reflected in the PR's core modifications.

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

✨ Finishing touches
  • 📝 Generate docstrings


📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e9db4e9 and 7376bdb.

📒 Files selected for processing (3)
  • loc/RU/strings_db.lua
  • lua/UserSync.lua
  • lua/ui/game/tabs.lua
🚧 Files skipped from review as they are similar to previous changes (3)
  • lua/ui/game/tabs.lua
  • lua/UserSync.lua
  • loc/RU/strings_db.lua

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


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.

@Garanas Garanas marked this pull request as ready for review January 11, 2026 10:27
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @loc/US/strings_db.lua:
- Around line 4716-4717: The lobui_CAUponDisconnectDescription localization
entry is missing the gameplay constraint that defeated armies are not shared;
update the lobui_CAUponDisconnectDescription string to include the same clause
used in the fallback help text (e.g., append "and the share condition does not
trigger" or equivalent phrasing) so the tooltip matches the behavior described
in lobbyOptions.lua's help text.
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8434c72 and 8f7bd9f.

📒 Files selected for processing (6)
  • engine/Sim.lua
  • loc/US/strings_db.lua
  • lua/AIChatSorian.lua
  • lua/aibrain.lua
  • lua/simInit.lua
  • lua/ui/lobby/lobbyOptions.lua
🧰 Additional context used
🧬 Code graph analysis (2)
lua/simInit.lua (2)
engine/Core.lua (2)
  • SPEW (408-409)
  • IsAlly (237-238)
engine/Sim.lua (1)
  • SetCommandSource (1177-1178)
lua/aibrain.lua (3)
engine/Core.lua (2)
  • IsAlly (237-238)
  • SPEW (408-409)
engine/Sim.lua (3)
  • SetCommandSource (1177-1178)
  • IsGameOver (714-715)
  • GetGameTick (624-625)
lua/SimSyncUtils.lua (1)
  • SyncAIChat (15-19)
🪛 Luacheck (1.2.0)
lua/aibrain.lua

[error] 491-491: expected '=' near 'end'

(E011)

🔇 Additional comments (8)
lua/AIChatSorian.lua (1)

19-19: LGTM: Localization support added correctly.

The localization lookup with fallback ensures backward compatibility while supporting the new localized chat messages introduced in this PR.

lua/simInit.lua (2)

460-460: LGTM: Helpful debug logging added.

The logging additions provide useful debugging information for the new union army control feature without affecting runtime behavior.


478-479: LGTM: Debug logging added for common army setup.

Consistent with the union army logging approach.

lua/aibrain.lua (2)

517-545: Well-structured abandon handling with union control support.

The modification correctly:

  1. Records abandonment time and logs the event
  2. Conditionally triggers union control sharing based on the lobby option
  3. Bypasses default defeat flow when control sharing succeeds

The logic is sound, assuming the continue keyword issue in ShareControlWithAllies is resolved.


474-513: No issue: FAF Lua supports the continue keyword.

The continue keyword is fully supported in FAF's extended Lua implementation. This is evident from the extensive use of continue throughout the codebase (200+ instances across multiple files including lua/simInit.lua, lua/EffectUtilities.lua, lua/WeaponPriorities.lua, and many AI/UI files), with no syntax errors or failures reported. The code is correct as written.

Likely an incorrect or invalid review comment.

engine/Sim.lua (1)

1174-1176: LGTM: Documentation clarified for SetCommandSource.

The updated documentation clearly specifies that both parameters use 0-based indexing and explains that sourceHumanIndex refers to actual command sources as shown in GetClientSessions. This clarification aligns with the implementation in lua/aibrain.lua line 500.

lua/ui/lobby/lobbyOptions.lua (2)

195-195: Nice formatting cleanup.

The indentation adjustment improves consistency with the rest of the options structure.


188-192: LGTM! New option integrated correctly.

The new "Union control upon disconnect" option is well-positioned as a middle ground between default and full union control. The localization keys are properly defined in loc/US/strings_db.lua, and the backend implementation correctly handles the UnionWhenDisconnected key in lua/aibrain.lua. Structure follows the established pattern with consistent indentation.

@Garanas Garanas requested review from KionX and lL1l1 January 11, 2026 10:48
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @lua/aibrain.lua:
- Line 494: The inline comment above the check "if brain.AbandonedAt then
continue end" is incorrect (it says allied players "are also abandoned")—update
that comment to say allied players "are NOT also abandoned" or "that are not
abandoned" so it matches the actual logic; locate the comment near the
brain.AbandonedAt check in aibrain.lua and replace the wording to correctly
reflect the skip behavior.
🧹 Nitpick comments (1)
lua/aibrain.lua (1)

525-529: Optional: Consider simplifying the condition formatting.

The multi-line condition with separate checks is readable but could be slightly more compact. This is purely a style preference.

♻️ Alternative formatting
-        if  ScenarioInfo.Options.CommonArmy == "UnionWhenDisconnected" and
-
-            -- do not trigger this behavior when this army is already defeated
-            (not self:IsDefeated())
-        then
+        -- Attempt union control if enabled and army is not already defeated
+        if ScenarioInfo.Options.CommonArmy == "UnionWhenDisconnected" and not self:IsDefeated() then
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8f7bd9f and 00e9da0.

📒 Files selected for processing (3)
  • loc/RU/strings_db.lua
  • loc/US/strings_db.lua
  • lua/aibrain.lua
✅ Files skipped from review due to trivial changes (1)
  • loc/RU/strings_db.lua
🚧 Files skipped from review as they are similar to previous changes (1)
  • loc/US/strings_db.lua
🧰 Additional context used
🧬 Code graph analysis (1)
lua/aibrain.lua (3)
engine/Core.lua (2)
  • IsAlly (237-238)
  • SPEW (408-409)
engine/Sim.lua (3)
  • SetCommandSource (1177-1178)
  • IsGameOver (714-715)
  • GetGameTick (624-625)
lua/SimSyncUtils.lua (1)
  • SyncAIChat (15-19)
🪛 Luacheck (1.2.0)
lua/aibrain.lua

[error] 491-491: expected '=' near 'end'

(E011)

🔇 Additional comments (4)
lua/aibrain.lua (4)

522-524: Good addition of abandonment tracking and logging.

Recording the abandonment timestamp and logging the event will help with debugging and understanding game flow. The use of GetGameTick() provides a precise timestamp for tracking when the abandonment occurred.


525-535: Verify the feature works as intended with the new option.

The integration logic looks correct: the code checks if the UnionWhenDisconnected option is enabled and the army isn't already defeated, then attempts to share control with allies. The early return on line 533 properly prevents the army from being defeated when sharing succeeds.

However, this depends on the correctness of ShareControlWithAllies (see previous comments about index calculation and continue keyword).

Test scenarios to verify:

  1. Player disconnects with UnionWhenDisconnected enabled → allies can control the army
  2. Player disconnects with option disabled → army is defeated immediately
  3. Already-defeated player's ally disconnects → control is not shared (line 528 check)
  4. Last player on a team disconnects → entire team defeated (mentioned in PR objectives)

491-491: Remove the continue keyword concern—it is valid in this codebase.

The continue keyword is a supported language feature widely used throughout the repository (50+ instances), including in similar patterns in lua/simInit.lua and other files. The static analysis error is a false positive and does not reflect actual syntax validity.


486-500: sourceIndex must only count human brains that pass all filters

The sourceIndex counter increments for every human brain (line 492), but some humans are skipped by the ally/abandoned checks (lines 494-496). This breaks the alignment with GetSessionClients indexing. Per the SetCommandSource documentation, sourceHumanIndex must "include only actual sources."

Move the sourceIndex increment to after all filtering checks to ensure it only counts humans that will actually receive command source updates.

if brain.BrainType ~= 'Human' then continue end
sourceIndex = sourceIndex + 1

-- only take into account allied players are also abandoned
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix incorrect comment.

The comment states "are also abandoned" but the logic on line 496 actually skips abandoned players (if brain.AbandonedAt then continue end). The comment should read "are NOT also abandoned" or "that are not abandoned".

📝 Proposed fix
-            -- only take into account allied players are also abandoned
+            -- only take into account allied players that are not abandoned
🤖 Prompt for AI Agents
In @lua/aibrain.lua at line 494, The inline comment above the check "if
brain.AbandonedAt then continue end" is incorrect (it says allied players "are
also abandoned")—update that comment to say allied players "are NOT also
abandoned" or "that are not abandoned" so it matches the actual logic; locate
the comment near the brain.AbandonedAt check in aibrain.lua and replace the
wording to correctly reflect the skip behavior.

@Garanas
Copy link
Member Author

Garanas commented Jan 11, 2026

A known bug is that the Score button does not disappear 😃 , if anyone has suggestions on how to address that then feel free. Fixed in e9db4e9

@4z0t could you check the Russian localization?

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @lua/ui/game/tabs.lua:
- Around line 1203-1209: ClearModeText mutates the upvalue table modes while
iterating which can skip keys and leave stale entries; instead reset modes by
replacing it with a new empty table (or build a separate list of keys and clear
those) and then call UpdateModeDisplay(); locate the ClearModeText function and
the modes upvalue and replace the in-loop nil assignments with an assignment
that reinitializes modes to {} (or clear via a collected key list) before
invoking UpdateModeDisplay().
🧹 Nitpick comments (1)
lua/ui/game/tabs.lua (1)

1272-1274: Exported focusArmyChanged wrapper is fine; consider making it a normal function for consistency.
This works, but for readability/consistency with other exported handlers in the codebase, consider function focusArmyChanged() instead of assigning an anonymous function.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 00e9da0 and e9db4e9.

📒 Files selected for processing (2)
  • lua/UserSync.lua
  • lua/ui/game/tabs.lua
🔇 Additional comments (1)
lua/UserSync.lua (1)

294-300: focusArmyChanged() is safe to call before tabs UI initialization. The handler only clears a local module-level modes table and calls UpdateModeDisplay(), which safely checks if controls.modeDisplay then before accessing UI state. The defensive pattern matches other handlers like massfabs and is idempotent—it works correctly whether Create has been called or not.

Copy link
Member

@4z0t 4z0t left a comment

Choose a reason for hiding this comment

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

I'm not a professional translator, but overall it is good

@Garanas Garanas merged commit fc23416 into develop Jan 17, 2026
4 checks passed
@Garanas Garanas deleted the feature/union-when-disconnected branch January 17, 2026 08:48
@Garanas
Copy link
Member Author

Garanas commented Jan 17, 2026

I casually forgot to make a changelog snippet 😞

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.

3 participants