Repository: beyond-all-reason/Beyond-All-Reason
Pull request #4397: Add auto trajectory toggle attempt 2, and improve reactiveness of switch queuing for vanguard
Author: login: SethDGamre
Created: 2025-02-24T22:45:57Z | Merged: 2025-03-20T18:11:13Z

OK, this time it's gonna work beautifully.  Previous attempt had a few bugs and in the process of discovering it I made the code pretty ugly. Started over fresh.

Testing steps:
load up a game, enable legion
/give armguard
/give armamb
/give armvang
/give corpun
/give cortoast
/give legcluster
/give legacluster

you should now see a toggle button in the UI for each that allows you to set low, high and auto. Auto is the default. Do weird stuff with it to try to break it. Thanks


Diff:
diff --git a/language/en/interface.json b/language/en/interface.json
index e9979225dd9..87d7bf5297d 100644
--- a/language/en/interface.json
+++ b/language/en/interface.json
@@ -265,7 +265,10 @@
 			"apAirRepair_tooltip": "Air factory: Set at which health %% an aircraft should automatically move to and land on an air repair pad",
 			"Low traj": "High Traj",
 			"High traj": "High Traj",
-			"trajectory_tooltip": "Sets fire mode in artillery state (firing upwards instead of forwards)",
+			"trajectory_toggle_tooltip": "Switch artillery firing angle between low, high and automatic trajectory",
+			"trajectory_low": "Low Trajectory",
+			"trajectory_high": "High Trajectory",
+			"trajectory_auto": "Auto Trajectory",
 			"hound_weapon_plasma": "Heavy Plasma",
 			"hound_weapon_gauss": "Gauss Cannon",
 			"hound_weapon_toggle_tooltip": "Sets weapon to a slow plasma projectile with a large aoe, or a fast projectile gauss weapon",
diff --git a/luarules/configs/customcmds.h.lua b/luarules/configs/customcmds.h.lua
index b2700ea01b3..f15efc4c889 100644
--- a/luarules/configs/customcmds.h.lua
+++ b/luarules/configs/customcmds.h.lua
@@ -33,6 +33,7 @@ CMD_UNIT_CANCEL_TARGET = 34924
 CMD_PRIORITY = 34571
 CMD_WANT_CLOAK = 37382
 CMD_HOUND_WEAPON_TOGGLE = 37383
+CMD_SMART_TOGGLE = 37384
 
 -- terraform
 CMD_RESTORE = 39739
diff --git a/luarules/gadgets/unit_weapon_smart_select_helper.lua b/luarules/gadgets/unit_weapon_smart_select_helper.lua
index 9a46083bf6d..d5890f573b5 100644
--- a/luarules/gadgets/unit_weapon_smart_select_helper.lua
+++ b/luarules/gadgets/unit_weapon_smart_select_helper.lua
@@ -31,6 +31,7 @@ local aggroDecayRate = 0.7 --aggro is multiplied by this until it falls within p
 local aggroDecayCap = 10  -- this caps the aggro decay so that misfire state can last a significant amount of time
 local aggroPriorityCap = 1 --The maximum aggro that can be accumulated. This prevents manual targetting from getting stuck in a fire mode for too long.
 local aggroBackupCap = -16 --Like above, but a negative value because backup is triggered with negative aggro.
+local gameSpeed = Game.gameSpeed
 
 --misfire occurs when the weapon thinks it can shoot a target due to faulty Spring.GetUnitWeaponHaveFreeLineOfFire return values. We must detect when this failure occurs and force high for a long duration.
 local misfireMultiplier = Game.gameSpeed * 1.5
@@ -51,8 +52,9 @@ local backupSwitchThreshold = backupAutoAggro * 1.2 * -1 --the aggro at which ba
 local priorityCooldownFrames = Game.gameSpeed * 1.5 -- so that no matter how the aggro weights are set, the mode switches will happen no sooner than this.
 local backupCooldownFrames = Game.gameSpeed * 4
 
-local PRIORITY_AIMINGSTATE = 1
-local BACKUP_AIMINGSTATE = 2
+local PRIORITY_AIMINGSTATE = 0
+local BACKUP_AIMINGSTATE = 1
+local AUTO_TOGGLESTATE = 2
 local UNIT_TARGET = 1
 local GROUND_TARGET = 2
 
@@ -73,7 +75,26 @@ local smartUnits = {}
 local smartUnitDefs = {}
 local modeSwitchFrames = {}
 
+-- Add with other local function declarations
+local spInsertUnitCmdDesc = Spring.InsertUnitCmdDesc
+local spEditUnitCmdDesc = Spring.EditUnitCmdDesc
+local spFindUnitCmdDesc = Spring.FindUnitCmdDesc
+
+include("luarules/configs/customcmds.h.lua")
+
+local trajectoryCmdDesc = {
+    id = CMD_SMART_TOGGLE,
+    type = CMDTYPE.ICON_MODE,
+    tooltip = 'trajectory_tooltip',
+    name = 'trajectory_toggle',
+    cursor = 'cursornormal',
+    action = 'trajectory_toggle',
+    params = { AUTO_TOGGLESTATE, "trajectory_low", "trajectory_high", "trajectory_auto" },
+}
+local defaultCmdDesc = trajectoryCmdDesc
+
 function gadget:Initialize()
+	gadgetHandler:RegisterAllowCommand(CMD_SMART_TOGGLE)
 	local units = Spring.GetAllUnits()
 	local spGetUnitDefID = Spring.GetUnitDefID
 	for i = 1, #units do
@@ -95,6 +116,13 @@ for unitDefID, def in ipairs(UnitDefs) do
 					if def.speed and def.speed ~= 0 then
 						smartUnitDefs[unitDefID].canMove = true
 					end
+					if def.customParams and def.customParams.smart_weapon_cmddesc then
+						if def.customParams.smart_weapon_cmddesc == "trajectory" then
+							smartUnitDefs[unitDefID].smartCmdDesc = trajectoryCmdDesc
+						end
+					else
+						smartUnitDefs[unitDefID].smartCmdDesc = defaultCmdDesc
+					end
 				end
 				if WeaponDefs[weaponDefID].customParams.smart_backup then
 					smartUnitDefs[unitDefID] = smartUnitDefs[unitDefID] or {}
@@ -118,7 +146,6 @@ local function updatePredictedShotFrame(attackerID, unitData, defData)
 	end
 end
 
---custom functions
 local function failureToFireCheck(attackerID, data, defData)
 	if not data.suspendMisfireUntilFrame or data.aggroBias < prioritySwitchThreshold then return false end
 
@@ -147,39 +174,44 @@ end
 --switch the fire mode in the middle of the next reloadtime when available to both make transitions at the ideal time
 --and completely eliminate indecisive wobbling
 local function queueSwitchFrame(attackerID, data, defData, setState)
