Skip to content

Add moderation exception to self destruct#7132

Open
Garanas wants to merge 6 commits into
developfrom
feature/moderation-self-destruct-rule
Open

Add moderation exception to self destruct#7132
Garanas wants to merge 6 commits into
developfrom
feature/moderation-self-destruct-rule

Conversation

@Garanas

@Garanas Garanas commented Jun 2, 2026

Copy link
Copy Markdown
Member

Description of the proposed changes

Adds the exception that when you self destruct a selection with one or more ACUs it will only self destruct the ACUs.

Testing done on the proposed changes

Launched the game and verified that the ACU is isolated when I try to self destruct all my units.

Additional context

For this fortunate enough to view it:

Checklist

Summary by CodeRabbit

  • Bug Fixes
    • In Full Share mode, self-destructing a selection now targets only Command Units if present, rather than all selected units.
    • Added validation and permission checks for self-destruct commands.

@github-actions github-actions Bot marked this pull request as draft June 2, 2026 17:30
@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds an EmmyLua type for the self-destruct data and replaces the direct callback alias with a validating wrapper that enforces owner authorization, rejects empty selections, and in FullShare mode restricts the action to COMMAND (ACU) units when present before delegating to the core self-destruct function.

Changes

Self-Destruct Callback Validation

Layer / File(s) Summary
Type definition and parameter annotation
lua/selfdestruct.lua
ToggleSelfDestructData EmmyLua type is added (owner, noDelay), and ToggleSelfDestruct's data parameter annotation references this named type.
Callback wrapper with validation and FullShare filtering
lua/SimCallbacks.lua, changelog/snippets/features.7132.md
Callbacks.ToggleSelfDestruct becomes a validating wrapper: it early-returns on missing/empty units, requires data.owner and OkayToMessWithArmy, narrows to categories.COMMAND when ScenarioInfo.Options.Share == "FullShare" (uses filtered set if non-empty), and forwards to selfdestruct.ToggleSelfDestruct. Changelog documents ACU-only behavior in FullShare.

Sequence Diagram(s)

sequenceDiagram
  participant Caller as Callbacks.ToggleSelfDestruct
  participant Scenario as ScenarioInfo
  participant Filter as CategoryFilter
  participant SD as selfdestruct.ToggleSelfDestruct

  Caller->>Scenario: read Options.Share
  Caller->>Filter: filter units by categories.COMMAND
  Note over Filter,Scenario: if FullShare and filtered non-empty -> use filtered\nelse -> use original units
  Caller->>SD: ToggleSelfDestruct(data, selectedUnits)
  SD-->>Caller: done
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

area: sim

Suggested reviewers

  • speed2CZ
  • BlackYps
  • clyfordv

Poem

