Skip to content

Rework the chat window#7098

Open
Garanas wants to merge 131 commits into
FAForever:developfrom
Garanas:refactor/chat
Open

Rework the chat window#7098
Garanas wants to merge 131 commits into
FAForever:developfrom
Garanas:refactor/chat

Conversation

@Garanas
Copy link
Copy Markdown
Member

@Garanas Garanas commented Apr 26, 2026

Description of the proposed changes

See #7098 (comment) for some visuals

I was playing a game recently and someone shared me some units. I could not find them. Then I thought: let's add a chat message with some camera information in it in order to find them easier next time. That turned out to be quite hard in the original implementation, I could not figure it out.

On work I've been using Claude quite a bit. It is quite powerful. But what better to test its capabilities then on a code base, with a framework that the LLM did not train on indefinitely? One thing led to another, and here we have an entirely new chat window.

This chat window is implemented using the MVC-principle, to keep the model, the controller and the interface cleanly separated. I've learned from my last attempts with the autolobby, this time no property drilling but proper use of LazyVars.

In general all functionality of the existing chat window also exists in the new chat window. There are a few subtle differences, but none that are breaking in my point of view:

  • The chat (and the feed) starts bottom-up, instead of top-down.
  • The chat feed does not persist when opening/closing the chat window. I am not talking about the chat history here, but about the feed that shows when you receive a chat message but you do not have the chat window open.
  • The chat is a single window/rect now. The legacy SetLayout rotated through bottom / left / right HUD layouts; the new chat lets the user drag and resize freely instead.
  • Idle fade is now activity-based rather than a window-level wall-clock timer. Any interaction (keystrokes, scrolling, recipient hovers, incoming messages) re-stamps a LastActivity LazyVar; the window's OnFrame checks now − LastActivity against fade_time. Net effect: a burst of incoming chat keeps the window open instead of auto-closing mid-conversation. Unchecking the pin also re-stamps activity, so you get a full fade_time window instead of being closed instantly.

In comparison, we receive a lot of new features and capabilities:

  • The chat window fully supports UI scaling. Saved window positions are stored inverse-scaled and re-applied on next launch, so a user who changes ui_scale doesn't end up with a half-off-screen chat window.
  • The chat window will now always snap to frame(0) when opened, so that you can always view the chat even after changing resolution.
  • The chat window is now almost a command line interface where you can issue commands using /, these are simple to extend for UI mods. Built-ins include /all, /allies, /whisper (/w / /pm), /help, /gift-resources, /gift-units, /recall, /mute, /unmute, /clear, /restart, /save, /load, /pause, /resume, /speed, /end-mission, /to-engineers, /to-tick, /taunt, plus a few /debug-… helpers.
  • The chat window now supports various forms of auto complete. When you use /, you get a few suggestions of available commands in a popup above the edit box. When you use @ (or just press Tab on a partial nickname) you can auto-complete player names. Tab cycles through candidates; commands like /whisper @Jip accept the @-prefixed form too.
  • The chat window now mutes players on a technically feasible manner. The mute filter is no longer persisted, but we can adjust this accordingly. It is also reachable via the /mute @Nick and /unmute @Nick commands.
  • The simulation can send chat messages much more conveniently than before. Sim-side senders can also attach a lightweight Location hint (a world point or rectangle) instead of a full camera snapshot — useful for AI brains that don't have a saved view to ship.
  • The chat messages can now be localized, useful for system, event or AI messages.
  • Every top-level view (chat window, options dialog, edit area) is independently callable from a hotkey or the console with UI_Lua import(…).Toggle(). Useful for debugging in isolation, and the keybinding UI now exposes them under the chat category.
  • Hot reload support: saving any chat module rebuilds the in-game UI in place, with the model preserving history across the reload. Made the development cycle a lot faster.

We fix some bugs:

  • The UI scaling now works properly on all elements.
  • The scrollbar fades out properly when the window fades.
  • A new sim callback is introduced for chat messages instead of (ab)using the GiftResourcesToPlayer callback. The old sim callback is still emitted, but now once per message instead of X − 1 per player per message.
  • Incoming messages are shape-validated; modded / hostile / malformed payloads are dropped silently instead of being coerced. Legacy trusted whatever shape arrived on the wire.

And of course, what we started with:

  • When you gift units you receive a whisper with a camera location of where those units are.
  • When you gift resources you receive a whisper of the amount received.

Checklist

  • Check compatibility with replay parsing.

    This should be untouched, but then again - one can't know for sure. Better be safe than sorry here.

  • Check whether the way AIs use the chat still works.

I did some basic testing using the new methods, called from the ai brain this works:

ForkThread (
    function()
        WaitTicks(10)
        self:SendChatToAll("Easy AI engaged! This is a work in progress, expect weird behaviour and crashes. Report any issues on the forums with a detailed description of what happened and a save file if possible.")
        self:SendChatToPlayer(1, "I see yooouuu")
    end
)

Testing done on the proposed changes

Tested locally with multiple players using the launch script inside the scripts folder. Messages can be send just fine.

Debug key actions

All actions are filed under the chat category in the keybindings dialog (so they group with regular chat actions) but ship without default keys — bind them via the keybindings UI or invoke from the console with UI_Lua import("/lua/ui/game/chat/ChatDebug.lua").Foo().

Action Description
debug_chat_window Toggle the chat window.
debug_chat_config Toggle the chat options dialog.
debug_chat_append_system_message Append a local-only System: line — exercises AppendLocalSystemMessage and the system colour.
debug_chat_append_short_message Append one synthetic chat entry stamped with the local focus army's metadata — exercises the basic model→view path and snap-to-bottom.
debug_chat_append_long_message Append a multi-paragraph entry — exercises WrapEntry and the continuation-row layout at every supported font size.
debug_chat_append_burst Append ten synthetic entries in one batch — exercises pool sizing past the line cap, virtual-size accounting, and snap-to-bottom on rapid arrivals.
debug_chat_append_camera_message Append an entry with a Location hint at the current camera focus — exercises the cam-icon toggle on the row and Camera:MoveTo on click (press, pan away, click the icon — camera bounces back).
debug_chat_set_recipient_all Force the send target to all — exercises the recipient-label LazyVar binding.
debug_chat_set_recipient_allies Force the send target to allies.
debug_chat_clear_history Wipe the history log — exercises the empty-pool branch of CalcVisible and model-side dirty propagation.

Additional context

@BlackYps I don't think it makes sense to review this pull request line by line. Instead, it's a question of whether we want this as a whole or not. Yes, it will break a few things. Yes, it won't be perfect immediately. But, in my humble opinion, it is an improvement that makes the chat window maintainable again for the foreseeable future.

