Skip to content

move /take to LUA#5446

Closed
keithharvey wants to merge 34 commits into
beyond-all-reason:masterfrom
keithharvey:move_take
Closed

move /take to LUA#5446
keithharvey wants to merge 34 commits into
beyond-all-reason:masterfrom
keithharvey:move_take

Conversation

@keithharvey

@keithharvey keithharvey commented Jul 1, 2025

Copy link
Copy Markdown
Collaborator

Required Engine PR

beyond-all-reason/RecoilEngine#2423

Work done

  • Create cmd_take.lua gadget implementing /take command functionality
  • Move all give logic into cmd_give
  • ChangeTeam gets a new reason parameter, mostly managed by a LUA enum which is comprehensive, with a subset of functionality the engine handled defined in a new enum
  • AllowUnitTransfer gets this reason from the engine pass through, allowing more sensible handling of the parameter beyond "capture". For example, this allows fixing the /take bug in sharing code easily
  • some code cleanup/fixes

Test steps

The /take command now works reliably in skirmish mode, transferring units and resources from inactive AI teams to human players without Lua errors

@keithharvey keithharvey changed the title feat: Refactor /take command to Lua gadget with comprehensive fixes feat: Refactor /take command to Lua gadget Jul 1, 2025
@keithharvey keithharvey marked this pull request as draft July 1, 2025 06:55
- Create cmd_take.lua gadget implementing /take command functionality
- Transfer units and resources from AI teams to human player teams
- Fix multiple GetUnitCommands errors in unit transfer gadgets:
  * unit_prevent_share_load.lua: Add pcall error handling
  * unit_prevent_share_self_d.lua: Add type checking and pcall
  * unit_prevent_load_hax.lua: Add pcall error handling
- Fix GetTeamResource API usage (use GetTeamResources with select(1, ...))
- Handle playerID=0 case in skirmish mode by finding actual human player
- Add proper error handling for synced context limitations
- Remove debug logging and clean up code

The /take command now works reliably in skirmish mode, transferring units
and resources from inactive AI teams to human players without Lua errors.
@keithharvey keithharvey marked this pull request as ready for review July 3, 2025 02:05
@keithharvey keithharvey changed the title feat: Refactor /take command to Lua gadget feat: Refactor ChangeTeam/AllowUnitTransfer, move /take and /give to LUA Jul 3, 2025
@keithharvey keithharvey changed the title feat: Refactor ChangeTeam/AllowUnitTransfer, move /take and /give to LUA refactor: reason added to ChangeTeam/AllowUnitTransfer, move /take and /give to LUA Jul 3, 2025
@keithharvey keithharvey changed the title refactor: reason added to ChangeTeam/AllowUnitTransfer, move /take and /give to LUA refactor: ChangeTeam/AllowUnitTransfer simplification, move /take and /give to LUA Jul 3, 2025

@sprunk sprunk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Will need changes due to beyond-all-reason/RecoilEngine#2423.

@WatchTheFort WatchTheFort left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why is the gadget global table GG being localized? That table is huge, what benefit are we incurring?

local isSharing = reason == GG.CHANGETEAM_REASON.GIVEN or reason == GG.CHANGETEAM_REASON.IDLE_PLAYER_TAKEOVER or reason == GG.CHANGETEAM_REASON.TAKEN or reason == GG.CHANGETEAM_REASON.SOLD

This is used in a lot of places, can it be refactored to be defined in a single location?

Why are oldTeam == newTeam checks being added in AllowUnitTransfer callins? When that is the case, it makes no difference what the return value is.

Comment thread luarules/gadgets.lua Outdated
}