A rabbit inspects the battlefield bright,
Ensuring owners and choices are right,
In FullShare it finds ACUs to defend,
Toggles with care from beginning to end,
Hopping home happy — safe logic in sight! 🐰

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title is vague and does not clearly describe the main change; 'moderation exception' is unclear without context and does not convey what the actual functionality is. Revise the title to clearly describe the change, e.g., 'Isolate ACUs in self-destruct when in full share mode' or 'Filter self-destruct to ACUs only when selection contains command units'.
✅ Passed checks (4 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description covers all required sections: clear description of changes, testing methodology, and additional context. All checklist items are marked complete.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/moderation-self-destruct-rule

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

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@lua/SimCallbacks.lua`:
- Around line 185-207: The callback dereferences units and data.owner before
validation; normalize and validate them up front by calling SecureUnits(units)
into a local variable (e.g., local units = SecureUnits(units)) before any
table.getn or EntityCategoryFilterDown calls, and check that data is non-nil and
data.owner exists (if not, return) rather than accessing data.owner earlier;
then use the normalized units variable in the rest of the function (including
the FullShare branch and the final
import("/lua/selfdestruct.lua").ToggleSelfDestruct calls) to avoid crashes on
malformed payloads.
🪄 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: 37d00683-45e8-4e3f-998e-b074075b3cc6

📥 Commits

Reviewing files that changed from the base of the PR and between f8cbb67 and 469057a.

📒 Files selected for processing (2)
  • lua/SimCallbacks.lua
  • lua/selfdestruct.lua

Comment thread lua/SimCallbacks.lua
@Garanas Garanas marked this pull request as ready for review June 2, 2026 18:59
@Garanas Garanas requested a review from BlackYps June 2, 2026 19:02

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
lua/SimCallbacks.lua (1)

185-207: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalize units before any table.getn/forwarding.

Line 186 still dereferences raw callback input, so a singleton unit/id or any other non-table payload will throw before this wrapper can return safely. SecureUnits already canonicalizes and filters the selection; normalize once up front and reuse that sanitized table in both the FullShare branch and the fallback call.

Suggested fix
 Callbacks.ToggleSelfDestruct = function(data, units)
-    -- prevent malformed input
-    if (not units) or (table.getn(units) == 0) then
+    -- prevent malformed input
+    units = SecureUnits(units)
+    if TableEmpty(units) then
         return
     end
 
     -- prevent abuse
     if (not data) or (not data.owner) or (not OkayToMessWithArmy(data.owner)) then
@@
     -- moderation rule: if you self destruct with one or more ACUs in the selection when playing full 
     -- share, then you only self destruct the ACUs. This does not make it impossible to abuse, but it 
     -- does introduce a simple guardrail. 
     if ScenarioInfo.Options.Share == "FullShare" then
-        local commandUnits = EntityCategoryFilterDown(categories.COMMAND, SecureUnits(units))
+        local commandUnits = EntityCategoryFilterDown(categories.COMMAND, units)
         if table.getn(commandUnits) > 0 then
             import("/lua/selfdestruct.lua").ToggleSelfDestruct(data, commandUnits)
             return
         end
     end
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lua/SimCallbacks.lua` around lines 185 - 207, Normalize the incoming
selection by calling SecureUnits(units) once at the top (e.g., replace raw uses
of units with a local sanitized = SecureUnits(units)), then use that sanitized
table for the table.getn check and for all calls to
import("/lua/selfdestruct.lua").ToggleSelfDestruct; ensure you perform the early
return when the sanitized table is empty and still validate
data.owner/OkayToMessWithArmy(data.owner) before invoking ToggleSelfDestruct,
and use the same sanitized variable in the ScenarioInfo.Options.Share branch
(commandUnits = EntityCategoryFilterDown(categories.COMMAND, sanitized)).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@lua/SimCallbacks.lua`:
- Around line 185-207: Normalize the incoming selection by calling
SecureUnits(units) once at the top (e.g., replace raw uses of units with a local
sanitized = SecureUnits(units)), then use that sanitized table for the
table.getn check and for all calls to
import("/lua/selfdestruct.lua").ToggleSelfDestruct; ensure you perform the early
return when the sanitized table is empty and still validate
data.owner/OkayToMessWithArmy(data.owner) before invoking ToggleSelfDestruct,
and use the same sanitized variable in the ScenarioInfo.Options.Share branch
(commandUnits = EntityCategoryFilterDown(categories.COMMAND, sanitized)).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6657cad3-a08f-4d2d-a15e-429c81770c8d

📥 Commits

Reviewing files that changed from the base of the PR and between 469057a and 521e09e.

📒 Files selected for processing (3)
  • changelog/snippets/features.7132.md
  • changelog/snippets/graphics.7095.md
  • lua/SimCallbacks.lua
✅ Files skipped from review due to trivial changes (1)
  • changelog/snippets/features.7132.md

Comment thread lua/SimCallbacks.lua
end

-- prevent abuse
if (not data) or (not data.owner) or (not OkayToMessWithArmy(data.owner)) then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Using user-defined Args in the data table cannot be relied upon for security.

When I look into it further, the verification for this function is ending up a bit all over the place:

  1. data.owner is verified with OkayToMessWithArmy in this callback and the import function.
  2. data.owner is confirmed to not be -1 (observer) in the import function. Which I guess prevents observers from self-destructing units in cheat games? But data.owner is GetFocusArmy from UI side, and observers can't select units so they can't trigger the callback with any selection to act upon while also sending an owner of -1.
  3. units is checked with SecureUnits, which loops over units and checks OkayToMessWithArmy(unit.Army) (it also has a part converting entity ids into entities but that isn't relevant here). This is all sim-side info and the units list is from the engine UI code, so it is secure. The issue is the duplication where this OkayToMessWithArmy(unit.Army) is used in your category filter, and the imported function (twice at that).

Overall, the import function has the final results and always checks OkayToMessWithArmy(unit.Army), so it works, but it seems like the rest of the checks can be replaced. data.owner can instead be an early exit using GetCommandSourceArmy or GetFocusArmy sim-side. Checks for OkayToMessWithArmy(unit.Army) can be replaced with SecureUnits in the main callback function replacing the units variable, and then sending it to the imported function.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is just me pulling the logic from the SelfDestruct function to the boundary, see also lines 35: if data.owner ~= -1 and OkayToMessWithArmy(data.owner) then.

I personally do not think that the units list needs to be checked, since it's the engine that appends it. Unless we also want to guard against hacked clients, where you can append arbitrary units.

Note that I intentionally did not mess with the existing functionality, since that's not what was requested.

Comment thread lua/SimCallbacks.lua
Comment on lines +195 to +204
-- moderation rule: if you self destruct with one or more ACUs in the selection when playing full
-- share, then you only self destruct the ACUs. This does not make it impossible to abuse, but it
-- does introduce a simple guardrail.
if ScenarioInfo.Options.Share == "FullShare" then
local commandUnits = EntityCategoryFilterDown(categories.COMMAND, SecureUnits(units))
if table.getn(commandUnits) > 0 then
import("/lua/selfdestruct.lua").ToggleSelfDestruct(data, commandUnits)
return
end
end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is trivial to bypass with UI mods splitting the user's order into two callback calls, so why not implement it in the UI layer? Then it's easy to have a handler with proper user feedback.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It's also trivial to identify that this happened when parsing the replay. Malicious intent is then confirmed, easier to ban in my point of view.

You're free to also implement something in the UI layer, by all means.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants