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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions lua/SimCallbacks.lua
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,38 @@ do

end

do

---@param data { Enable: boolean, ShowMsg: boolean }
---@param selection Unit[]
Callbacks.ForceReverseMove = function(data, selection)
-- verify selection
if not data.Units then
selection = SecureUnits(selection)
else
selection = SecureUnits(data.Units)
Copy link
Member

Choose a reason for hiding this comment

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

Would it not be better to always use the current selection? That way you don't even need to use the SecureUnits function as far as I am aware.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There are cases when we don't need all selected units but only part of them (exlcude units without MaxSpeedReverse or with MaxSpeedReverse = 0). I do this check on UI and sim side just in case. Theoretically we can do this check only in sim and just use current selection, idk what is better

end

if (not selection) or TableEmpty(selection) then
return
end

for k, unit in selection do
if unit.Blueprint.Physics.MaxSpeedReverse and unit.Blueprint.Physics.MaxSpeedReverse > 0 then
unit:ForceReverseMove(data.Enable)
end
end

if data.ShowMsg then
if data.Enable == true then
print(string.format("Force reverse move ENABLED for %d units", table.getn(selection)))
else
print(string.format("Force reverse move DISABLED for %d units", table.getn(selection)))
end
Comment on lines +831 to +856
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

Count only reverse-capable units in the status message.
The log currently reports the full selection size even when some units lack MaxSpeedReverse, which can be misleading. Track the affected count instead.

🐛 Proposed fix
-        for k, unit in selection do
+        local affected = 0
+        for k, unit in selection do
             if unit.Blueprint.Physics.MaxSpeedReverse and unit.Blueprint.Physics.MaxSpeedReverse > 0 then
                 unit:ForceReverseMove(data.Enable)
+                affected = affected + 1
             end
         end

         if data.ShowMsg then
             if data.Enable == true then
-                print(string.format("Force reverse move ENABLED for %d units", table.getn(selection)))
+                print(string.format("Force reverse move ENABLED for %d units", affected))
             else
-                print(string.format("Force reverse move DISABLED for %d units", table.getn(selection)))
+                print(string.format("Force reverse move DISABLED for %d units", affected))
             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
---@param data { Enable: boolean, ShowMsg: boolean }
---@param selection Unit[]
Callbacks.ForceReverseMove = function(data, selection)
-- verify selection
if not data.Units then
selection = SecureUnits(selection)
else
selection = SecureUnits(data.Units)
end
if (not selection) or TableEmpty(selection) then
return
end
for k, unit in selection do
if unit.Blueprint.Physics.MaxSpeedReverse and unit.Blueprint.Physics.MaxSpeedReverse > 0 then
unit:ForceReverseMove(data.Enable)
end
end
if data.ShowMsg then
if data.Enable == true then
print(string.format("Force reverse move ENABLED for %d units", table.getn(selection)))
else
print(string.format("Force reverse move DISABLED for %d units", table.getn(selection)))
end
---@param data { Enable: boolean, ShowMsg: boolean }
---@param selection Unit[]
Callbacks.ForceReverseMove = function(data, selection)
-- verify selection
if not data.Units then
selection = SecureUnits(selection)
else
selection = SecureUnits(data.Units)
end
if (not selection) or TableEmpty(selection) then
return
end
local affected = 0
for k, unit in selection do
if unit.Blueprint.Physics.MaxSpeedReverse and unit.Blueprint.Physics.MaxSpeedReverse > 0 then
unit:ForceReverseMove(data.Enable)
affected = affected + 1
end
end
if data.ShowMsg then
if data.Enable == true then
print(string.format("Force reverse move ENABLED for %d units", affected))
else
print(string.format("Force reverse move DISABLED for %d units", affected))
end
🤖 Prompt for AI Agents
In `@lua/SimCallbacks.lua` around lines 831 - 856, In Callbacks.ForceReverseMove,
the status message currently prints table.getn(selection) even for units that
lack a reverse speed; update the function to track a counter (e.g.,
affectedCount) and increment it only when you actually call
unit:ForceReverseMove (i.e., when unit.Blueprint.Physics.MaxSpeedReverse and >
0); then use that affectedCount in the data.ShowMsg print statements (respecting
data.Enable) so the log reflects only the units that were modified rather than
the full selection.