gadgetHandler.GG.CHANGETEAM_REASON = {
RECLAIMED = 0, -- engine-side

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

How can a unit change teams by being reclaimed?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please refer to Spring.GetSpiritRealmTeamID()

Comment thread luarules/gadgets.lua Outdated
Comment on lines +107 to +108
SOLD = 5, -- market sales
SCAVENGED = 6, -- scavenger transfers

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why do these gadgets need to register custom reasons? Why can't they use a generic base set of reasons?

@sprunk sprunk Jul 29, 2025

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Widgets might want to know the specific reason, e.g. to produce different notifications.

Caution

UNIT LOST TO SCAVENGER CLOUD!!! <click to center>

vs

Note

Unit X bought for Y metal.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

How do new reasons get added? I'm not crazy about gadgets.lua containing functionality related to a specific gadget.

@badosu badosu Aug 6, 2025

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.

gadgetHandler.RegisterGlobal(self, "RegisterUnitChangeTeamReason", fn(owner, reason) ->
  reasons[reason] = owner
  flattenedReasons = table_keys(reasons)
end)
gadgetHandler.RegisterGlobal(self, "UNIT_CHANGE_TEAM_REASONS", fn() -> flattenedReasons end)

That would be one idea. Once offloaded to a gadget it's easier to reason about a reasonable interface.

In fact, the specification above incentivizes a straightforward natural extension for reason watching, e.g.:

local function changedTeamsDueToX(...context)
  doSomethingMyGadgetCaresAboutWhenX(...context)
end

function gadget:Initialize()
  -- alternatively just pass "X"
  RegisterUnitChangedTeams(UNIT_CHANGE_TEAM_REASONS.X, changedTeamsDueToX)
end

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@WatchTheFort there would need to be a functional difference in behavior in game but the current list should be a reductive categorization of existing behaviours, just less obfuscated. I'll move it to a different file.

Comment thread luarules/gadgets.lua Outdated
for _, g in ipairs(self.AllowUnitTransferList) do
if not g:AllowUnitTransfer(unitID, unitDefID, oldTeam, newTeam, capture) then
return false
oldTeam, newTeam, reason)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Put these on the same line as the other parameters, as was done with all the other callins

Comment thread luarules/gadgets.lua Outdated
Comment thread luarules/gadgets.lua
Spring.SetUnitNoSelect(hatID, false)
Spring.TransferUnit(hatID, Spring.GetGaiaTeamID()) -- ( number unitID, numer newTeamID [, boolean given = true ] ) -> nil if given=false, the unit is captured
local px, py, pz = Spring.GetUnitPosition(unitID)
Spring.SetUnitPosition(hatID, px + 32, pz + 32)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why was this call removed?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why is this file part of the PR?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why is this file part of the PR?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why is this file part of the PR?

Comment on lines +3243 to +3245
if WG.Chobby.Take then
WG.Chobby.Take(clickedPlayer.team)
else

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What is this? Why would Chobby have any take functionality?