To be honest: I would not mind if this gets closed. The goal was to experiment with Claude and spec driven development on a code base that it did not quite train as much on as regular programming languages and/or frameworks. But it would be a loss in my opinion 😃 !

See #7098 (comment) for some visuals

Checklist

Summary by CodeRabbit

  • New Features
    • Complete chat system overhaul with redesigned UI, command palette, and autocomplete.
    • New slash commands: /all, /allies, /whisper, /mute, /unmute, /gift-units, /gift-resources, /save, /load, /pause, /resume, /recall, /help, and more.
    • Chat configuration dialog for colors, font size, fade time, and window opacity.
    • Player muting feature.
    • Localized chat notifications for resource and unit transfers (German, Russian, English).

Garanas added 30 commits April 18, 2026 21:29
@Garanas
Copy link
Copy Markdown
Member Author

Garanas commented Apr 26, 2026

@CodeRabbit review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 26, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@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: 3

🧹 Nitpick comments (4)
lua/ui/game/chat/ChatController.lua (2)

343-345: LOCF mutates msg.text in place; ensure no other receiver re-processes the same msg.

If the same msg table reference were ever passed through OnReceive more than once (e.g., a future sync replay handler also feeding the same payload), the second pass would LOCF an already-formatted string with the original Args and produce garbage. The current dedupe in OnSyncChatMessages keeps this safe today, but it relies on the dedupe set being populated correctly. Consider stamping msg.text only after a Args = nil clear, or formatting into a local variable instead of mutating the payload, so the invariant is enforced rather than implicit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lua/ui/game/chat/ChatController.lua` around lines 343 - 345, LOCF mutates
msg.text in place which can corrupt reprocessed messages; in the handler that
calls LOCF (the OnReceive path that currently sets msg.text = LOCF(msg.text,
unpack(msg.Args))), avoid mutating the incoming payload: either format into a
local variable (e.g., local formatted = LOCF(msg.text, unpack(msg.Args)) and use
that for downstream display) or call LOCF and then immediately clear msg.Args
(msg.Args = nil) before writing back to msg.text so subsequent passes won't
re-format; update the code around LOCF usage and ensure OnSyncChatMessages /
OnReceive consumers use the non-mutated msg or the cleared Args invariant.

233-245: GetArmyData mutates the engine's armies table during nickname lookups.

Line 240 writes v.ArmyID = i directly onto the table returned by GetArmiesTable(). If that table is shared or cached by the engine, this leaks state. The fix is trivial — wrap into a returned copy or pass i back via a separate return — but since the mutation is idempotent (always the same value for a given nickname), the impact is bounded. Worth confirming this doesn't trip anything that compares ArmyID for entries that were never looked up by nickname.

📝 Possible cleanup
 local function GetArmyData(army)
     local armies = GetArmiesTable()
     if type(army) == 'number' then
         return armies.armiesTable[army]
     elseif type(army) == 'string' then
         for i, v in armies.armiesTable do
             if v.nickname == army then
-                v.ArmyID = i
-                return v
+                local copy = table.copy(v)
+                copy.ArmyID = i
+                return copy
             end
         end
     end
 end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lua/ui/game/chat/ChatController.lua` around lines 233 - 245, GetArmyData
currently mutates the engine's armies table by assigning v.ArmyID = i when
looking up by nickname; instead avoid touching the original table — either
return a shallow copy of the army entry with ArmyID set, or return the found
entry and the index i as a second return value (e.g., return armyCopy, i) so
callers can use the ID without altering GetArmiesTable() data; update the
function GetArmyData and any callers to handle the new return shape accordingly.
lua/ui/game/chat/ChatLinesInterface.lua (2)

264-281: Minor: per-line SetFontSize runs before RebuildPool may destroy some lines.

Line 267–269 sets the font on every existing line, then RebuildPool at line 272 may destroy a tail of those lines if the new row height implies fewer slots. The wasted setup is harmless but inefficient. Reordering — RebuildPool first, then iterating the surviving rows — avoids work on destroyed controls.

♻️ Possible reorder
     ApplyOptions = function(self, options)
         local oldPoolSize = table.getn(self.ChatLineInterfaces)
         local size = options.font_size or 14
+        -- Set the seed row's font first so RebuildPool's height calc uses
+        -- the new metric, then resize the pool, then propagate the font
+        -- to the surviving rows.
+        if self.ChatLineInterfaces[1] then
+            self.ChatLineInterfaces[1]:SetFontSize(size)
+        end
+        self:RebuildPool()
         for _, line in ipairs(self.ChatLineInterfaces) do
             line:SetFontSize(size)
         end