end
end
end

--#endregion


Expand Down
10 changes: 9 additions & 1 deletion lua/keymap/keyactions.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1653,7 +1653,15 @@ local keyActionsOrdersAdvanced = {
['shift_discharge'] = {
action = 'UI_Lua import("/lua/keymap/misckeyactions.lua").DischargeShields()',
category = 'ordersAdvanced',
}
},
['reverse_move'] = {
action = 'UI_Lua import("/lua/keymap/misckeyactions.lua").StartReverseMoveCommandMode()',
category = 'ordersAdvanced',
},
['shift_reverse_move'] = {
action = 'UI_Lua import("/lua/keymap/misckeyactions.lua").StartReverseMoveCommandMode()',
category = 'ordersAdvanced',
},
}

local keyActionsOrdersQueueBased = {
Expand Down
2 changes: 2 additions & 0 deletions lua/keymap/keydescriptions.lua
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,8 @@ keyDescriptions = {
['shift_load_transports_clear'] = '<LOC key_desc_shift_load_transports_clear>Load into transports. Applies immediately',
['copy_orders'] = '<LOC key_desc_copy_orders>Copy orders of the unit the mouse is on top of',
['shift_copy_orders'] = '<LOC key_desc_shift_copy_orders>Copy orders of the unit the mouse is on top of',
['reverse_move'] = '<LOC key_desc_reverse_move>Reverse move',
['shift_reverse_move'] = '<LOC key_desc_shift_reverse_move>Reverse move',

['select_surface_bombers'] = '<LOC key_desc_0407>Select all Bombers (Normal)',
['select_torpedo_bombers'] = '<LOC key_desc_0408>Select all Bombers (Torpedo)',
Expand Down
5 changes: 5 additions & 0 deletions lua/keymap/misckeyactions.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
local Prefs = import("/lua/user/prefs.lua")
local SelectionUtils = import("/lua/ui/game/selection.lua")
local SetIgnoreSelection = import("/lua/ui/game/gamemain.lua").SetIgnoreSelection
local StartCommandMode = import("/lua/ui/game/commandmode.lua").StartCommandMode

local lockZoomEnable = false
function lockZoom()
Expand Down Expand Up @@ -557,6 +558,10 @@ AIPlatoonSimpleStructureBehavior = function()
SimCallback({ Func = 'AIPlatoonSimpleStructureBehavior', Args = {} }, true)
end

function StartReverseMoveCommandMode()
StartCommandMode("order", {name = "RULEUCC_Move", isReverseMove = true})
end

StoreCameraPosition = function()
local camera = GetCamera('WorldCamera')
local settings = camera:SaveSettings()
Expand Down
1 change: 1 addition & 0 deletions lua/skins/skins.lua
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ skins = {
RULEUCC_Teleport = {'/textures/ui/common/game/cursors/transport.dds', 15, 15},
RULEUCC_Script = {'/textures/ui/common/game/cursors/attack.dds', 15, 15},
RULEUCC_Invalid = {'/textures/ui/common/game/cursors/attack-invalid.dds', 15, 15},
RULEUCC_ReverseMove = {'/textures/ui/common/game/cursors/reverse_move-.dds', 15, 15, 12, 12},
Copy link

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:

# Verify the reverse-move cursor DDS exists in the repo
fd -a 'reverse_move-.dds' .

Repository: FAForever/fa

Length of output: 38


🏁 Script executed:

# Search for CursorType definition to understand the typing context
rg -i "CursorType" --type lua

Repository: FAForever/fa

Length of output: 138


🏁 Script executed:

# Get context around line 204 in the target file
sed -n '190,220p' lua/skins/skins.lua

Repository: FAForever/fa

Length of output: 2821


🏁 Script executed:

# Find the CursorType alias definition in skins.lua
rg -A 50 '---@alias CursorType' lua/skins/skins.lua

Repository: FAForever/fa

Length of output: 477


🏁 Script executed:

# Find the CursorType alias definition in skins.lua with proper escaping
rg -A 50 '@alias CursorType' lua/skins/skins.lua

Repository: FAForever/fa

Length of output: 1306


Add the missing reverse_move-.dds asset and update the CursorType alias.
The cursor asset /textures/ui/common/game/cursors/reverse_move-.dds does not exist in the repository; without it, the cursor will fail to render. Additionally, add "RULEUCC_ReverseMove" to the CursorType alias definition in lua/skins/skins.lua for type consistency with other cursor entries.

🤖 Prompt for AI Agents
In `@lua/skins/skins.lua` at line 204, Add the missing cursor asset file at
/textures/ui/common/game/cursors/reverse_move-.dds (matching the naming used in
RULEUCC_ReverseMove) and commit it so the skin entry RULEUCC_ReverseMove has a
real texture to load; then update the CursorType alias in lua/skins/skins.lua to
include "RULEUCC_ReverseMove" alongside the other cursor names to keep the type
definition consistent with the RULEUCC_* entries.

COORDINATED_ATTACK = {'/textures/ui/common/game/cursors/attack_coordinated.dds', 15, 15},
MESSAGE = {'/textures/ui/common/game/cursors/message-.dds', 15, 15, 11, 12},
BUILD = {'/textures/ui/common/game/cursors/selectable-.dds', 2, 2, 7, 12},
Expand Down
22 changes: 22 additions & 0 deletions lua/ui/controls/worldview.lua
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,9 @@ local orderToCursorCallback = {
RULEUCC_Script = 'OnCursorScript',
RULEUCC_Invalid = 'OnCursorInvalid',
RULEUCC_CallTransport = 'OnCursorCallTransport',

-- A fake RULEUCC for cursor rendering. Actual CM in commandmode.lua is still RULEUCC_Move
RULEUCC_ReverseMove = 'OnCursorReverseMove',

-- misc
CommandHighlight = 'OnCursorCommandHover',
Expand Down Expand Up @@ -392,6 +395,10 @@ WorldView = ClassUI(moho.UIWorldView, Control, WorldViewShapeComponent, WorldVie
if order == 'RULEUCC_Attack' then
order = 'RULEUCC_AttackGround'
end

if command_data.isReverseMove then
order = 'RULEUCC_ReverseMove'
end
-- 2. then command highlighting
elseif self:HasHighlightCommand() then
order = 'CommandHighlight'
Expand Down Expand Up @@ -526,6 +533,21 @@ WorldView = ClassUI(moho.UIWorldView, Control, WorldViewShapeComponent, WorldVie
self:EnableIgnoreMode(false)
end
end,

--- Called when the order `RULEUCC_ReverseMove` is being applied
---@param self WorldView
---@param identifier 'RULEUCC_ReverseMove'
---@param enabled boolean
---@param changed boolean
OnCursorReverseMove = function(self, identifier, enabled, changed)
if enabled then
if changed then
local cursor = self.Cursor
cursor[1], cursor[2], cursor[3], cursor[4], cursor[5] = UIUtil.GetCursor(identifier)
self:ApplyCursor()
end
end
Comment on lines +537 to +549
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

Align reverse-move cursor with Move ignore-mode behavior.
OnCursorMove toggles EnableIgnoreMode on enter/exit; reverse move should mirror this to avoid inconsistent click handling.

🐛 Proposed fix
     OnCursorReverseMove = function(self, identifier, enabled, changed)
         if enabled then
             if changed then
                 local cursor = self.Cursor
                 cursor[1], cursor[2], cursor[3], cursor[4], cursor[5] = UIUtil.GetCursor(identifier)
                 self:ApplyCursor()
+                self:EnableIgnoreMode(true)
             end
         else
+            self:EnableIgnoreMode(false)
         end
     end,
🤖 Prompt for AI Agents
In `@lua/ui/controls/worldview.lua` around lines 537 - 549, OnCursorReverseMove
currently updates the cursor but doesn't toggle the ignore-mode like
OnCursorMove does, causing inconsistent click handling; update
OnCursorReverseMove to mirror OnCursorMove's enter/exit behavior by calling the
same EnableIgnoreMode toggle (or method/property used in OnCursorMove) when
enabled changes, so when the reverse-move cursor is applied you set
EnableIgnoreMode on entry and clear it on exit, then continue to update the
cursor and call self:ApplyCursor(); reference OnCursorReverseMove, OnCursorMove,
and the EnableIgnoreMode toggle used in the existing code.

end,

--- Called when the order `RULEUCC_Guard` is being applied
---@param self WorldView
Expand Down
73 changes: 72 additions & 1 deletion lua/ui/game/commandmode.lua
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ local issuedOneCommand = false
local startBehaviors = {}
local endBehaviors = {}

local reverseMoveCMIsActive = false
local ReverseMoveEnabledUnits = {}

--- Callback triggers when command mode starts
---@param behavior fun(mode?: CommandMode, data?: CommandModeData)
---@param identifier? string
Expand Down Expand Up @@ -181,6 +184,10 @@ function StartCommandMode(newCommandMode, data)
if commandMode then
EndCommandMode(true)
end

if data.isReverseMove then
reverseMoveCMIsActive = true
end

-- update our local state
commandMode = newCommandMode
Expand All @@ -198,7 +205,9 @@ function EndCommandMode(isCancel)
if ignoreSelection then
return
end


reverseMoveCMIsActive = false

-- in case we want to end the command mode, without knowing it has already ended or not
if modeData then
-- add information to modeData for end behavior
Expand Down Expand Up @@ -644,6 +653,62 @@ local function OnStopIssued(command)
EnhancementQueueFile.clearEnhancements(command.Units)
end

local function EnableReverseMove(command)
local EnableReverseMoveFor = {}

for _,unit in command.Units do
local bp = unit:GetBlueprint()
if bp.Physics.MaxSpeedReverse and bp.Physics.MaxSpeedReverse > 0 then
local commandQueue = unit:GetCommandQueue()
local queueLength = TableGetN(commandQueue)
local entityID = unit:GetEntityId()

-- make reverse move a single command without being able to queue it or combine with other commands
-- this is the easiest way to avoid making some complex tracking system for the queue that
-- contains different orders including reverse move.
if queueLength > 1 then
for k, cmd in commandQueue do
if k ~= queueLength then
DeleteCommand(cmd.ID)
end
end
end

ReverseMoveEnabledUnits[entityID] = unit
TableInsert(EnableReverseMoveFor, entityID)
end
end

local cb = { Func = 'ForceReverseMove', Args = { Enable = true, ShowMsg = false, Units = EnableReverseMoveFor } }
SimCallback(cb, false)
end

-- If there are any units on the map with ReverseMove enabled (ReverseMoveEnabledUnits is not empty)
-- then we have to check if these units are in the command.Units list every time OnCommandIssued is triggered (without reverseMoveCMIsActive)
-- and if yes - disable reverse move for them via SimCallback.
local function DisableReverseMove(command)
-- remove dead units
for k, u in ReverseMoveEnabledUnits do
if u:IsDead() then
ReverseMoveEnabledUnits[k] = nil
end
end

local disableReverseMoveFor = {}
for _,unit in command.Units do
local id = unit:GetEntityId()
if ReverseMoveEnabledUnits[id] then
TableInsert(disableReverseMoveFor, id)
ReverseMoveEnabledUnits[id] = nil
end
end

if not TableEmpty(disableReverseMoveFor) then
local cb = { Func = 'ForceReverseMove', Args = { Enable = false, ShowMsg = false, Units = disableReverseMoveFor} }
SimCallback(cb, false)
end
end

-- Callbacks for different command types, nil values for reference to functions that don't exist yet
local OnCommandIssuedCallback = {
None = nil,
Expand Down Expand Up @@ -691,6 +756,12 @@ function OnCommandIssued(command)
issuedOneCommand = true
end

if reverseMoveCMIsActive then
EnableReverseMove(command)
elseif not TableEmpty(ReverseMoveEnabledUnits) then
DisableReverseMove(command)
end

-- If our callback returns true or we don't have a command type, we skip the rest of our logic
if (OnCommandIssuedCallback[command.CommandType] and OnCommandIssuedCallback[command.CommandType](command))
or command.CommandType == 'None' then
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.