Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
7fcd756
AllowWeaponTarget callin now accepts Allow
SethDGamre Jun 28, 2026
31a2b22
new firemode and renaming structure
SethDGamre Jun 28, 2026
5e52f45
remove defensive behavior logic from firestate handler
SethDGamre Jun 28, 2026
d363fd3
create defensive firestate behavior gadget
SethDGamre Jun 28, 2026
9c4162b
extract dps calc's from gui_info.lua to separate file
SethDGamre Jun 28, 2026
67f7598
mostly base-functional defensive firestate
SethDGamre Jun 29, 2026
53f2cde
factories canReclaim = false
SethDGamre Jun 29, 2026
2a46047
cloak behavior done
SethDGamre Jun 29, 2026
062d351
add customparam for always threats at all range override
SethDGamre Jun 29, 2026
762654b
add alldefs customparam adding for always shoot threats all ranges
SethDGamre Jun 29, 2026
bea6617
always shoot kamikaze, and other stuff
SethDGamre Jun 29, 2026
c1c5b4c
rename to Fire At Will, Defend, and Hold Fire
SethDGamre Jun 29, 2026
2813f67
improve kamikaze, add echo's
SethDGamre Jun 30, 2026
8dd7572
ignore non-threats to self
SethDGamre Jun 30, 2026
c9567d9
fix aa priority bug
SethDGamre Jun 30, 2026
e19f5e7
make a modoption
SethDGamre Jun 30, 2026
3cee359
add Spring.Echo's for debugging threat ranges
SethDGamre Jun 30, 2026
09a8a4b
oneshot math
SethDGamre Jun 30, 2026
9dee9b8
improve modoption name and description
SethDGamre Jun 30, 2026
48aa7ab
rename modoption
SethDGamre Jun 30, 2026
3ca70a7
remove debug echos
SethDGamre Jun 30, 2026
4faa885
DPS -> time to kill based on # of shots rework
SethDGamre Jul 1, 2026
6b4d5a5
add drone carriers to always threat list
SethDGamre Jul 1, 2026
5724b8c
rename defense -> defend variables
SethDGamre Jul 1, 2026
6fb5bbf
working, best optimized so far, rename defense->defend
SethDGamre Jul 2, 2026
4a9db55
various improvements
SethDGamre Jul 2, 2026
d24a7f8
separate out threat calcs into a separate file
SethDGamre Jul 2, 2026
7b0e00a
code cleanup
SethDGamre Jul 2, 2026
aea5e3f
more cleanup
SethDGamre Jul 2, 2026
01c3914
unhallucinate gadgets.lua
SethDGamre Jul 3, 2026
3d3293a
reduce direct alterations of gui_ordermenu.lua
SethDGamre Jul 3, 2026
66b5ba1
revert canReclaim changes to factories, will be separate PR
SethDGamre Jul 3, 2026
e7276ac
more descriptive tooltips
SethDGamre Jul 3, 2026
7c2521a
gadgets.lua priority -> result rename, fixed inaccurate comment
SethDGamre Jul 3, 2026
6637ca4
improve tooltip for defend
SethDGamre Jul 3, 2026
7daeb90
Add factory firestate inheritance
SethDGamre Jul 3, 2026
3df04b0
more improvements to threat calcs and qualifiers
SethDGamre Jul 3, 2026
6cc0f32
polish up firestate gui, add keybind support
SethDGamre Jul 3, 2026
e929c9c
Merge branch 'master' into return-fire-rework-2
SethDGamre Jul 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions gamedata/alldefs_post.lua
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,28 @@ local function unitDef_Post(name, uDef)
end
end

-- Defend firestate: aircraft, long-range, starburst, and drone carrier units engage threats at any range in defend mode
-- regardless of calculated threat distance (see luarules/gadgets/unit_defend_firestate.lua).
--DEFEND FIRESTATE REWORK: Remove modoption guard; always set defend_never_hesitate for qualifying units
if modOptions.experimental_defend_firestate then
local DEFEND_NEVER_HESITATE_RANGE = 2000