-        -- Row height tracks the font, so the pool may need resizing;
-        -- wrap widths depend on font metrics, so rewrap all entries.
-        self:RebuildPool()
         self:RewrapAll()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lua/ui/game/chat/ChatLinesInterface.lua` around lines 264 - 281, In
ApplyOptions, avoid calling SetFontSize on lines that may be destroyed by
RebuildPool: compute size as now, call self:RebuildPool() before iterating
self.ChatLineInterfaces, then loop over the surviving entries and call
line:SetFontSize(size) and then self:RewrapAll() (and keep the subsequent
RefreshVirtualSize(), RecomputeScrollTopForPoolChange(oldPoolSize) and
CalcVisible calls) so you only update font on controls that still exist;
reference ApplyOptions, SetFontSize, RebuildPool, RewrapAll, RefreshVirtualSize,
RecomputeScrollTopForPoolChange, and CalcVisible.

576-591: Duplicate bodies in OnResizeLive / OnResizeFinished.

The two handlers are identical line-for-line. If the intent is for them to diverge later (e.g., skip rewrap during live resize), keep as-is; otherwise factor into a single helper to avoid drift.

♻️ Possible factor
+    ---@param self UIChatLinesInterface
+    ApplyResize = function(self)
+        local oldPoolSize = table.getn(self.ChatLineInterfaces)
+        self:RebuildPool()
+        self:RewrapAll()
+        self:RecomputeScrollTopForPoolChange(oldPoolSize)
+        self:CalcVisible()
+    end,
+
     ---@param self UIChatLinesInterface
-    OnResizeLive = function(self)
-        local oldPoolSize = table.getn(self.ChatLineInterfaces)
-        self:RebuildPool()
-        self:RewrapAll()
-        self:RecomputeScrollTopForPoolChange(oldPoolSize)
-        self:CalcVisible()
-    end,
+    OnResizeLive = function(self) self:ApplyResize() end,

     ---@param self UIChatLinesInterface
-    OnResizeFinished = function(self)
-        local oldPoolSize = table.getn(self.ChatLineInterfaces)
-        self:RebuildPool()
-        self:RewrapAll()
-        self:RecomputeScrollTopForPoolChange(oldPoolSize)
-        self:CalcVisible()
-    end,
+    OnResizeFinished = function(self) self:ApplyResize() end,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lua/ui/game/chat/ChatLinesInterface.lua` around lines 576 - 591, Both
OnResizeLive and OnResizeFinished currently contain identical bodies (calling
RebuildPool, RewrapAll, RecomputeScrollTopForPoolChange(oldPoolSize), and
CalcVisible) which risks drift; factor this shared logic into a single helper
(e.g., a method named ApplyResizeUpdate or DoResizeUpdate) that captures
obtaining oldPoolSize via table.getn(self.ChatLineInterfaces) and then calls
RebuildPool, RewrapAll, RecomputeScrollTopForPoolChange, and CalcVisible, then
have OnResizeLive and OnResizeFinished each call that helper (or, if intended to
differ later, keep one as a thin wrapper that calls the helper and adds extra
behavior).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lua/ChatUtils.lua`:
- Line 65: Update the `@param` annotation in ChatUtils.lua for the data parameter
to match actual usage: change Sender to a string and Msg to the ChatPayload
table type (instead of integer and string); reference the producing call site
ChatController.Send (where Sender = senderData and senderData.nickname or
tostring(focusArmy) and Msg = msg) to verify the payload shape and ensure the
annotation uses the correct type name for ChatPayload.

In `@lua/shared/ChatPayload.lua`:
- Line 33: The `@field` annotation for ChatPayload's From should be changed to
optional to match the class doc and runtime behavior: update the ChatPayload
declaration to mark From as optional (e.g., use `number?` or the
language-specific optional syntax for `@field`) so static analysis aligns with the
fact that Send() in ChatController does not set From and the sim relay
(RelayChatMessage) populates it later; ensure the annotation uses the same
optional style used elsewhere in this file for consistency.

In `@lua/SimUtils.lua`:
- Around line 1538-1549: The code computes capped amounts massGiven and
energyGiven but still calls toBrain:GiveResource with the uncapped massTaken and
energyTaken, causing the giver to lose overflowed resources; update the two
calls to toBrain:GiveResource to pass massGiven and energyGiven (respectively)
instead of massTaken and energyTaken so the actual transferred amounts match the
computed caps (refer to variables massCapacity, energyCapacity, massGiven,
energyGiven and the toBrain:GiveResource calls).

---

Nitpick comments:
In `@lua/ui/game/chat/ChatController.lua`:
- Around line 343-345: LOCF mutates msg.text in place which can corrupt
reprocessed messages; in the handler that calls LOCF (the OnReceive path that
currently sets msg.text = LOCF(msg.text, unpack(msg.Args))), avoid mutating the
incoming payload: either format into a local variable (e.g., local formatted =
LOCF(msg.text, unpack(msg.Args)) and use that for downstream display) or call
LOCF and then immediately clear msg.Args (msg.Args = nil) before writing back to
msg.text so subsequent passes won't re-format; update the code around LOCF usage
and ensure OnSyncChatMessages / OnReceive consumers use the non-mutated msg or
the cleared Args invariant.
- Around line 233-245: GetArmyData currently mutates the engine's armies table
by assigning v.ArmyID = i when looking up by nickname; instead avoid touching
the original table — either return a shallow copy of the army entry with ArmyID
set, or return the found entry and the index i as a second return value (e.g.,
return armyCopy, i) so callers can use the ID without altering GetArmiesTable()
data; update the function GetArmyData and any callers to handle the new return
shape accordingly.

In `@lua/ui/game/chat/ChatLinesInterface.lua`:
- Around line 264-281: In ApplyOptions, avoid calling SetFontSize on lines that
may be destroyed by RebuildPool: compute size as now, call self:RebuildPool()
before iterating self.ChatLineInterfaces, then loop over the surviving entries
and call line:SetFontSize(size) and then self:RewrapAll() (and keep the
subsequent RefreshVirtualSize(), RecomputeScrollTopForPoolChange(oldPoolSize)
and CalcVisible calls) so you only update font on controls that still exist;
reference ApplyOptions, SetFontSize, RebuildPool, RewrapAll, RefreshVirtualSize,
RecomputeScrollTopForPoolChange, and CalcVisible.
- Around line 576-591: Both OnResizeLive and OnResizeFinished currently contain
identical bodies (calling RebuildPool, RewrapAll,
RecomputeScrollTopForPoolChange(oldPoolSize), and CalcVisible) which risks
drift; factor this shared logic into a single helper (e.g., a method named
ApplyResizeUpdate or DoResizeUpdate) that captures obtaining oldPoolSize via
table.getn(self.ChatLineInterfaces) and then calls RebuildPool, RewrapAll,
RecomputeScrollTopForPoolChange, and CalcVisible, then have OnResizeLive and
OnResizeFinished each call that helper (or, if intended to differ later, keep
one as a thin wrapper that calls the helper and adds extra behavior).
🪄 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: 27ce0f1f-395c-41b4-99a8-452bc5b9d587

📥 Commits

Reviewing files that changed from the base of the PR and between fb255f6 and bb667e9.

📒 Files selected for processing (9)
  • .claude/.gitignore
  • lua/ChatUtils.lua
  • lua/SimUtils.lua
  • lua/shared/ChatPayload.lua
  • lua/ui/game/chat/ChatController.lua
  • lua/ui/game/chat/ChatEditInterface.lua
  • lua/ui/game/chat/ChatLinesInterface.lua
  • lua/ui/game/chat/ChatModel.lua
  • lua/ui/game/chat/ChatUtils.lua
✅ Files skipped from review due to trivial changes (1)
  • .claude/.gitignore

Comment thread lua/ChatUtils.lua Outdated
--- `Sync.ChatMessages` (see `SendChatMessage`) and external replay parsers
--- read the `Sender`/`Msg` fields off the recorded `GiveResourcesToPlayer`
--- callback args, which the UI sender emits once per outgoing message.
---@param data {Sender: integer, Msg: string}
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

Annotation mismatch: Sender is a string and Msg is a ChatPayload, not the documented types.

Looking at the producing call site in ChatController.Send (lines 533–534): Sender = senderData and senderData.nickname or tostring(focusArmy) is a string and Msg = msg is the ChatPayload table. The @param data {Sender: integer, Msg: string} annotation is reversed/wrong on both fields.

📝 Proposed fix
----@param data {Sender: integer, Msg: string}
+---@param data {Sender: string, Msg: ChatPayload}
 function SendChatToReplay(data)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lua/ChatUtils.lua` at line 65, Update the `@param` annotation in ChatUtils.lua