-    if data.state ~= setState and data.switchCooldownFrame < gameFrame then
-        local idealSubtraction = defData.reloadFrames * 0.75 -- so the switch occurs soon after a shot
-        local idealAddition = defData.reloadFrames - idealSubtraction
-        local idealFrame
-        
+	if data.state ~= setState and data.switchCooldownFrame < gameFrame then
+		local idealSubtraction = defData.reloadFrames * 0.75 -- so the switch occurs soon after a shot
+		local idealAddition = defData.reloadFrames - idealSubtraction
+		local maxSubtraction = gameSpeed * 2 -- so that very slow reloading units don't refuse to switch within too large of a time frame
+		local idealFrame
+		
 		updatePredictedShotFrame(attackerID, data, defData)
 
-        if data.predictedShotFrame < gameFrame then
-            -- we're so far past the last reloadtime, weapon is either stuck or otherwise can't fire
-            spCallCOBScript(attackerID, data.setStateScriptID, 0, setState)
-        else
+		if data.predictedShotFrame < gameFrame then
+			-- we're so far past the last reloadtime, weapon is either stuck or otherwise can't fire
+			spCallCOBScript(attackerID, data.setStateScriptID, 0, setState)
+		else
 			-- is now just before the ideal frame to switch on?
-            idealFrame = data.predictedShotFrame - idealSubtraction
-            if idealFrame <= gameFrame then
-                -- remaining possibility, queue switch for after next predicted shot
-                idealFrame = data.predictedShotFrame + idealAddition
-            end
-			idealFrame = math.floor(idealFrame)
-			modeSwitchFrames[idealFrame] = modeSwitchFrames[idealFrame] or {}
-			modeSwitchFrames[idealFrame][attackerID] = setState
-        end
-        
-        data.state = setState
+			local tooCloseToFiringToSwitchFrame = mathMax(data.predictedShotFrame - idealSubtraction, data.predictedShotFrame - maxSubtraction)
+			if tooCloseToFiringToSwitchFrame <= gameFrame then
+				-- remaining possibility, queue switch for after next predicted shot
+				idealFrame = math.floor(data.predictedShotFrame + idealAddition)
+				modeSwitchFrames[idealFrame] = modeSwitchFrames[idealFrame] or {}
+				modeSwitchFrames[idealFrame][attackerID] = setState
+			else
+				spCallCOBScript(attackerID, data.setStateScriptID, 0, setState)
+			end
+
+		end
+		
+		data.state = setState
 		if data.state == PRIORITY_AIMINGSTATE then
-        	data.switchCooldownFrame = gameFrame + priorityCooldownFrames
+			data.switchCooldownFrame = gameFrame + priorityCooldownFrames
 		else
 			data.switchCooldownFrame = gameFrame + backupCooldownFrames
 		end
-    end
+	end
 end
 
-
 local function updateAimingState(attackerID)
+	if smartUnits[attackerID].toggleState ~= AUTO_TOGGLESTATE then
+		return
+	end
 	local data = smartUnits[attackerID]
 	local defData = smartUnitDefs[data.unitDefID]
 
@@ -262,7 +294,21 @@ local function updateAimingState(attackerID)
 	data.misfireTallyMultiplier = data.misfireTallyMultiplier * misfireTallyDecayRate
 end
 
---call-ins
+local function toggleTrajectory(unitID, state)
+    local cmdDescID = spFindUnitCmdDesc(unitID, CMD_SMART_TOGGLE)
+    if cmdDescID then
+		local unitData = smartUnits[unitID]
+        state = (state % 3)
+        trajectoryCmdDesc.params[1] = state
+        spEditUnitCmdDesc(unitID, cmdDescID, {params = trajectoryCmdDesc.params})
+		unitData.toggleState = state
+		unitData.state = state
+		if state ~= AUTO_TOGGLESTATE then
+			spCallCOBScript(unitID, smartUnits[unitID].setStateScriptID, 0, state)
+		end
+    end
+end
+
 function gadget:UnitCreated(unitID, unitDefID)
 	if smartUnitDefs[unitDefID] then
 		local scriptID = Spring.GetCOBScriptID(unitID, "SetAimingState")
@@ -275,12 +321,15 @@ function gadget:UnitCreated(unitID, unitDefID)
 				misfireTallyMultiplier = 0,
 				lastTargetMatchNumber = 0, --this exists so that a player switching targets frequently doesn't trigger a faulty misfire.
 				switchCooldownFrame = 0,
-				state = PRIORITY_AIMINGSTATE
+				state = PRIORITY_AIMINGSTATE,
+				toggleState = AUTO_TOGGLESTATE
 			}
-			spCallCOBScript(unitID, smartUnits[unitID].setStateScriptID, 0, smartUnitDefs[unitDefID].priorityWeapon)
+			spCallCOBScript(unitID, smartUnits[unitID].setStateScriptID, 0, PRIORITY_AIMINGSTATE)
+			
+			smartUnitDefs[unitDefID].smartCmdDesc.params[1] = AUTO_TOGGLESTATE
+			spInsertUnitCmdDesc(unitID, smartUnitDefs[unitDefID].smartCmdDesc)
 		end
 	end
-
 end
 
 function gadget:UnitDestroyed(unitID, unitDefID, unitTeam, attackerID, attackerDefID, attackerTeam, weaponDefID)
@@ -293,7 +342,7 @@ function gadget:GameFrame(frame)
 		for attackerID in pairs(smartUnits) do
 			updateAimingState(attackerID)
 		end
-	end --zzz need to add the execution of the queueSwitchFrame
+	end 
 	local switchModeQueue = modeSwitchFrames[frame]
 	if switchModeQueue then
 		for unitID, setState in pairs(switchModeQueue) do
@@ -304,3 +353,11 @@ function gadget:GameFrame(frame)
 		end
 	end
 end
+
+function gadget:AllowCommand(unitID, unitDefID, teamID, cmdID, cmdParams, cmdOptions)
+    if smartUnitDefs[unitDefID] and cmdID == CMD_SMART_TOGGLE then
+        toggleTrajectory(unitID, cmdParams[1])
+        return false  -- command was used
+    end
+    return true  -- command was not used
+end
diff --git a/scripts/Units/armamb.cob b/scripts/Units/armamb.cob
index fdf0783258c..5b17ef91de0 100644
Binary files a/scripts/Units/armamb.cob and b/scripts/Units/armamb.cob differ
diff --git a/scripts/Units/armguard.cob b/scripts/Units/armguard.cob
index 28afa83529e..cc15ac477b3 100644
Binary files a/scripts/Units/armguard.cob and b/scripts/Units/armguard.cob differ
diff --git a/scripts/Units/armvang.bos b/scripts/Units/armvang.bos
index 651b87f26a8..19f788baf91 100644
--- a/scripts/Units/armvang.bos
+++ b/scripts/Units/armvang.bos
@@ -24,8 +24,6 @@ static-var  bMoving, oldHeading, bSteering, wpn1_lasthead, last_primary_heading,
 // this animation uses the static-var animFramesPerKeyframe which contains how many frames each keyframe takes
 // Author Beherith mysterme@gmail.com. License: GNU GPL v2.
 