if not customparams.defend_never_hesitate and next(weapondefs) then
for _, weaponDef in pairs(weapondefs) do
if not weaponDef.customparams.bogus then
if uDef.canfly
or weaponDef.weapontype == "StarburstLauncher"
or (weaponDef.range and weaponDef.range > DEFEND_NEVER_HESITATE_RANGE)
or (weaponDef.customparams.carried_unit and weaponDef.customparams.carried_unit ~= "")
then
customparams.defend_never_hesitate = true
break
end
end
end
end
end

-- Suppress engine default piece explosion effects (handled by gfx_death_fire_smoke_gl4 widget)
if not uDef.sfxtypes then
uDef.sfxtypes = {}
Expand Down
11 changes: 9 additions & 2 deletions language/en/interface.json
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,14 @@
"Fire at will": "Fire at will",
"Hold fire": "Hold fire",
"Return fire": "Return fire",
"firestate_tooltip": "Set under what conditions a unit should start firing at enemies (without explicit attack order)",
"Defend": "Defend",
"Fire at all": "Fire At All",
"firestate_hold_fire_descr": "Don't acquire targets without orders",
"firestate_return_fire_descr": "Return fire when attacked",
"firestate_defend_descr": "Fire upon enemies when they individually become a threat",
"firestate_fire_at_will_descr": "Fire upon enemies within range",
"firestate_fire_at_all_descr": "Fire upon anything belonging to the enemy within range",
"firestate_tooltip": "Set under what conditions a unit should start firing at enemies (without explicit attack order).",
"Hold pos": "Hold pos",
"Maneuver": "Maneuver",
"Roam": "Roam",
Expand Down Expand Up @@ -1683,7 +1690,7 @@
"unitidlebuildericons": "Unit idle builder icons",
"unitidlebuildericons_descr": "Shows a sleep icon above builder units that are idle.",
"unitfirestateicons": "Unit firing state icons",
"unitfirestateicons_descr": "Shows an icon above units that are either set on Hold Fire or Return Fire.",
"unitfirestateicons_descr": "Shows an icon above units that are either set on Hold fire or Defend.",
"resurrectionhalos": "Resurrected unit halos",
"resurrectionhalos_descr": "Adds halos above resurrected units.",
"nametags_rank": "Commander name tag rank icon",
Expand Down
15 changes: 6 additions & 9 deletions luarules/gadgets.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1712,30 +1712,27 @@ end
function gadgetHandler:AllowWeaponTarget(attackerID, targetID, attackerWeaponNum, attackerWeaponDefID, defPriority)
-- Calls with no input priority are pure pass/fail tests.
-- These are common, and BAR never disallows any of them.
if not defPriority then
return true -- The second return value is never used.
end
-- if not defPriority then
-- return true -- The second return value is never used.
-- end

local allowed = true
local result = 1.0

if targetID == -1 and attackerWeaponNum == -1 then
-- The `targetPriority` return value is actually the autotarget search radius,
-- The `defPriority` return value here is actually the autotarget search radius,
-- and applies to the unit's targeting search for its command AI, not weapons.
for _, g in ipairs(self.UnitAutoTargetRangeList) do
defPriority = g:UnitAutoTargetRange(attackerID, defPriority)
end

allowed, result = defPriority > 0, defPriority
else
-- The actual callin. BAR only uses AllowWeaponTarget for the target priority.
for _, g in ipairs(self.AllowWeaponTargetList) do
local targetPriority = g:AllowWeaponTarget(attackerID, targetID, attackerWeaponNum, attackerWeaponDefID, defPriority)
if targetPriority then
result = targetPriority
end
allowed, result = g:AllowWeaponTarget(attackerID, targetID, attackerWeaponNum, attackerWeaponDefID, defPriority)
end
end

return allowed, result
end

Expand Down
3 changes: 2 additions & 1 deletion luarules/gadgets/unit_aa_targeting_priority.lua
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ if gadgetHandler:IsSyncedCode() then
function gadget:AllowWeaponTarget(unitID, targetID, attackerWeaponNum, attackerWeaponDefID, defPriority)
local mult = airPriorityMultiplier[spGetUnitDefID(targetID)]
if mult then
return defPriority * mult
return true, (defPriority or 1.0) * mult
end
return true
end
end
249 changes: 249 additions & 0 deletions luarules/gadgets/unit_defend_firestate.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
--DEFEND FIRESTATE REWORK: Remove guard; defend targeting is always required
if not Spring.GetModOptions().experimental_defend_firestate then
return
end