Comment thread luarules/gadgets.lua Outdated
gadgetHandler.GG.CHANGETEAM_REASON = {
RECLAIMED = 0, -- engine-side
GIVEN = 1, -- engine-side
CAPTURED = 2, -- engine-side

@badosu badosu Aug 6, 2025

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.

I'll avoid extensive review, I'll just give a strong recommendation, feel free to resolve this, I have no stakes except with Recoil consumers having maintainable widget/gadget managers.

Keep in mind this is an opinionated decision to make, one that is based on my experience in my previous life as a BAR contributor and a current Recoil contributor. There is nothing wrong with having manager code being as complex as a game wants, it's game code nevertheless. But there is a strong reason for encapsulating concerns so the codebase is more maintainable and less bug-prone, especially as most BAR contributors avoid touching manager code, due to it's complex nature.


Strongly consider having a gadget to handle taking/given logic instead of having this logic in the manager:

  • Have a gadget with handler = true on a very low layer
  • Move the GG definitions there, alternatively use gadgetHandler.RegisterGlobal(self, "CHANGETEAM_REASON", {...}) instead. If you wish to retain GG use gadgetHandler.GG for the assignment.
  • Have the gadget implement all the callins specifically concerned with taking/giving units. Keep in mind when handler = true the gadgetHandler global is the same gadgetHandler as the one here. Some logic such as having the entry in callinslist registered might need to be kept in the manager, as it might be in the limit of what is reasonable to require in an initial refactoring effort.

There might be some other steps I didn't think of more deeply that require careful introspection and understanding of the gadgetHandler to implement above, but BAR heavily benefits of having a more maintainable gadget manager and I highly recommend trying to understand how it works so the implementation works with it instead of fighting against it.

The other important aspect is that having a first gadget implementing offload of callin concerns also incentivizes further manager level BAR logic to do the same, since there's at least one reference implementation. It also nudges BARs contributor base to attempt developing more maintainable and less bug-prone patterns.

If you wish to attempt this route, I can assist, but I won't specifically be reviewing this PR. I'd encourage to reach me at #lua-scripting in the recoil discord server but I don't mind BAR discord.

Example for illustration purpose

An example of "non-vanilla" manager change in BAR that can/might be offloaded to a gadget: AllowCommand watching. There's more code associated to it, but it's just and example.

An example of "non-vanilla" manager change in BAR that is partly offloaded to a widget: VisibleUnit* callins. This one could still be refactored to not need code in manager but at least it keeps its changes minimal. A tangent argument is that this specific widget should be an unsynced gadget, sending messages to LuaUI, since it's a major part of BARs unsynced infrastructure.

@sprunk sprunk Aug 6, 2025

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm not so sure about handler = true. If callins can be overridden then it can become hard to tell what the actual callout function signature for individual gadgets really is. Transparent wrappers that do stuff but keep args semantics intact when passing to gadgets (such as the wrapper for AllowCommand) are kosher, as are completely new callins (such as VisibleUnitsChanged), but I'd argue that if both the handler and the engine source says the args for some callin are unitID, bool captured but gadgets actually receive unitID, int reason because some obscure gadget override it it's less maintainable than having a few explicit lines directly in the handler that are just not present in the wider Recoil.

@badosu badosu Aug 6, 2025

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.

It is a good argument, but it's not an argument against offloading of ad-hoc handler concerns to addons (gadgets, widgets), it's an argument against having complex callin boilerplate, which is something that BAR already does and probably will keep doing.

Having handler be as thin and transparent as possible between engine -> handler -> addons is a concern that is independent of encapsulation. My proposal is specifically applied to the fact BAR already intends to not have as much of a thin wrapper as possible.

My argument is that if a game has scoped custom logic that deviates from mainstream managers, it's much better to have this custom logic extracted to a place where it's evident what changes exist and contains what exactly are these changes. In fact, the pattern improves understanding of how a game deviates from standard callin handling (be it from other games via discrepancy to basecontent manager or from the engine's own callin signature) by encapsulating the games ad-hoc changes 1.

My argument is that this mostly improves maintainability, if your decision is not to have too thin wrappers anyway. There is a slight con on figuring out where callin dispatch is defined, but perhaps having callin_*.lua filename would help on that regard.

I'd argue that if the handler says the args are unitID, bool captured but gadgets actually receive unitID, int reason because some obscure gadget override the callin it's less maintainable than having a few explicit lines directly in the handler that are just not present in the wider Recoil.

Sure, but that is also unrelated to encapsulation. Nothing precludes sensible consideration of callin signature on offloaded callin pattern. Unexpected signature can be done on manager code or on offloaded callin gadget code, it's not an intrinsic problem with the pattern.

Footnotes

  1. A practical example: BAR makes extensive use of lua language server annotations. The encapsulated logic assists in defining the types gadgets are expected to have defined in their scope as well, instead of having them spread around in the manager code, in a way conducive to bugs.

@keithharvey keithharvey Aug 6, 2025

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I think this is a good time for a functional approach to this problem. I am currently deprecating the Spring side where appropriate and moving everything that touches CHANGE_REASON (including capture), but this is a good time to code what you're talking about in exactly 1 place based on reason.

I want to also make a permanently open (or deprecation?) PR cleaning the dead code, for when we eventually decide it's time to end the deprecation, as part of this PR. With that we can make a test grid for (1) new lua implementation, with updated engine (2) bar on master with engine running fallbacks (3) post-deprecation removals everything works fine/our lua implementation is guaranteed complete

@keithharvey keithharvey changed the title refactor: ChangeTeam/AllowUnitTransfer simplification, move /take and /give to LUA refactor: team transfer gadget Aug 7, 2025
Comment thread luarules/gadgets.lua
Daniel Harvey and others added 3 commits August 11, 2025 11:46
- Refactor unit sharing system into three modes: enabled, t2cons, disabled
- Fix /take command blockage when sharing is restricted
- Decouple Unit Market transfers from sharing restrictions
- Remove redundant unit transfer check from tax resource sharing

Addresses Issue beyond-all-reason#4416 and incorporates PR feedback.
keithharvey and others added 19 commits August 11, 2025 13:40
… manager examples

Co-Authored-By: Keith Harvey <keithdanielharvey@gmail.com>
cleanup: remove auto-generated files and examples
- Add ReclaimIncome module for processing reclaim income through Lua
- Create command_pipeline_handler gadget to handle SyncedActionFallback messages
- Implement mod_command_validation gadget for tax policy registration
- Engine will call Lua for reclaim income processing instead of blocking commands
- Allows players to reclaim allied wrecks while still applying tax to income
- More elegant solution than blocking commands entirely

Co-Authored-By: Keith Harvey <keithdanielharvey@gmail.com>
- Remove AllowCommand blocking from mod_resource_tax.lua
- Remove AllowCommand blocking from mod_allied_construction.lua
- Tax now applied at income level via SYNCED_ACTION_FALLBACK instead of command blocking
- Players can now reclaim allied wrecks while tax is still applied to income
- Cleaner separation between command validation and income taxation

Co-Authored-By: Keith Harvey <keithdanielharvey@gmail.com>
Co-Authored-By: Keith Harvey <keithdanielharvey@gmail.com>
…l breakdown

- Create mod_allied_assist.lua with AllowCommand functionality for RECLAIM/GUARD/BUILD
- Move command restrictions from tax module to allied assist module
- Add mod_reclaim.lua for reclaim-specific policies per gist structure
- Remove mod_allied_construction.lua (functionality moved to mod_allied_assist.lua)
- Remove mod_command_validation.lua (duplicate functionality)
- Follow gist factoring: Resources (tax) separate from Allied Construction (commands)

Co-Authored-By: Keith Harvey <keithdanielharvey@gmail.com>
…ns access

- Replace direct Spring.GetModOptions() calls with encapsulated configuration functions
- Update mod_resource_tax.lua to use config object instead of global variables
- Apply consistent pattern across all team_transfer modules for better maintainability
- Maintain CommandPipeline system usage while properly encapsulating mod options access

Co-Authored-By: Keith Harvey <keithdanielharvey@gmail.com>
- Rename mod_mex_upgrades.lua to mod_structure_upgrades.lua
- Add support for geothermal upgrade ownership transfer policies
- Implement unified structure detection for both mex and geothermal units
- Add tier detection for geothermal units based on energy output and cost
- Maintain backward compatibility with mex_upgrade_ownership mod option
- Support structure_upgrade_ownership as the new preferred mod option name
- Expand value calculation to handle both mex and geothermal structures
- Update all function names and variables to use generic 'structure' terminology

Co-Authored-By: Keith Harvey <keithdanielharvey@gmail.com>
- Add category checking methods (isMetalExtractor, isGeothermal) to blueprint API
- Add tier detection method (getUnitTier) using category hierarchy
- Update mod_structure_upgrades.lua to use blueprint API with fallback
- Maintain backward compatibility with manual detection logic

Co-Authored-By: Keith Harvey <keithdanielharvey@gmail.com>
- Add SendToUnsynced/AddSyncAction bridge for blueprint category queries
- Create blueprint_category_bridge.lua gadget for reliable synced access
- Update mod_structure_upgrades.lua to use GG.BlueprintCategories API
- Add caching to avoid repeated cross-environment calls
- Maintain backward compatibility with manual detection fallback

Co-Authored-By: Keith Harvey <keithdanielharvey@gmail.com>
- Remove complex cross-environment communication bridge (out of scope)
- Use direct VFS.Include() to load blueprint substitution logic.lua
- Maintain backward compatibility with manual detection fallback
- Clean up api_blueprint.lua by removing unused sync handlers

Co-Authored-By: Keith Harvey <keithdanielharvey@gmail.com>
Co-Authored-By: Keith Harvey <keithdanielharvey@gmail.com>
- Remove defensive VFS.FileExists check (trust file exists)
- Load SubLogic once at module level instead of per function call
- Remove LoadBlueprintLogic function (no longer needed)
- Cleaner, more efficient blueprint integration

Co-Authored-By: Keith Harvey <keithdanielharvey@gmail.com>
- Load blueprint definitions to access UNIT_CATEGORIES enum constants
- Replace hardcoded strings with CATEGORIES.METAL_EXTRACTOR and CATEGORIES.GEOTHERMAL
- Only trigger upgrade logic for T1 units (METAL_EXTRACTOR, GEOTHERMAL) not advanced variants
- Cleaner, more maintainable category detection using proper enums

Co-Authored-By: Keith Harvey <keithdanielharvey@gmail.com>
- Add GetUnitCategory() helper function for clean category lookup
- Simplify IsMexUnit() and IsGeothermalUnit() to one-liner comparisons
- Remove all fallback detection logic - rely purely on blueprint system
- Streamline GetStructureTier() to use category helper directly
- Cleaner, more maintainable code with single source of truth

Co-Authored-By: Keith Harvey <keithdanielharvey@gmail.com>
- Add getCategory() helper function to definitions.lua for direct unit category lookup
- Simplify structure upgrades to use single include and one-liner detection
- Remove logic.lua dependency and intermediate helper functions
- True one-liner: IsMexUnit() = BlueprintDefs.getCategory(unitDefID) == CATEGORIES.METAL_EXTRACTOR
- Cleaner, more maintainable single source of truth for category detection

Co-Authored-By: Keith Harvey <keithdanielharvey@gmail.com>
- Remove categories.find() calls for WEAPON, DEFENSE, TURRET from IsMilitaryBuilding()
- Remove categories.find() calls for ENERGY, METAL, STORAGE from IsEconomicBuilding()
- Keep only direct unit property checks (weapons, energyMake, etc.)
- Stick to blueprint system detection as primary approach per Keith's feedback

Co-Authored-By: Keith Harvey <keithdanielharvey@gmail.com>
feat: implement SYNCED_ACTION_FALLBACK for reclaim income taxation
@keithharvey keithharvey changed the title refactor: team transfer gadget refactor: move /take /give to LUA Aug 14, 2025
@keithharvey

Copy link
Copy Markdown
Collaborator Author

My hunt for ChangeTeamReason led me to realize all of these code paths were related and that all transfers should be handled by LUA. This simplifies things greatly on the engine side greatly, and doing it piecemeal seemed harder since the test grid was already huge, even for extremely limited engine changes to transfers.

Closing this in favor of the new PR, which has the new focus: #5667

@keithharvey keithharvey changed the title refactor: move /take /give to LUA Policy-Based Team Transfer Framework Aug 25, 2025
@keithharvey keithharvey changed the title Policy-Based Team Transfer Framework move /take to LUA Aug 25, 2025
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.

4 participants