for the data parameter to match actual usage: change Sender to a string and Msg
to the ChatPayload table type (instead of integer and string); reference the
producing call site ChatController.Send (where Sender = senderData and
senderData.nickname or tostring(focusArmy) and Msg = msg) to verify the payload
shape and ensure the annotation uses the correct type name for ChatPayload.

Comment thread lua/shared/ChatPayload.lua Outdated
Comment thread lua/SimUtils.lua Outdated
Copy link
Copy Markdown
Contributor

@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

🧹 Nitpick comments (1)
lua/ui/game/chat/commands/ChatCommandTypes.lua (1)

58-61: Resolvers.Player doc claim doesn't match behavior.

The --- ... Rejects "all"/"allies". comment suggests an explicit rejection, but ResolveArmy only rejects them implicitly via the "no player named 'all'." fallthrough. For /whisper all the user gets a misleading "no player named 'all'" error instead of something like "use a player nickname or army ID, not 'all'/'allies'". Optional refinement:

♻️ Proposed clearer rejection
 Resolvers.Player = function(token)
+    local lower = string.lower(token)
+    if lower == 'all' or lower == 'allies' or lower == 'team' then
+        return false, string.format("'%s' is a recipient, not a player.", token)
+    end
     return ResolveArmy(token)
 end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lua/ui/game/chat/commands/ChatCommandTypes.lua` around lines 58 - 61, The
comment says the doc implies explicit rejection of "all"/"allies" but
Resolvers.Player currently just calls ResolveArmy and returns a misleading "no
player named 'all'" error; update Resolvers.Player to first check if token
equals "all" or "allies" (case-insensitive) and return a clear rejection/error
message like "use a player nickname or army ID, not 'all'/'allies'"; otherwise
call ResolveArmy(token) as before so normal resolution remains unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lua/ui/game/chat/commands/builtin/DebugDumpControls.lua`:
- Around line 5-6: The Description string for the builtin command Name =
'debug-dump-controls' is incorrect (a copy/paste leftover referencing the
Alt+click army-switch toggle); update the Description to accurately describe
what Execute does (calling UI_DumpControlsUnderCursor) so help/autocomplete
shows the correct behavior — locate the Description field in
DebugDumpControls.lua and replace the misleading text with a concise description
like "Dump UI controls under the cursor" or similar.

---

Nitpick comments:
In `@lua/ui/game/chat/commands/ChatCommandTypes.lua`:
- Around line 58-61: The comment says the doc implies explicit rejection of
"all"/"allies" but Resolvers.Player currently just calls ResolveArmy and returns
a misleading "no player named 'all'" error; update Resolvers.Player to first
check if token equals "all" or "allies" (case-insensitive) and return a clear
rejection/error message like "use a player nickname or army ID, not
'all'/'allies'"; otherwise call ResolveArmy(token) as before so normal
resolution remains unchanged.
🪄 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: 38ea1374-7118-47a4-834e-d20213cf8155

📥 Commits

Reviewing files that changed from the base of the PR and between bb667e9 and d04350f.

📒 Files selected for processing (26)
  • lua/ui/game/chat/commands/ChatCommandRegistry.lua
  • lua/ui/game/chat/commands/ChatCommandTypes.lua
  • lua/ui/game/chat/commands/builtin/All.lua
  • lua/ui/game/chat/commands/builtin/Allies.lua
  • lua/ui/game/chat/commands/builtin/Clear.lua
  • lua/ui/game/chat/commands/builtin/DebugDumpControls.lua
  • lua/ui/game/chat/commands/builtin/DebugLog.lua
  • lua/ui/game/chat/commands/builtin/DebugStatistics.lua
  • lua/ui/game/chat/commands/builtin/Debugger.lua
  • lua/ui/game/chat/commands/builtin/EndMission.lua
  • lua/ui/game/chat/commands/builtin/GiftResources.lua
  • lua/ui/game/chat/commands/builtin/GiftUnits.lua
  • lua/ui/game/chat/commands/builtin/Help.lua
  • lua/ui/game/chat/commands/builtin/Load.lua
  • lua/ui/game/chat/commands/builtin/Mute.lua
  • lua/ui/game/chat/commands/builtin/Pause.lua
  • lua/ui/game/chat/commands/builtin/Recall.lua
  • lua/ui/game/chat/commands/builtin/Restart.lua
  • lua/ui/game/chat/commands/builtin/Resume.lua
  • lua/ui/game/chat/commands/builtin/Save.lua
  • lua/ui/game/chat/commands/builtin/Speed.lua
  • lua/ui/game/chat/commands/builtin/Taunt.lua
  • lua/ui/game/chat/commands/builtin/ToEngineers.lua
  • lua/ui/game/chat/commands/builtin/ToTick.lua
  • lua/ui/game/chat/commands/builtin/Unmute.lua
  • lua/ui/game/chat/commands/builtin/Whisper.lua
✅ Files skipped from review due to trivial changes (1)
  • lua/ui/game/chat/commands/builtin/Resume.lua
🚧 Files skipped from review as they are similar to previous changes (15)
  • lua/ui/game/chat/commands/builtin/Pause.lua
  • lua/ui/game/chat/commands/builtin/Restart.lua
  • lua/ui/game/chat/commands/builtin/Allies.lua
  • lua/ui/game/chat/commands/builtin/EndMission.lua
  • lua/ui/game/chat/commands/builtin/DebugStatistics.lua
  • lua/ui/game/chat/commands/builtin/Recall.lua
  • lua/ui/game/chat/commands/builtin/Speed.lua
  • lua/ui/game/chat/commands/builtin/Unmute.lua
  • lua/ui/game/chat/commands/builtin/Load.lua
  • lua/ui/game/chat/commands/builtin/Debugger.lua
  • lua/ui/game/chat/commands/builtin/DebugLog.lua
  • lua/ui/game/chat/commands/builtin/Help.lua
  • lua/ui/game/chat/commands/builtin/ToTick.lua
  • lua/ui/game/chat/commands/builtin/GiftUnits.lua
  • lua/ui/game/chat/commands/builtin/Taunt.lua

Comment thread lua/ui/game/chat/commands/builtin/DebugDumpControls.lua Outdated
Copy link
Copy Markdown
Contributor

@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: 6

🧹 Nitpick comments (4)
lua/ui/game/chat/ChatLineInterface.lua (1)

94-95: Reuse DefaultBodyColor instead of duplicating the literal.