local gadget = gadget ---@type Gadget

function gadget:GetInfo()
return {
name = "Firestate Defend",
desc = "Limits defend firestate to nearby targets",
author = "SethDGamre",
date = "2026.06.28",
license = "GNU GPL, v2 or later",
layer = 0,
enabled = true
}
end

if not gadgetHandler:IsSyncedCode() then
return false
end

--increase safety margin buffer so a pawn can walk through a minefield that's exposed

local Firestates = VFS.Include("modules/firestates.lua")
local WeaponThreat = VFS.Include("modules/weaponthreat.lua")
local CMD_FIRE_STATE = CMD.FIRE_STATE
local ALWAYS_SHOOT = WeaponThreat.ALWAYS_SHOOT
local NO_THREAT = WeaponThreat.NO_THREAT
local HP_CHECK_INTERVAL_FRAMES = Game.gameSpeed * 3
local MIN_RADAR_DEFPRIORITY = 10000000 -- this is the floor of what a radar covered unit will generate for defpriorirty. If below this, it's certainly in LOS. For performance
local UNIT_DEF_ID = 1
local IS_DEFEND = 2
local NEVER_HESITATE = 3
local WEAPON_DEF_IDS = 4
local LAST_HEALTH = 5
local HP_CHECK_FRAME = 6
local RADAR_AGGRO = 7
local CLOAKED = 8
local ALWAYS_HARMLESS = 9
local defThreatRanges = {}
local neverHesitateAttackers = {}
local alwaysHarmlessUnitDefs = {}
local weaponWatchRefCount = {}
local watchedWeaponsByUnitDef = {}
local metaData = {}
local gameFrame = 0

local spGetUnitDefID = Spring.GetUnitDefID
local spGetUnitRulesParam = Spring.GetUnitRulesParam
local spGetUnitIsCloaked = Spring.GetUnitIsCloaked
local spGetAllUnits = Spring.GetAllUnits
local spGetUnitHealth = Spring.GetUnitHealth
local spGetUnitSeparation = Spring.GetUnitSeparation

local function addWeaponWatches(weaponDefIDs)
for index = 1, #weaponDefIDs do
local weaponDefID = weaponDefIDs[index]
local refCount = (weaponWatchRefCount[weaponDefID] or 0) + 1
weaponWatchRefCount[weaponDefID] = refCount
if refCount == 1 then
Script.SetWatchAllowTarget(weaponDefID, true)

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.

See notes on AllowWeaponTarget in unit_aa_targeting_priority.lua.

end
end
end

local function removeWeaponWatches(weaponDefIDs)
for index = 1, #weaponDefIDs do
local weaponDefID = weaponDefIDs[index]
local refCount = (weaponWatchRefCount[weaponDefID] or 0) - 1
if refCount <= 0 then
weaponWatchRefCount[weaponDefID] = nil
Script.SetWatchAllowTarget(weaponDefID, false)
else
weaponWatchRefCount[weaponDefID] = refCount
end
end
end

local function setDefendWatch(unitID, isDefend)
local meta = metaData[unitID]
if not meta then
return
end

if isDefend then
if meta[IS_DEFEND] then
meta[LAST_HEALTH] = spGetUnitHealth(unitID)
return
end

local unitDefID = meta[UNIT_DEF_ID]
local weaponDefIDs = watchedWeaponsByUnitDef[unitDefID]
meta[IS_DEFEND] = true
meta[NEVER_HESITATE] = neverHesitateAttackers[unitDefID] or false
meta[WEAPON_DEF_IDS] = weaponDefIDs
meta[LAST_HEALTH] = spGetUnitHealth(unitID)

if weaponDefIDs then
addWeaponWatches(weaponDefIDs)
end
elseif meta[IS_DEFEND] then
if meta[WEAPON_DEF_IDS] then
removeWeaponWatches(meta[WEAPON_DEF_IDS])
end
meta[IS_DEFEND] = nil
meta[WEAPON_DEF_IDS] = nil
meta[RADAR_AGGRO] = nil
meta[HP_CHECK_FRAME] = nil
end
end