-static-var animSpeed, maxSpeed, animFramesPerKeyframe;
-
 lua_UnitScriptDecal(lightIndex, xpos,zpos, heading) 
 {
 	return 0;
diff --git a/scripts/Units/armvang.cob b/scripts/Units/armvang.cob
index fec7ff2a3ea..112736fcbed 100644
Binary files a/scripts/Units/armvang.cob and b/scripts/Units/armvang.cob differ
diff --git a/scripts/Units/cortoast.cob b/scripts/Units/cortoast.cob
index 3a2f8e30515..0946346e77b 100644
Binary files a/scripts/Units/cortoast.cob and b/scripts/Units/cortoast.cob differ
diff --git a/scripts/Units/legacluster.cob b/scripts/Units/legacluster.cob
index b4b5dd9cc7a..f362e89cf30 100644
Binary files a/scripts/Units/legacluster.cob and b/scripts/Units/legacluster.cob differ
diff --git a/scripts/Units/legcluster.cob b/scripts/Units/legcluster.cob
index 87264189c49..935691a1889 100644
Binary files a/scripts/Units/legcluster.cob and b/scripts/Units/legcluster.cob differ
diff --git a/scripts/Units/smart_weapon_select.h b/scripts/Units/smart_weapon_select.h
index 7e74a277020..b35932d3aff 100644
--- a/scripts/Units/smart_weapon_select.h
+++ b/scripts/Units/smart_weapon_select.h
@@ -36,8 +36,8 @@ static-var AimingState;
 
 #define __SMARTSELECT_H_
 
-#define AIMING_PRIORITY		1
-#define AIMING_BACKUP		2
+#define AIMING_PRIORITY		0
+#define AIMING_BACKUP		1
 
 SetAimingState(newState)
 {


Reviews:
user: login: WatchTheFort
body: Make sure to enter the keybinding entry into [the big keybinding spreadsheet](https://docs.google.com/spreadsheets/d/1vOgKa1EO3g8Qg-2LkyavzBfDoMDeRrI00rrflZ8dEcw/edit?usp=sharing).
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-03T21:07:50Z
user: login: SethDGamre
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T22:35:58Z
user: login: sprunk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T11:44:16Z
user: login: SethDGamre
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-09T18:04:20Z
user: login: SethDGamre
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-09T18:04:53Z
user: login: SethDGamre
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-09T19:08:33Z
user: login: SethDGamre
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-09T19:08:54Z
user: login: badosu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T02:58:18Z
user: login: badosu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T04:10:49Z
user: login: SethDGamre
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-14T11:33:32Z
user: login: badosu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T14:30:00Z
user: login: badosu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T16:26:41Z
user: login: badosu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T16:29:12Z
user: login: SethDGamre
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-19T10:06:37Z
user: login: SethDGamre
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-19T10:24:40Z
user: login: WatchTheFort
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T12:35:30Z
user: login: SethDGamre
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-19T12:48:49Z
user: login: WatchTheFort
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T13:12:34Z
user: login: WatchTheFort
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T16:00:55Z
user: login: SethDGamre
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-19T16:01:41Z
user: login: WatchTheFort
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T16:06:13Z
user: login: SethDGamre
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-19T16:29:45Z
user: login: WatchTheFort
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T16:36:25Z
user: login: sprunk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-19T16:40:19Z
user: login: badosu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T17:29:23Z
user: login: badosu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T17:31:46Z
user: login: badosu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T17:41:22Z
user: login: SethDGamre
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-19T17:44:06Z
user: login: badosu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T17:45:12Z
user: login: WatchTheFort
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T17:46:49Z
user: login: badosu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T17:50:34Z
user: login: WatchTheFort
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T17:59:40Z
user: login: badosu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T18:16:14Z
user: login: sprunk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-19T19:13:04Z
user: login: WatchTheFort
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T19:49:09Z
user: login: sprunk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-19T21:14:35Z
user: login: WatchTheFort
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T21:51:51Z
user: login: sprunk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-19T23:22:01Z
user: login: badosu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T23:26:53Z
user: login: badosu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T00:34:25Z
user: login: sprunk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-20T01:28:14Z
user: login: WatchTheFort
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T02:03:39Z
user: login: sprunk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-20T10:36:46Z
user: login: badosu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T16:57:55Z
user: login: badosu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T17:07:53Z

Review comments:
diff_hunk: @@ -95,6 +115,13 @@ for unitDefID, def in ipairs(UnitDefs) do
 					if def.speed and def.speed ~= 0 then
 						smartUnitDefs[unitDefID].canMove = true
 					end
+					if def.customParams and def.customParams.smart_weapon_cmddesc then
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: WatchTheFort
body: What exactly is this custom param? Is there more than one type of smart weapon?
created_at: 2025-03-03T21:04:09Z
author_association: MEMBER
diff_hunk: @@ -262,7 +290,21 @@ local function updateAimingState(attackerID)
 	data.misfireTallyMultiplier = data.misfireTallyMultiplier * misfireTallyDecayRate
 end
 
---call-ins
+local function toggleTrajectory(unitID, state)
+    local cmdDescID = spFindUnitCmdDesc(unitID, CMD_SMART_TOGGLE)
+    if cmdDescID then
+		local unitData = smartUnits[unitID]
+        state = (state % 3)
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: WatchTheFort
body: Can the value of `state` ever not be a valid state?
created_at: 2025-03-03T21:05:14Z
author_association: MEMBER
diff_hunk: @@ -262,7 +290,21 @@ local function updateAimingState(attackerID)
 	data.misfireTallyMultiplier = data.misfireTallyMultiplier * misfireTallyDecayRate
 end
 
---call-ins
+local function toggleTrajectory(unitID, state)
+    local cmdDescID = spFindUnitCmdDesc(unitID, CMD_SMART_TOGGLE)
+    if cmdDescID then
+		local unitData = smartUnits[unitID]
+        state = (state % 3)
+        trajectoryCmdDesc.params[1] = state
+        spEditUnitCmdDesc(unitID, cmdDescID, {params = trajectoryCmdDesc.params})
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: WatchTheFort
body: `spEditUnitCmdDesc` only needs to be called once per unit, not every time the command is used. Also, now that this is a distinct command, not the On/Off command, why does this need to be called at all?
created_at: 2025-03-03T21:06:28Z
author_association: MEMBER
diff_hunk: @@ -304,3 +349,11 @@ function gadget:GameFrame(frame)
 		end
 	end
 end
+
+function gadget:AllowCommand(unitID, unitDefID, teamID, cmdID, cmdParams, cmdOptions)
+    if smartUnitDefs[unitDefID] and cmdID == CMD_SMART_TOGGLE then
+        toggleTrajectory(unitID, cmdParams[1])
+        return false  -- command was used
+    end
+    return true  -- command was not used
+end
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: WatchTheFort
body: Why is this needed? This should work like any other toggle command.
created_at: 2025-03-03T21:07:44Z
author_association: MEMBER
diff_hunk: @@ -304,3 +349,11 @@ function gadget:GameFrame(frame)
 		end
 	end
 end
+
+function gadget:AllowCommand(unitID, unitDefID, teamID, cmdID, cmdParams, cmdOptions)
+    if smartUnitDefs[unitDefID] and cmdID == CMD_SMART_TOGGLE then
+        toggleTrajectory(unitID, cmdParams[1])
+        return false  -- command was used
+    end
+    return true  -- command was not used
+end
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: SethDGamre
body: What is the correct way
created_at: 2025-03-03T22:35:58Z
author_association: COLLABORATOR
diff_hunk: @@ -304,3 +349,11 @@ function gadget:GameFrame(frame)
 		end
 	end
 end
+
+function gadget:AllowCommand(unitID, unitDefID, teamID, cmdID, cmdParams, cmdOptions)
+    if smartUnitDefs[unitDefID] and cmdID == CMD_SMART_TOGGLE then
+        toggleTrajectory(unitID, cmdParams[1])
+        return false  -- command was used
+    end
+    return true  -- command was not used
+end
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: sprunk
body: Remove the block and tell WtF what blew up.
created_at: 2025-03-04T11:44:16Z
author_association: COLLABORATOR
diff_hunk: @@ -95,6 +115,13 @@ for unitDefID, def in ipairs(UnitDefs) do
 					if def.speed and def.speed ~= 0 then
 						smartUnitDefs[unitDefID].canMove = true
 					end
+					if def.customParams and def.customParams.smart_weapon_cmddesc then
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: SethDGamre
body: I made smart weapon switching something that can happen with any arrangement of weapons - not just high and low. This is to support multiple different toggle names for other smart weapon arrangements.
created_at: 2025-03-09T18:04:20Z
author_association: COLLABORATOR
diff_hunk: @@ -262,7 +290,21 @@ local function updateAimingState(attackerID)
 	data.misfireTallyMultiplier = data.misfireTallyMultiplier * misfireTallyDecayRate
 end
 
---call-ins
+local function toggleTrajectory(unitID, state)
+    local cmdDescID = spFindUnitCmdDesc(unitID, CMD_SMART_TOGGLE)
+    if cmdDescID then
+		local unitData = smartUnits[unitID]
+        state = (state % 3)
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: SethDGamre
body: not from my testing
I stole all this from the hound toggle
created_at: 2025-03-09T18:04:53Z
author_association: COLLABORATOR
diff_hunk: @@ -262,7 +290,21 @@ local function updateAimingState(attackerID)
 	data.misfireTallyMultiplier = data.misfireTallyMultiplier * misfireTallyDecayRate
 end
 
---call-ins
+local function toggleTrajectory(unitID, state)
+    local cmdDescID = spFindUnitCmdDesc(unitID, CMD_SMART_TOGGLE)
+    if cmdDescID then
+		local unitData = smartUnits[unitID]
+        state = (state % 3)
+        trajectoryCmdDesc.params[1] = state
+        spEditUnitCmdDesc(unitID, cmdDescID, {params = trajectoryCmdDesc.params})
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: SethDGamre
body: You're thinking of https://springrts.com/wiki/Lua_SyncedCtrl#InsertUnitCmdDesc

not

https://springrts.com/wiki/Lua_SyncedCtrl#EditUnitCmdDesc

what you're suggesting won't change button state when clicked. Yes, I tested it. It broke.
created_at: 2025-03-09T19:08:32Z
author_association: COLLABORATOR
diff_hunk: @@ -304,3 +349,11 @@ function gadget:GameFrame(frame)
 		end
 	end
 end
+
+function gadget:AllowCommand(unitID, unitDefID, teamID, cmdID, cmdParams, cmdOptions)
+    if smartUnitDefs[unitDefID] and cmdID == CMD_SMART_TOGGLE then
+        toggleTrajectory(unitID, cmdParams[1])
+        return false  -- command was used
+    end
+    return true  -- command was not used
+end
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: SethDGamre
body: this is how I detect when the button was clicked. It stays.
created_at: 2025-03-09T19:08:54Z
author_association: COLLABORATOR
diff_hunk: @@ -304,3 +349,11 @@ function gadget:GameFrame(frame)
 		end
 	end
 end
+
+function gadget:AllowCommand(unitID, unitDefID, teamID, cmdID, cmdParams, cmdOptions)
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: badosu
body: Use `AllowCommand` only to return true or false without changing internal state. Use `UnitCommand` for command handling (calling `toggleTrajectory`).
created_at: 2025-03-14T02:58:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -262,7 +290,21 @@ local function updateAimingState(attackerID)
 	data.misfireTallyMultiplier = data.misfireTallyMultiplier * misfireTallyDecayRate
 end
 
---call-ins
+local function toggleTrajectory(unitID, state)
+    local cmdDescID = spFindUnitCmdDesc(unitID, CMD_SMART_TOGGLE)
+    if cmdDescID then
+		local unitData = smartUnits[unitID]
+        state = (state % 3)
+        trajectoryCmdDesc.params[1] = state
+        spEditUnitCmdDesc(unitID, cmdDescID, {params = trajectoryCmdDesc.params})
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: badosu
body: I made a huge comment investigating the need for editing the cmddesc, in the end I decided to remove it and synthesize my findings.

First, answering @WatchTheFort concern (which also was mine): the engine has a convention for stateful commands to store the current state inside the units command description.

Now: the engine does not set `params[1]` in the cmddescr when receiving a game defined state command order, even though it does for all internally known state commands. I [created an engine issue](https://github.com/beyond-all-reason/spring/issues/2019) for it.

BAR [uses](https://github.com/beyond-all-reason/Beyond-All-Reason/blob/master/luaui/Widgets/gui_ordermenu.lua#L560-L567) the `params[1]` convention for retrieving state in stateful commands for UI, so I understand why one would do the editing themselves.
created_at: 2025-03-14T04:10:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -262,7 +290,21 @@ local function updateAimingState(attackerID)
 	data.misfireTallyMultiplier = data.misfireTallyMultiplier * misfireTallyDecayRate
 end
 
---call-ins
+local function toggleTrajectory(unitID, state)
+    local cmdDescID = spFindUnitCmdDesc(unitID, CMD_SMART_TOGGLE)
+    if cmdDescID then
+		local unitData = smartUnits[unitID]
+        state = (state % 3)
+        trajectoryCmdDesc.params[1] = state
+        spEditUnitCmdDesc(unitID, cmdDescID, {params = trajectoryCmdDesc.params})
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: SethDGamre
body: So .... Do I need to change anything?
created_at: 2025-03-14T11:33:32Z
author_association: COLLABORATOR
diff_hunk: @@ -262,7 +290,21 @@ local function updateAimingState(attackerID)
 	data.misfireTallyMultiplier = data.misfireTallyMultiplier * misfireTallyDecayRate
 end
 
---call-ins
+local function toggleTrajectory(unitID, state)
+    local cmdDescID = spFindUnitCmdDesc(unitID, CMD_SMART_TOGGLE)
+    if cmdDescID then
+		local unitData = smartUnits[unitID]
+        state = (state % 3)
+        trajectoryCmdDesc.params[1] = state
+        spEditUnitCmdDesc(unitID, cmdDescID, {params = trajectoryCmdDesc.params})
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: badosu
body: Nope, you can resolve this
created_at: 2025-03-14T14:30:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -262,7 +290,21 @@ local function updateAimingState(attackerID)
 	data.misfireTallyMultiplier = data.misfireTallyMultiplier * misfireTallyDecayRate
 end
 
---call-ins
+local function toggleTrajectory(unitID, state)
+    local cmdDescID = spFindUnitCmdDesc(unitID, CMD_SMART_TOGGLE)
+    if cmdDescID then
+		local unitData = smartUnits[unitID]
+        state = (state % 3)
+        trajectoryCmdDesc.params[1] = state
+        spEditUnitCmdDesc(unitID, cmdDescID, {params = trajectoryCmdDesc.params})
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: badosu
body: Just as a sidenote, the pattern of using EditCmdDesc to store stateful commands state is pervasive in ZeroK and used to great effect as well.
created_at: 2025-03-14T16:26:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -262,7 +290,21 @@ local function updateAimingState(attackerID)
 	data.misfireTallyMultiplier = data.misfireTallyMultiplier * misfireTallyDecayRate
 end
 
---call-ins
+local function toggleTrajectory(unitID, state)
+    local cmdDescID = spFindUnitCmdDesc(unitID, CMD_SMART_TOGGLE)
+    if cmdDescID then
+		local unitData = smartUnits[unitID]
+        state = (state % 3)
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: badosu
body: Would have to test the 2 main modes to send commands from unsynced:

- `bind <somekey> toggleTrajectory 10` and hitting somekey without other actions in conflict
- `Spring.GiverOrder()` in unsynced with insane argument.

I'd rather have state validation than the modulus here, if no validation is required then neither is the modulus.
created_at: 2025-03-14T16:29:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -262,7 +290,21 @@ local function updateAimingState(attackerID)
 	data.misfireTallyMultiplier = data.misfireTallyMultiplier * misfireTallyDecayRate
 end
 
---call-ins
+local function toggleTrajectory(unitID, state)
+    local cmdDescID = spFindUnitCmdDesc(unitID, CMD_SMART_TOGGLE)
+    if cmdDescID then
+		local unitData = smartUnits[unitID]
+        state = (state % 3)
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: SethDGamre
body: I gotta be honest I don't follow what you are saying. I'm unfamiliar with ketbinds and don't understand what state validation is in this context.

Once again, I stole and adapted code from the existing hound toggle. It worked, so I didn't mess with it. 

If something should be different, I require explicit instructions
created_at: 2025-03-19T10:06:37Z
author_association: COLLABORATOR
diff_hunk: @@ -304,3 +349,11 @@ function gadget:GameFrame(frame)
 		end
 	end
 end
+
+function gadget:AllowCommand(unitID, unitDefID, teamID, cmdID, cmdParams, cmdOptions)
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: SethDGamre
body: Just tested, `UnitCommand` removes existing target when the command is accepted. `AllowCommand` retains target. As such, I'm keeping `AllowCommand`
created_at: 2025-03-19T10:24:40Z
author_association: COLLABORATOR
diff_hunk: @@ -262,7 +290,21 @@ local function updateAimingState(attackerID)
 	data.misfireTallyMultiplier = data.misfireTallyMultiplier * misfireTallyDecayRate
 end
 
---call-ins
+local function toggleTrajectory(unitID, state)
+    local cmdDescID = spFindUnitCmdDesc(unitID, CMD_SMART_TOGGLE)
+    if cmdDescID then
+		local unitData = smartUnits[unitID]
+        state = (state % 3)
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: WatchTheFort
body: Valid states are 0, 1, and 2. In what situation would the number passed not be one of these?
created_at: 2025-03-19T12:35:29Z
author_association: MEMBER
diff_hunk: @@ -262,7 +290,21 @@ local function updateAimingState(attackerID)
 	data.misfireTallyMultiplier = data.misfireTallyMultiplier * misfireTallyDecayRate
 end
 
---call-ins
+local function toggleTrajectory(unitID, state)
+    local cmdDescID = spFindUnitCmdDesc(unitID, CMD_SMART_TOGGLE)
+    if cmdDescID then
+		local unitData = smartUnits[unitID]
+        state = (state % 3)
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: SethDGamre
body: Never? Unless there's some wizardy involved with players being able to set invalid values via keybinds. But that is A. Mod-tier do at your own risk stuff and B. Something I'm not familiar with. 
created_at: 2025-03-19T12:48:48Z
author_association: COLLABORATOR
diff_hunk: @@ -262,7 +290,21 @@ local function updateAimingState(attackerID)
 	data.misfireTallyMultiplier = data.misfireTallyMultiplier * misfireTallyDecayRate
 end
 
---call-ins
+local function toggleTrajectory(unitID, state)
+    local cmdDescID = spFindUnitCmdDesc(unitID, CMD_SMART_TOGGLE)
+    if cmdDescID then
+		local unitData = smartUnits[unitID]
+        state = (state % 3)
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: WatchTheFort
body: That's why we're saying to remove the `state = (state % 3)` line.
created_at: 2025-03-19T13:12:33Z
author_association: MEMBER
diff_hunk: @@ -95,6 +115,13 @@ for unitDefID, def in ipairs(UnitDefs) do
 					if def.speed and def.speed ~= 0 then
 						smartUnitDefs[unitDefID].canMove = true
 					end
+					if def.customParams and def.customParams.smart_weapon_cmddesc then
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: WatchTheFort
body: But isn't the code specific to switching between low and high trajectory? How would other smart switching weapons work?
created_at: 2025-03-19T16:00:55Z
author_association: MEMBER
diff_hunk: @@ -262,7 +290,21 @@ local function updateAimingState(attackerID)
 	data.misfireTallyMultiplier = data.misfireTallyMultiplier * misfireTallyDecayRate
 end
 
---call-ins
+local function toggleTrajectory(unitID, state)
+    local cmdDescID = spFindUnitCmdDesc(unitID, CMD_SMART_TOGGLE)
+    if cmdDescID then
+		local unitData = smartUnits[unitID]
+        state = (state % 3)
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: SethDGamre
body: and do what?
created_at: 2025-03-19T16:01:41Z
author_association: COLLABORATOR
diff_hunk: @@ -262,7 +290,21 @@ local function updateAimingState(attackerID)
 	data.misfireTallyMultiplier = data.misfireTallyMultiplier * misfireTallyDecayRate
 end
 
---call-ins
+local function toggleTrajectory(unitID, state)
+    local cmdDescID = spFindUnitCmdDesc(unitID, CMD_SMART_TOGGLE)
+    if cmdDescID then
+		local unitData = smartUnits[unitID]
+        state = (state % 3)
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: WatchTheFort
body: Literally nothing, no extra processing needs to be done. As a test, echo the value of `state` when the function is called and see if it is ever anything other than 0, 1, or 2. If it _is_ ever a different value, you need to figure out what is passing an invalid value, and why. Otherwise it is just covering up a bug, which will likely impact other areas as well.
created_at: 2025-03-19T16:06:13Z
author_association: MEMBER
diff_hunk: @@ -262,7 +290,21 @@ local function updateAimingState(attackerID)
 	data.misfireTallyMultiplier = data.misfireTallyMultiplier * misfireTallyDecayRate
 end
 
---call-ins
+local function toggleTrajectory(unitID, state)
+    local cmdDescID = spFindUnitCmdDesc(unitID, CMD_SMART_TOGGLE)
+    if cmdDescID then
+		local unitData = smartUnits[unitID]
+        state = (state % 3)
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: SethDGamre
body: it is never a different value
the first param selects which of the other keys is validated or something like that
what you're suggesting for testing will just break it
no more rabbitholes thanks
created_at: 2025-03-19T16:29:45Z
author_association: COLLABORATOR
diff_hunk: @@ -262,7 +290,21 @@ local function updateAimingState(attackerID)
 	data.misfireTallyMultiplier = data.misfireTallyMultiplier * misfireTallyDecayRate
 end
 
---call-ins
+local function toggleTrajectory(unitID, state)
+    local cmdDescID = spFindUnitCmdDesc(unitID, CMD_SMART_TOGGLE)
+    if cmdDescID then
+		local unitData = smartUnits[unitID]
+        state = (state % 3)
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: WatchTheFort
body: > what you're suggesting for testing will just break it

`Spring.Echo(state)` will break it?


created_at: 2025-03-19T16:36:25Z
author_association: MEMBER
diff_hunk: @@ -262,7 +290,21 @@ local function updateAimingState(attackerID)
 	data.misfireTallyMultiplier = data.misfireTallyMultiplier * misfireTallyDecayRate
 end
 
---call-ins
+local function toggleTrajectory(unitID, state)
+    local cmdDescID = spFindUnitCmdDesc(unitID, CMD_SMART_TOGGLE)
+    if cmdDescID then
+		local unitData = smartUnits[unitID]
+        state = (state % 3)
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: sprunk
body: > what you're suggesting for testing will just break it

extremely based take, i gotta use this approach to refuse going into rabbitholes more often
created_at: 2025-03-19T16:40:19Z
author_association: COLLABORATOR
diff_hunk: @@ -262,7 +290,21 @@ local function updateAimingState(attackerID)
 	data.misfireTallyMultiplier = data.misfireTallyMultiplier * misfireTallyDecayRate
 end
 
---call-ins
+local function toggleTrajectory(unitID, state)
+    local cmdDescID = spFindUnitCmdDesc(unitID, CMD_SMART_TOGGLE)
+    if cmdDescID then
+		local unitData = smartUnits[unitID]
+        state = (state % 3)
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: badosu
body: At this point it's just bikeshedding. It's problematic that the main methods for testing the possible inputs are not covered before merging, but really it's not a big deal. The failure mode is just a clueless user trying to assign a value that is not allowed to the state command, be it via a widget `Spring.GiveOrder` or a keybinding. I was expecting testing this to not be a big deal but it's ok if we can't expect BAR PRs to do it.

There are some levels from which the engine might sanitize (or not) the inputs here, so for example triggering a key bound by `bind <key> toggleTrajectory 10` might set state to `10 % 3` instead of doing nothing and returning false to `AllowCommand` (which would be the correct thing to do) but only testing to check if engine sanitized it ahead of time or not. Similar for unsynced `Spring.GiveOrder`.

The modulus here is just putting lipstick on a pig, we don't care about sanitization so we just do a yolo whatever goes as long as its one of the 0-n parameters. But in the end, the failure mode is just a clueless user trying to assign a value that is not allowed to the state command, so I guess we don't need to waste much time over it.
created_at: 2025-03-19T17:29:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -304,3 +349,11 @@ function gadget:GameFrame(frame)
 		end
 	end
 end
+
+function gadget:AllowCommand(unitID, unitDefID, teamID, cmdID, cmdParams, cmdOptions)
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: badosu
body: I don't understand, you mean `UnitCommand` with the exact same signature as `AllowCommand` is not passing the right parameters to the function?
created_at: 2025-03-19T17:31:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -304,3 +353,11 @@ function gadget:GameFrame(frame)
 		end
 	end
 end
+
+function gadget:AllowCommand(unitID, unitDefID, teamID, cmdID, cmdParams, cmdOptions)
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: badosu
body: I just realized you actually made an attempt to use `UnitCommand` [here](https://github.com/beyond-all-reason/Beyond-All-Reason/pull/4397/commits/871601c7eaa314957a07230a4ebad0c384729264).

This attempt for sure will not work, mainly because you need `AllowCommand` to exist returning true or false. After `AllowCommand` returns true `UnitCommand` executes.

So basically:

- `AllowCommand`: Returns true except when `cmdID = CMD_SMART_TOGGLE` and `cmdParams[1]` is an invalid input
- `UnitCommand`: Executes the `toggleTrajectory` logic
created_at: 2025-03-19T17:41:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -304,3 +353,11 @@ function gadget:GameFrame(frame)
 		end
 	end
 end
+
+function gadget:AllowCommand(unitID, unitDefID, teamID, cmdID, cmdParams, cmdOptions)
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: SethDGamre
body: what went wrong is that the target would be cancelled, but the trajectory would change correctly. I'd have to re-set the target.
created_at: 2025-03-19T17:44:06Z
author_association: COLLABORATOR
diff_hunk: @@ -304,3 +353,11 @@ function gadget:GameFrame(frame)
 		end
 	end
 end
+
+function gadget:AllowCommand(unitID, unitDefID, teamID, cmdID, cmdParams, cmdOptions)
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: badosu
body: You know what, given how fucked up the engine is with the state commands maybe it's better this way. Feel free to resolve this issue.
created_at: 2025-03-19T17:45:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -262,7 +290,21 @@ local function updateAimingState(attackerID)
 	data.misfireTallyMultiplier = data.misfireTallyMultiplier * misfireTallyDecayRate
 end
 
---call-ins
+local function toggleTrajectory(unitID, state)
+    local cmdDescID = spFindUnitCmdDesc(unitID, CMD_SMART_TOGGLE)
+    if cmdDescID then
+		local unitData = smartUnits[unitID]
+        state = (state % 3)
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: WatchTheFort
body: I'm just trying to break the cycle of cargo-cult programming, where a no-op line gets perpetuated because that's how it is done everywhere else.
created_at: 2025-03-19T17:46:49Z
author_association: MEMBER
diff_hunk: @@ -262,7 +290,21 @@ local function updateAimingState(attackerID)
 	data.misfireTallyMultiplier = data.misfireTallyMultiplier * misfireTallyDecayRate
 end
 
---call-ins
+local function toggleTrajectory(unitID, state)
+    local cmdDescID = spFindUnitCmdDesc(unitID, CMD_SMART_TOGGLE)
+    if cmdDescID then
+		local unitData = smartUnits[unitID]
+        state = (state % 3)
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: badosu
body: Keep in mind `Spring.Echo` is not available in synced environment. `Spring.Log` might work though, e.g.: `		Spring.Log(gadget:GetInfo().name, LOG.WARNING, 'Invalid trajectory state ' .. state)`
created_at: 2025-03-19T17:50:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -262,7 +290,21 @@ local function updateAimingState(attackerID)
 	data.misfireTallyMultiplier = data.misfireTallyMultiplier * misfireTallyDecayRate
 end
 
---call-ins
+local function toggleTrajectory(unitID, state)
+    local cmdDescID = spFindUnitCmdDesc(unitID, CMD_SMART_TOGGLE)
+    if cmdDescID then
+		local unitData = smartUnits[unitID]
+        state = (state % 3)
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: WatchTheFort
body: What are you talking about? `Spring.Echo` is available everywhere.
created_at: 2025-03-19T17:59:40Z
author_association: MEMBER
diff_hunk: @@ -262,7 +290,21 @@ local function updateAimingState(attackerID)
 	data.misfireTallyMultiplier = data.misfireTallyMultiplier * misfireTallyDecayRate
 end
 
---call-ins
+local function toggleTrajectory(unitID, state)
+    local cmdDescID = spFindUnitCmdDesc(unitID, CMD_SMART_TOGGLE)
+    if cmdDescID then
+		local unitData = smartUnits[unitID]
+        state = (state % 3)
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: badosu
body: sure...
created_at: 2025-03-19T18:16:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -262,7 +290,21 @@ local function updateAimingState(attackerID)
 	data.misfireTallyMultiplier = data.misfireTallyMultiplier * misfireTallyDecayRate
 end
 
---call-ins
+local function toggleTrajectory(unitID, state)
+    local cmdDescID = spFindUnitCmdDesc(unitID, CMD_SMART_TOGGLE)
+    if cmdDescID then
+		local unitData = smartUnits[unitID]
+        state = (state % 3)
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: sprunk
body: `%` doesn't even properly limit the range because it does nothing to values like `0.5`.
created_at: 2025-03-19T19:13:04Z
author_association: COLLABORATOR
diff_hunk: @@ -262,7 +290,21 @@ local function updateAimingState(attackerID)
 	data.misfireTallyMultiplier = data.misfireTallyMultiplier * misfireTallyDecayRate
 end
 
---call-ins
+local function toggleTrajectory(unitID, state)
+    local cmdDescID = spFindUnitCmdDesc(unitID, CMD_SMART_TOGGLE)
+    if cmdDescID then
+		local unitData = smartUnits[unitID]
+        state = (state % 3)
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: WatchTheFort
body: I'm lost at this point. @sprunk @badosu Should we remove this line or leave it in? As far as I can tell, this line is no-op.
created_at: 2025-03-19T19:49:09Z
author_association: MEMBER
diff_hunk: @@ -262,7 +290,21 @@ local function updateAimingState(attackerID)
 	data.misfireTallyMultiplier = data.misfireTallyMultiplier * misfireTallyDecayRate
 end
 
---call-ins
+local function toggleTrajectory(unitID, state)
+    local cmdDescID = spFindUnitCmdDesc(unitID, CMD_SMART_TOGGLE)
+    if cmdDescID then
+		local unitData = smartUnits[unitID]
+        state = (state % 3)
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: sprunk
body: We don't know whether the line does anything (by virtue of not knowing how the engine treats arguments outside the nominally valid set) so the PR author should verify what happens by sending a bunch of commands with parameters like `0.5`, or `10`, or `nil`, or `1, 2`. Then we will be able decide whether the line can be removed or not, and whether it needs some other replacement instead, based on the result of that test.
created_at: 2025-03-19T21:14:35Z
author_association: COLLABORATOR
diff_hunk: @@ -262,7 +290,21 @@ local function updateAimingState(attackerID)
 	data.misfireTallyMultiplier = data.misfireTallyMultiplier * misfireTallyDecayRate
 end
 
---call-ins
+local function toggleTrajectory(unitID, state)
+    local cmdDescID = spFindUnitCmdDesc(unitID, CMD_SMART_TOGGLE)
+    if cmdDescID then
+		local unitData = smartUnits[unitID]
+        state = (state % 3)
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: WatchTheFort
body: I'm not interested in artificially passing invalid values, I'm only interested in whether the caller can pass them. Otherwise we're sanitizing input that never needs sanitizing.
created_at: 2025-03-19T21:51:51Z
author_association: MEMBER
diff_hunk: @@ -262,7 +290,21 @@ local function updateAimingState(attackerID)
 	data.misfireTallyMultiplier = data.misfireTallyMultiplier * misfireTallyDecayRate
 end
 
---call-ins
+local function toggleTrajectory(unitID, state)
+    local cmdDescID = spFindUnitCmdDesc(unitID, CMD_SMART_TOGGLE)
+    if cmdDescID then
+		local unitData = smartUnits[unitID]
+        state = (state % 3)
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: sprunk
body: > I'm not interested in artificially passing invalid values, I'm only interested in whether the caller can pass them.

Sure, in that case that is what the test should check (I don't understand the difference).
created_at: 2025-03-19T23:22:01Z
author_association: COLLABORATOR
diff_hunk: @@ -262,7 +290,21 @@ local function updateAimingState(attackerID)
 	data.misfireTallyMultiplier = data.misfireTallyMultiplier * misfireTallyDecayRate
 end
 
---call-ins
+local function toggleTrajectory(unitID, state)
+    local cmdDescID = spFindUnitCmdDesc(unitID, CMD_SMART_TOGGLE)
+    if cmdDescID then
+		local unitData = smartUnits[unitID]
+        state = (state % 3)
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: badosu
body: I'll test it myself when I can, and settle this debate. WTF this is not an "artificial" test, it's checking whether engine sanitizes input or not before reaching luaui callins 
created_at: 2025-03-19T23:26:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -262,7 +290,21 @@ local function updateAimingState(attackerID)
 	data.misfireTallyMultiplier = data.misfireTallyMultiplier * misfireTallyDecayRate
 end
 
---call-ins
+local function toggleTrajectory(unitID, state)
+    local cmdDescID = spFindUnitCmdDesc(unitID, CMD_SMART_TOGGLE)
+    if cmdDescID then
+		local unitData = smartUnits[unitID]
+        state = (state % 3)
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: badosu
body: Alright, tested with this [sample gadget](https://gist.github.com/badosu/0737e65090a3ddb610c3db5555461190).

Out of bounds bindings triggers such as `bind <key> dummystate 9` sends `cmdParams[1]` as `0`.
`Spring.GiveOrderToUnit(unitId, CMD_DUMMY, { 10 }, 0)` sends `cmdParams[1]` as `10`.

In summary, there's some dubious sanitization on bindings, none on unsynced giveorder.

Also noticed some poor copypasta in some gadgets bringing code from zk relying on zks gadget infrastructure such as `AllowCommand_*` callins while BAR doesn't have it. Anyway, unrelated.
created_at: 2025-03-20T00:34:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -262,7 +290,21 @@ local function updateAimingState(attackerID)
 	data.misfireTallyMultiplier = data.misfireTallyMultiplier * misfireTallyDecayRate
 end
 
---call-ins
+local function toggleTrajectory(unitID, state)
+    local cmdDescID = spFindUnitCmdDesc(unitID, CMD_SMART_TOGGLE)
+    if cmdDescID then
+		local unitData = smartUnits[unitID]
+        state = (state % 3)
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: sprunk
body: Perhaps try `{}` (might blow up the arithmetic `%` operator) and `{0.5}` (is not sanitized by the `%` operator but maybe engine does that).
created_at: 2025-03-20T01:28:14Z
author_association: COLLABORATOR
diff_hunk: @@ -262,7 +290,21 @@ local function updateAimingState(attackerID)
 	data.misfireTallyMultiplier = data.misfireTallyMultiplier * misfireTallyDecayRate
 end
 
---call-ins
+local function toggleTrajectory(unitID, state)
+    local cmdDescID = spFindUnitCmdDesc(unitID, CMD_SMART_TOGGLE)
+    if cmdDescID then
+		local unitData = smartUnits[unitID]
+        state = (state % 3)
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: WatchTheFort
body: What's next, are we going to type check in case it's passed a string? Obviously it can be passed invalid values, but only due to bad code in the caller, which is game-side. I'm still for removing it, but at this point I'm happy to leave it in and merge this PR.
created_at: 2025-03-20T02:03:39Z
author_association: MEMBER
diff_hunk: @@ -262,7 +290,21 @@ local function updateAimingState(attackerID)
 	data.misfireTallyMultiplier = data.misfireTallyMultiplier * misfireTallyDecayRate
 end
 
---call-ins
+local function toggleTrajectory(unitID, state)
+    local cmdDescID = spFindUnitCmdDesc(unitID, CMD_SMART_TOGGLE)
+    if cmdDescID then
+		local unitData = smartUnits[unitID]
+        state = (state % 3)
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: sprunk
body: > the caller, which is game-side

What specifically do you mean by "game-side"? If you mean "anything that produces invalid values has to be included in BAR itself, so we fully control it" then that is not true, anybody can send an invalid command via custom widget.

In particular anybody can provide a nil argument, which may or may not (we don't know because @badosu doesn't seem to have tested that particular case) stay nil after being passed through engine, in which case `nil % 3` will error (which affects everybody because this is synced code). If this is the case then `%` will not be the correct thing to use and I feel strongly about not erroring on user input.

Whether non-erroring (or at least not _immediately_ erroring) invalid states (like 0.5 or 10, and perhaps nil if testing reveals engine sanitizes it) cause any other problems when let through, such as with GUI when that unit is selected, sounds much less important and I don't feel strongly about it. This is partially because apparently every other state is already like this and partially because it would largely affect your own units since you can't control any others. But it would still be good to test what happens because if you can make a GUI blow up by somehow interacting with a unit in an invalid state then you could do it to other players by sharing such unit or by putting a unit being captured in an invalid state. I think both testing and handling this could in principle be left for later, but in practice I expect nobody will ever bother if it is not done now and I think the testing is trivial enough, and this patch unimportant enough, that it's fine to ask for it.
created_at: 2025-03-20T10:36:46Z
author_association: COLLABORATOR
diff_hunk: @@ -262,7 +290,21 @@ local function updateAimingState(attackerID)
 	data.misfireTallyMultiplier = data.misfireTallyMultiplier * misfireTallyDecayRate
 end
 
---call-ins
+local function toggleTrajectory(unitID, state)
+    local cmdDescID = spFindUnitCmdDesc(unitID, CMD_SMART_TOGGLE)
+    if cmdDescID then
+		local unitData = smartUnits[unitID]
+        state = (state % 3)
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: badosu
body: I say move this discussion to a conversational medium (such as discord or github discussions) and settle for current PR or any minor change.

At this point it's out of the scope of the PR to fix issues that might be prevalent not only on this particular gadget but on others, due to engine non standardization and erratic behavior or just copypasta culture.

When, either in github discussions or discord, we reach a consensus on action points to address this issue, then we act on it in an organized manner (and not just as a super deep discussion into a minor point of a PR like this).

Keep in mind, on the larger scheme of things this is a very important issue to be resolved - but not something that should be revolved on about in this particular PR.

In summary, resolve this comment or ask for a simple change that settles for something.
created_at: 2025-03-20T16:57:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -262,7 +290,21 @@ local function updateAimingState(attackerID)
 	data.misfireTallyMultiplier = data.misfireTallyMultiplier * misfireTallyDecayRate
 end
 
---call-ins
+local function toggleTrajectory(unitID, state)
+    local cmdDescID = spFindUnitCmdDesc(unitID, CMD_SMART_TOGGLE)
+    if cmdDescID then
+		local unitData = smartUnits[unitID]
+        state = (state % 3)
path: luarules/gadgets/unit_weapon_smart_select_helper.lua
user: login: badosu
body: https://github.com/beyond-all-reason/Beyond-All-Reason/issues/4545
created_at: 2025-03-20T17:07:53Z
author_association: CONTRIBUTOR

PR comments:
user: login: SethDGamre
created_at: 2025-02-25T23:44:37Z
body: Totermann on discord says it seems to work fine on his machine
https://discord.com/channels/549281623154229250/1127191779280699442/1344068163230498848
author_association: COLLABORATOR
user: login: WatchTheFort
created_at: 2025-02-28T17:31:14Z
body: Conflicting keybinds should not be part of the default keybinds. What is the reason for this to overlap with On/Off, other than because that's how it used to be? Leaving this unbound by default is fine.
author_association: MEMBER
user: login: SethDGamre
created_at: 2025-03-02T15:47:24Z
body: reverted hotkeys, respomonk agreed. It's bindable if you know what the cmddesc is in the gadget
author_association: COLLABORATOR
user: login: WatchTheFort
created_at: 2025-03-03T21:02:29Z
body: > reverted hotkeys, respomonk agreed. It's bindable if you know what the cmddesc is in the gadget

You didn't push that commit

author_association: MEMBER