'ffc2f6ff' here is the same magic value as the DefaultBodyColor constant declared at Line 17. The init color is overwritten by SetHeader/SetContinuation/Clear on first use, but keeping a duplicate literal is a quiet drift hazard.

♻️ Proposed fix
         self.Text = UIUtil.CreateText(self, '', 14, 'Arial')
-        self.Text:SetColor('ffc2f6ff')
+        self.Text:SetColor(DefaultBodyColor)
         self.Text:SetDropShadow(true)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lua/ui/game/chat/ChatLineInterface.lua` around lines 94 - 95, Replace the
duplicated literal color with the existing constant: in the ChatLineInterface
initializer where self.Text is created (UIUtil.CreateText(...)) and its color
set via self.Text:SetColor('ffc2f6ff'), use the predefined DefaultBodyColor
constant instead; update the call to SetColor to reference DefaultBodyColor so
the class (ChatLineInterface) consistently reuses the same color value that
SetHeader/SetContinuation/Clear expect.
lua/ui/game/chat/ChatUtils.lua (1)

53-65: Pre-scale the literal + 4 gap to keep wrap consistent across UI scales.

measureLine.Right(), Name.Left(), and Name:GetStringAdvance(name) are already in scaled pixels, so adding an unscaled 4 shrinks the reserved gap proportionally as ui_scale grows. Capture a scaled constant once and reuse it.

♻️ Proposed fix
 function WrapEntry(entry, measureLine)
     if not measureLine then
         entry.WrappedText = { entry.Text or '' }
         return
     end

     local name = entry.Name or ''
+    local gap = LayoutHelpers.ScaleNumber(4)
     local lines = MauiWrapText(entry.Text or '',
         function(lineIndex)
             if lineIndex == 1 then
                 return measureLine.Right()
-                    - (measureLine.Name.Left() + measureLine.Name:GetStringAdvance(name) + 4)
+                    - (measureLine.Name.Left() + measureLine.Name:GetStringAdvance(name) + gap)
             else
                 return measureLine.Right()
-                    - (measureLine.Name.Left() + 4)
+                    - (measureLine.Name.Left() + gap)
             end
         end,

This requires importing LayoutHelpers at the top:

 local MauiWrapText = import("/lua/maui/text.lua").WrapText
 local ChatPayload = import("/lua/shared/ChatPayload.lua")
+local LayoutHelpers = import("/lua/maui/layouthelpers.lua")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lua/ui/game/chat/ChatUtils.lua` around lines 53 - 65, The literal "+ 4" must
be scaled with the UI scale and reused: import LayoutHelpers (or ensure it's
available), compute a local scaled gap (e.g., local gap =
LayoutHelpers.ScaleNumber(4)) and replace both occurrences of " + 4" inside the
MauiWrapText width function with that scaled gap so measureLine.Right(),
measureLine.Name.Left(), and measureLine.Name:GetStringAdvance(name) remain in
scaled pixels and the reserved gap stays consistent across ui_scale; keep the
new local variable near where measureLine is used so MauiWrapText's width
callbacks reference gap instead of the unscaled 4.
lua/ui/game/chat/ChatLinesInterface.lua (1)

238-241: Redundant RefreshVirtualSize call in ApplyOptions.

self:RewrapAll() at line 239 already invokes self:RefreshVirtualSize(history) (line 264), so the explicit call at line 241 walks the entire history a second time on every committed-options change. Drop the duplicate to keep the option-change path single-pass.

♻️ Proposed cleanup
         self:RebuildPool()
         self:RewrapAll()
 
-        self:RefreshVirtualSize()
         self:RecomputeScrollTopForPoolChange(oldPoolSize)
         self:CalcVisible()

As per coding guidelines: "WrappedText cache on UIChatEntry should be updated by ChatLinesInterface when entry text or row width changes" — RewrapAll is already the canonical update path and refreshes virtual size as part of that cycle.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lua/ui/game/chat/ChatLinesInterface.lua` around lines 238 - 241, Remove the
redundant call to RefreshVirtualSize from ApplyOptions: after calling
self:RebuildPool() and self:RewrapAll() the virtual size is already recomputed
by RewrapAll (which calls self:RefreshVirtualSize(history)), so delete the
explicit self:RefreshVirtualSize() invocation in ApplyOptions to avoid a second
full-history pass; keep RebuildPool and RewrapAll as-is so wrapped-text caching
and virtual size updates remain driven by RewrapAll.
lua/ui/game/chat/ChatController.lua (1)

283-285: Wrap LOCF with pcall to handle mismatched Args gracefully.

ChatPayload.IsValidPayload validates that Args is a table (if present) but does not verify its contents match the format specifiers in msg.text. A malformed payload like { text='%d', Args={'oops'} } will cause string.format to fail. Use pcall to catch the error and fall back to the raw template:

Hardening
    if msg.Args then
-       msg.text = LOCF(msg.text, unpack(msg.Args))
+       local ok, formatted = pcall(LOCF, msg.text, unpack(msg.Args))
+       if ok then
+           msg.text = formatted
+       end
    end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lua/ui/game/chat/ChatController.lua` around lines 283 - 285, Wrap the