local function checkDefendUnitHealth(attackerID, meta)
local nextCheckFrame = meta[HP_CHECK_FRAME]
if nextCheckFrame and gameFrame < nextCheckFrame then
return
end

local currentHealth = spGetUnitHealth(attackerID)
if not currentHealth then
return
end

local lastHealth = meta[LAST_HEALTH]
if lastHealth and currentHealth < lastHealth then
meta[RADAR_AGGRO] = true
elseif lastHealth and currentHealth > lastHealth then
meta[RADAR_AGGRO] = nil
end

meta[LAST_HEALTH] = currentHealth
meta[HP_CHECK_FRAME] = gameFrame + HP_CHECK_INTERVAL_FRAMES
end

local function updateDefendWatchFromRulesParam(unitID)
local state = spGetUnitRulesParam(unitID, Firestates.RULES_PARAM)
setDefendWatch(unitID, state == Firestates.DEFEND)
end

local function createUnitMeta(unitDefID)
local meta = { [UNIT_DEF_ID] = unitDefID }
if alwaysHarmlessUnitDefs[unitDefID] then
meta[ALWAYS_HARMLESS] = true
end
return meta
end

function gadget:UnitCommand(unitID, unitDefID, unitTeamID, cmdID, cmdParams, cmdOptions, cmdTag, playerID, fromSynced, fromLua)
if cmdID == CMD_FIRE_STATE then
updateDefendWatchFromRulesParam(unitID)
end
end

function gadget:UnitCreated(unitID, unitDefID, unitTeam)
metaData[unitID] = createUnitMeta(unitDefID)
end

function gadget:UnitFinished(unitID, unitDefID, unitTeam)
updateDefendWatchFromRulesParam(unitID)
end

function gadget:UnitCloaked(unitID, unitDefID, unitTeam)
local meta = metaData[unitID]
if meta then
meta[CLOAKED] = true
end
end

function gadget:UnitDecloaked(unitID, unitDefID, unitTeam)
local meta = metaData[unitID]
if meta then
meta[CLOAKED] = nil
end
end

function gadget:UnitDestroyed(unitID, unitDefID, unitTeam)
local meta = metaData[unitID]
if meta and meta[WEAPON_DEF_IDS] then
removeWeaponWatches(meta[WEAPON_DEF_IDS])
end
metaData[unitID] = nil
end

function gadget:GameFrame(frame)
gameFrame = frame
end

function gadget:AllowWeaponTarget(attackerID, targetID, attackerWeaponNum, attackerWeaponDefID, defPriority)
local targetMeta = metaData[targetID]
if not targetMeta then
return false
end

local attackerMeta = metaData[attackerID]
local isDefend = attackerMeta and attackerMeta[IS_DEFEND]

if targetMeta[ALWAYS_HARMLESS] then
return not isDefend
end

if not isDefend then
return true
end

local rangesForWeapon = defThreatRanges[attackerWeaponDefID]
if not rangesForWeapon then
return true
end

local threatRange = rangesForWeapon[targetMeta[UNIT_DEF_ID]]
if not threatRange or threatRange == NO_THREAT then
return false
end

if attackerMeta[CLOAKED] then
return false
end

if attackerMeta[NEVER_HESITATE] then
return true
end

if (defPriority or 0) > MIN_RADAR_DEFPRIORITY then
checkDefendUnitHealth(attackerID, attackerMeta)
return attackerMeta[RADAR_AGGRO] or false
end

if threatRange == ALWAYS_SHOOT then
return true
end

local separation = spGetUnitSeparation(attackerID, targetID)
return not separation or separation <= threatRange
end

function gadget:Initialize()
defThreatRanges, watchedWeaponsByUnitDef, neverHesitateAttackers, alwaysHarmlessUnitDefs = WeaponThreat.buildDefendData()

for _, unitID in ipairs(spGetAllUnits()) do
local unitDefID = spGetUnitDefID(unitID)
local meta = createUnitMeta(unitDefID)
metaData[unitID] = meta
updateDefendWatchFromRulesParam(unitID)
if spGetUnitIsCloaked(unitID) then
meta[CLOAKED] = true
end
end
end
Loading
Loading