LOCF(msg.text, unpack(msg.Args)) call in a pcall to prevent a malformed Args
table from causing an error; in ChatController.lua where you currently have the
LOCF invocation (handling msg.Args), call pcall(LOCF, msg.text,
unpack(msg.Args)) and only replace msg.text when pcall returns true, otherwise
leave msg.text as the original template and optionally log or ignore the format
failure; this change hardens handling around msg.Args and works with existing
ChatPayload.IsValidPayload validations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lua/ui/game/chat/ChatController.lua`:
- Around line 268-328: OnReceive currently misses id-based deduping promised by
the docstring; add an early check to skip messages whose msg.Id is already in
the chat history and ensure messages processed here seed the history after
rendering. Concretely: in OnReceive, before any rendering/LOCF/Notify logic
consult ChatModel.history (or the module's history table used by
OnSyncChatMessages) and if msg.Id and history[msg.Id] then return; after
AppendChatLine succeeds, if msg.Id then set history[msg.Id] = true so future
OnReceive/OnSyncChatMessages calls will be deduped. Reference: OnReceive,
msg.Id, AppendChatLine, OnSyncChatMessages, ChatModel.history (history table).

In `@lua/ui/game/chat/ChatListInterface.lua`:
- Around line 62-72: The global click handler onOutsideClick currently calls
self:Destroy() unconditionally via UIMain.AddOnMouseClickedFunc, which preempts
control-level handlers; modify onOutsideClick to accept the event parameter and
only call self:Destroy() when the click is outside this popup's bounds by
checking event.x/event.y against this control's Left()/Right()/Top()/Bottom();
apply the same bounds-guarded pattern to the other onOutsideClick instance
around the 126–136 range and ensure UIMain.RemoveOnMouseClickedFunc is still
called in self.OnDestroy and _OnClosed behavior remains unchanged.
- Around line 162-174: The popup layout is double-scaling dimensions because
__post_init computes maxWidth/totalHeight from already-scaled entry.Text: do not
pass those raw numbers into Layouter:Width()/Height() which auto-scale; instead
call Layouter:SetFunction("Width", function() return maxWidth + 40 end) and
Layouter:SetFunction("Height", function() return totalHeight end) (or otherwise
use :SetFunction for the same keys) so the values bypass Layouter's internal
ScaleNumber; update the __post_init block around the
Layouter(self):Width(...):Height(...):End() call to use :SetFunction for "Width"
and "Height" referencing maxWidth and totalHeight computed from
entry.Text.Width()/Height().

In `@lua/ui/game/chat/config/ChatConfigController.lua`:
- Around line 69-80: SetMutedLive currently mutates only Committed (via Model()
and model.Committed:Set) causing Pending to remain stale and later Apply to
overwrite the live change; update SetMutedLive to mirror the same mutated
options into Pending as well (i.e., after computing options.muted and calling
model.Committed:Set(options), also update model.Pending:Set(...) or the
equivalent Pending API so the dialog’s Pending state and the PendingObserver in
ChatConfigInterface.lua remain in sync with the live mute change).

In `@lua/ui/game/chat/config/ChatConfigInterface.lua`:
- Around line 418-420: The OnClose handler currently calls Close() only, leaving
Pending edits intact; change the OnClose closure to call Cancel() followed by
Close() (i.e., mirror the explicit Cancel button behavior) so X discards drafts.
Additionally, modify Open (the function that currently calls Show() on the
existing instance) to re-sync Pending from Committed before calling Show() —
ensure Open resets the dialog's Pending state to the current Committed values
(e.g., copy/mirror Committed -> Pending or call the existing reset logic) so
reopening always reflects up-to-date committed settings and avoids clobbering
intervening SetMutedLive changes.
- Around line 39-51: The UI strings in ColorDefs and CheckboxDefs (e.g., Text
values "All","Allies","Private","Links","Notify" and "Default recipient:
allies","Show feed background","Show camera links") are hardcoded English; wrap
every user-visible Text/Tooltip and button/label strings referenced by
ChatConfigInterface (including slider preview labels generated with
string.format) with the localization token pattern <LOC ...>...LOC so they go
through the localization pipeline, and replace any string.format("Font Size:
%d", value) style formatting with a localized format token (e.g., a <LOC> token
that includes a placeholder) so the numeric value is interpolated via the
localization-aware formatting; update the entries in ColorDefs and CheckboxDefs
and the slider preview/label code paths accordingly (use the same unique
keys/identifiers currently used in the diff so callers like the slider preview
and any UI builders pick up the localized strings).

---

Nitpick comments:
In `@lua/ui/game/chat/ChatController.lua`:
- Around line 283-285: Wrap the LOCF(msg.text, unpack(msg.Args)) call in a pcall
to prevent a malformed Args table from causing an error; in ChatController.lua
where you currently have the LOCF invocation (handling msg.Args), call
pcall(LOCF, msg.text, unpack(msg.Args)) and only replace msg.text when pcall
returns true, otherwise leave msg.text as the original template and optionally
log or ignore the format failure; this change hardens handling around msg.Args
and works with existing ChatPayload.IsValidPayload validations.

In `@lua/ui/game/chat/ChatLineInterface.lua`:
- Around line 94-95: Replace the duplicated literal color with the existing
constant: in the ChatLineInterface initializer where self.Text is created
(UIUtil.CreateText(...)) and its color set via self.Text:SetColor('ffc2f6ff'),
use the predefined DefaultBodyColor constant instead; update the call to
SetColor to reference DefaultBodyColor so the class (ChatLineInterface)
consistently reuses the same color value that SetHeader/SetContinuation/Clear
expect.

In `@lua/ui/game/chat/ChatLinesInterface.lua`:
- Around line 238-241: Remove the redundant call to RefreshVirtualSize from
ApplyOptions: after calling self:RebuildPool() and self:RewrapAll() the virtual
size is already recomputed by RewrapAll (which calls
self:RefreshVirtualSize(history)), so delete the explicit
self:RefreshVirtualSize() invocation in ApplyOptions to avoid a second
full-history pass; keep RebuildPool and RewrapAll as-is so wrapped-text caching
and virtual size updates remain driven by RewrapAll.

In `@lua/ui/game/chat/ChatUtils.lua`:
- Around line 53-65: The literal "+ 4" must be scaled with the UI scale and
reused: import LayoutHelpers (or ensure it's available), compute a local scaled
gap (e.g., local gap = LayoutHelpers.ScaleNumber(4)) and replace both
occurrences of " + 4" inside the MauiWrapText width function with that scaled
gap so measureLine.Right(), measureLine.Name.Left(), and
measureLine.Name:GetStringAdvance(name) remain in scaled pixels and the reserved
gap stays consistent across ui_scale; keep the new local variable near where
measureLine is used so MauiWrapText's width callbacks reference gap instead of
the unscaled 4.
🪄 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: fcffe53d-b331-4bf0-90bf-4d70221471c6

📥 Commits

Reviewing files that changed from the base of the PR and between d04350f and 9002d91.

📒 Files selected for processing (17)
  • lua/ui/controls/floattext.lua
  • lua/ui/game/chat/ChatCommandHintInterface.lua
  • lua/ui/game/chat/ChatCompletion.lua
  • lua/ui/game/chat/ChatController.lua
  • lua/ui/game/chat/ChatDebug.lua
  • lua/ui/game/chat/ChatEditInterface.lua
  • lua/ui/game/chat/ChatFactionBadge.lua
  • lua/ui/game/chat/ChatFeedInterface.lua
  • lua/ui/game/chat/ChatInterface.lua
  • lua/ui/game/chat/ChatLineInterface.lua
  • lua/ui/game/chat/ChatLinesInterface.lua
  • lua/ui/game/chat/ChatListInterface.lua
  • lua/ui/game/chat/ChatModel.lua
  • lua/ui/game/chat/ChatUtils.lua
  • lua/ui/game/chat/config/ChatConfigController.lua
  • lua/ui/game/chat/config/ChatConfigInterface.lua
  • lua/ui/game/chat/config/ChatConfigModel.lua
✅ Files skipped from review due to trivial changes (3)
  • lua/ui/game/chat/ChatFactionBadge.lua
  • lua/ui/game/chat/ChatDebug.lua
  • lua/ui/game/chat/ChatEditInterface.lua
🚧 Files skipped from review as they are similar to previous changes (2)
  • lua/ui/game/chat/ChatCompletion.lua
  • lua/ui/game/chat/config/ChatConfigModel.lua

Comment on lines +268 to +328
function OnReceive(sender, msg)
if type(sender) ~= 'string' or sender == '' then
sender = 'nil sender'
end

-- Shape validation lives in `ChatPayload.IsValidPayload` (shared with the
-- sim relay). The receive path is reachable from external mods, so the
-- payload can't be trusted. Sender / observer consistency below needs
-- session context the shared validator can't see.
if not ChatPayload.IsValidPayload(msg) then return end

-- LOCF-style format-on-receive: when the sender ships `Args`, treat
-- `msg.text` as a `string.format` template (typically a `<LOC ...>` tag)
-- so the result respects the viewer's locale. The cap was applied to the
-- pre-format template, so `LOCF` can legitimately exceed it.
if msg.Args then
msg.text = LOCF(msg.text, unpack(msg.Args))
end

-- Notify owns the display decision for `to='notify'`; only fall through
-- to rendering a chat line if it returns false.
if msg.to == ChatModel.RecipientNotify and not import("/lua/ui/notify/notify.lua").processIncomingMessage(sender, msg) then
return
end

local armyData = GetArmyData(sender)
if not armyData and GetFocusArmy() ~= -1 and not SessionIsReplay() then
return
end

-- `msg.Observer` is only set when the sender has no army entry. A peer
-- claiming Observer while resolving to a real army is malformed — drop
-- the message entirely rather than stripping the flag, which would let
-- manipulated traffic render under a different label.
if msg.Observer and armyData then return end

local to = msg.to
local descriptor = ToStrings[to] or ToStrings.private
local towho = msg.Observer and LOC("<LOC lobui_0692>to observers:") or LOC(descriptor.text)

local name
if type(to) == 'number' and SessionIsReplay() then
-- In a replay, private messages need the full routing so spectators
-- can attribute the conversation.
name = string.format("%s %s %s:", sender, LOC(ToStrings.to.text),
(GetArmyData(to) or {}).nickname or tostring(to))
else
name = sender .. ' ' .. towho
end

AppendChatLine {
Name = name,
Text = msg.text,
ArmyData = armyData,
IsObserver = msg.Observer,
Recipient = to,
Camera = msg.camera,
Location = msg.location,
Id = msg.Id,
}
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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm OnReceive is the only sink for both delivery paths and that no
# other dedup layer exists upstream.
rg -nP --type=lua -C2 'RegisterChatFunc\s*\('
rg -nP --type=lua -C2 'AddOnSyncHashedCallback\s*\(\s*OnSyncChatMessages'
rg -nP --type=lua -C3 '\bOnReceive\s*\(' lua/ui/game/chat/

Repository: FAForever/fa

Length of output: 5300


🏁 Script executed:

# View docstring at lines 330-335 and Send function to confirm the claim
sed -n '330,335p' lua/ui/game/chat/ChatController.lua
echo "---"
sed -n '387,410p' lua/ui/game/chat/ChatController.lua

Repository: FAForever/fa

Length of output: 1272


🏁 Script executed:

# Continue reading Send function to find both delivery paths (SessionSendChatMessage and OnEcho calls)
sed -n '387,473p' lua/ui/game/chat/ChatController.lua

Repository: FAForever/fa

Length of output: 3357


OnReceive lacks the symmetric id-dedup the docstring promises.

OnSyncChatMessages (line 347) skips messages whose msg.Id is already in history, but OnReceive itself does no id check. In live play both delivery paths fire unconditionally: SessionSendChatMessage (direct via registered 'Chat' handler) and SimCallback SendChatMessage (sim relay). The engine does not guarantee ordering between them. When the sim path lands first, OnSyncChatMessages → OnReceive renders the entry; the subsequent direct SessionSendChatMessage → OnReceive then renders the same message again because OnReceive never consults history. The docstring at lines 330–335 explicitly states "whichever path lands first seeds the entry's Id and this handler skips duplicates by id" — making this an intent/impl mismatch.

🛡️ Proposed fix: dedup at the OnReceive entry point
 function OnReceive(sender, msg)
     if type(sender) ~= 'string' or sender == '' then
         sender = 'nil sender'
     end
 
     if not ChatPayload.IsValidPayload(msg) then return end
 
+    -- Symmetric dedupe with OnSyncChatMessages: whichever path lands first
+    -- seeds history with msg.Id; the other path then short-circuits here.
+    if msg.Id then
+        local history = ChatModel.GetSingleton().History()
+        for _, entry in history do
+            if entry.Id == msg.Id then return end
+        end
+    end
+
     if msg.Args then
         msg.text = LOCF(msg.text, unpack(msg.Args))
     end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lua/ui/game/chat/ChatController.lua` around lines 268 - 328, OnReceive
currently misses id-based deduping promised by the docstring; add an early check
to skip messages whose msg.Id is already in the chat history and ensure messages
processed here seed the history after rendering. Concretely: in OnReceive,
before any rendering/LOCF/Notify logic consult ChatModel.history (or the
module's history table used by OnSyncChatMessages) and if msg.Id and
history[msg.Id] then return; after AppendChatLine succeeds, if msg.Id then set
history[msg.Id] = true so future OnReceive/OnSyncChatMessages calls will be
deduped. Reference: OnReceive, msg.Id, AppendChatLine, OnSyncChatMessages,
ChatModel.history (history table).

Comment thread lua/ui/game/chat/ChatListInterface.lua
Comment thread lua/ui/game/chat/ChatListInterface.lua
Comment thread lua/ui/game/chat/config/ChatConfigController.lua
Comment on lines +39 to +51
local ColorDefs = {
{ Key = ChatConfigModel.KeyAllColor, Text = "All", Tooltip = 'chat_color' },
{ Key = ChatConfigModel.KeyAlliesColor, Text = "Allies", Tooltip = 'chat_color' },
{ Key = ChatConfigModel.KeyPrivColor, Text = "Private", Tooltip = 'chat_color' },
{ Key = ChatConfigModel.KeyLinkColor, Text = "Links", Tooltip = 'chat_color' },
{ Key = ChatConfigModel.KeyNotifyColor, Text = "Notify", Tooltip = 'chat_color' },
}

local CheckboxDefs = {
{ Key = ChatConfigModel.KeySendType, Text = "Default recipient: allies", Tooltip = 'chat_send_type' },
{ Key = ChatConfigModel.KeyFeedBackground, Text = "Show feed background", Tooltip = 'chat_feed_background' },
{ Key = ChatConfigModel.KeyLinks, Text = "Show camera links", Tooltip = 'chat_filter' },
}
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

Hardcoded English UI strings — inconsistent with the PR's localization story.

The PR objectives call out localized system/AI/event messages, but every label/title/button text in this dialog is a raw English string ("Chat Configuration", "Message Colors", "All", "Allies", "Font Size: 14", "Apply", "OK", "Cancel", etc.). Wrap user-visible strings with <LOC ...>/LOC so they go through the localization pipeline like the rest of the UI.

Also worth noting: the slider preview labels use string.format("Font Size: %d", value) etc., which won't survive translation either — the formatted prefix needs to come from a <LOC> token with the value interpolated.

Also applies to: 97-99, 106-106, 133-133, 148-148, 163-163, 179-179, 195-195, 216-216, 222-222, 225-225, 231-231

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lua/ui/game/chat/config/ChatConfigInterface.lua` around lines 39 - 51, The UI
strings in ColorDefs and CheckboxDefs (e.g., Text values
"All","Allies","Private","Links","Notify" and "Default recipient: allies","Show
feed background","Show camera links") are hardcoded English; wrap every
user-visible Text/Tooltip and button/label strings referenced by
ChatConfigInterface (including slider preview labels generated with
string.format) with the localization token pattern <LOC ...>...LOC so they go
through the localization pipeline, and replace any string.format("Font Size:
%d", value) style formatting with a localized format token (e.g., a <LOC> token
that includes a placeholder) so the numeric value is interpolated via the
localization-aware formatting; update the entries in ColorDefs and CheckboxDefs
and the slider preview/label code paths accordingly (use the same unique
keys/identifiers currently used in the diff so callers like the slider preview
and any UI builders pick up the localized strings).

Comment thread lua/ui/game/chat/config/ChatConfigInterface.lua
@Garanas Garanas added ui: chat window related to chatting with players area: ui Anything to do with the User Interface of the Game labels Apr 27, 2026
Copy link
Copy Markdown
Contributor

@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.

🧹 Nitpick comments (1)
lua/aibrains/components/ChatBrainComponent.lua (1)

101-115: Consider validating the payload before relaying.

SendChatTo only checks that text is a non-empty string before calling ChatUtils.RelayChatMessage, which (unlike the UI-originated ChatUtils.SendChatMessage) does not run ChatPayload.IsValidPayload. As a result, an AI/campaign caller can leak a payload into Sync.ChatMessages that the shared validator was designed to reject:

  • text longer than ChatPayload.MaxMessageLength (200 UTF‑8 chars) — silently passes.
  • to other than 'all' | 'allies' | integer (e.g., 'notify', a typo, or a stale recipient table) — silently dropped by IsLocalRecipient, with no feedback to the caller.
  • args passed as a non-table, or location as a non-table — these would normally be rejected by IsValidPayload, but here they reach the UI's LOCF/camera-link handlers and can crash on click.

Since RelayChatMessage is documented as the shared sim-side entry point with policy enforced "in exactly one place", running ChatPayload.IsValidPayload(msg) here (or moving the check into RelayChatMessage) would close the gap and give AI authors a deterministic failure mode rather than a silently dropped or malformed line.

♻️ Suggested change
+local ChatPayload = import("/lua/shared/ChatPayload.lua")
+
 ...
     SendChatTo = function(self, to, text, args, location)
         if type(text) ~= 'string' or text == '' then return end

         local msg = {
             Chat     = true,
             to       = to,
             text     = text,
             Args     = args,
             From     = self:GetArmyIndex(),
             location = location,
         }
         msg.Id = tostring(msg)

+        if not ChatPayload.IsValidPayload(msg) then
+            WARN(string.format("AIChatBrainComponent: dropped malformed chat from army %s (to=%s, len=%d)",
+                tostring(msg.From), tostring(to), STR_Utf8Len(text)))
+            return
+        end
+
         ChatUtils.RelayChatMessage(msg)
     end,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lua/aibrains/components/ChatBrainComponent.lua` around lines 101 - 115,
SendChatTo currently only checks that text is a non-empty string and then calls
ChatUtils.RelayChatMessage, which lets invalid chat payloads reach
Sync.ChatMessages; call ChatPayload.IsValidPayload(msg) inside SendChatTo (or
move the check into ChatUtils.RelayChatMessage) before relaying, and if it
returns false reject the message (log a descriptive error including
ChatPayload.MaxMessageLength and the offending fields like to/Args/location) so
malformed payloads (bad recipient values that IsLocalRecipient would drop,
non-table Args/location, or overly long text) are caught and not sent to
RelayChatMessage/LOCF.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@lua/aibrains/components/ChatBrainComponent.lua`:
- Around line 101-115: SendChatTo currently only checks that text is a non-empty
string and then calls ChatUtils.RelayChatMessage, which lets invalid chat
payloads reach Sync.ChatMessages; call ChatPayload.IsValidPayload(msg) inside
SendChatTo (or move the check into ChatUtils.RelayChatMessage) before relaying,
and if it returns false reject the message (log a descriptive error including
ChatPayload.MaxMessageLength and the offending fields like to/Args/location) so
malformed payloads (bad recipient values that IsLocalRecipient would drop,
non-table Args/location, or overly long text) are caught and not sent to
RelayChatMessage/LOCF.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 632b0887-8395-45c0-b167-ebbd8a39aaba

📥 Commits

Reviewing files that changed from the base of the PR and between 9002d91 and 60bd41d.

📒 Files selected for processing (3)
  • lua/ChatUtils.lua
  • lua/aibrain.lua
  • lua/aibrains/components/ChatBrainComponent.lua
🚧 Files skipped from review as they are similar to previous changes (1)
  • lua/aibrain.lua

@Garanas
Copy link
Copy Markdown
Member Author

Garanas commented Apr 28, 2026

@BlackYps I mentioned in the pull request description that this is primarily an experiment with Claude, but that the output in my opinion is a decent improvement on the status quo.

The experiment is done on my end. I won't process further feedback until the game team is sure whether they want this alternative implementation of the chat window. Beyond perhaps excessive comments, I think in general this setup is much more maintainable then the current spaghetti code. But the choice is all yours.

Please let me know what the game team wants in the upcoming weeks. If the answer is no, then feel free to close it. I won't process any additional feedback until the game team decided to want to proceed.

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

Labels

area: ui Anything to do with the User Interface of the Game ui: chat window related to chatting with players

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant