Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions config/fxdata/lua/libraries/patrols.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
-- patrols.lua
-- Contains functions to assign individual heroes or parties to patrol locations

local function is_close_enough(creature, target)
return creature.pos.stl_y < target.stl_y+2 and
creature.pos.stl_y > target.stl_y-2 and
creature.pos.stl_x < target.stl_x+2 and
creature.pos.stl_x > target.stl_x-2
end

local function UpdatePatrol(patrol)
if patrol.leader == nil then
return
end
if patrol.leader.state ~= "MoveToPosition" and patrol.leader.state ~= "GoodDoingNothing" and patrol.leader.state ~= "CreatureDoingNothing" then
return
end

local target = patrol.positions[patrol.next_post]

if is_close_enough(patrol.leader, target) then
patrol.next_post = (patrol.next_post % #patrol.positions) + 1
target = patrol.positions[patrol.next_post]
end
if (patrol.leader.moveto_pos.stl_x ~= target.stl_x or patrol.leader.moveto_pos.stl_y ~= target.stl_y) then
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

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

Potential nil access error. If the creature has not been given any movement orders yet, moveto_pos might be nil or uninitialized, causing this line to error when accessing moveto_pos.stl_x or moveto_pos.stl_y. Consider adding a nil check or initializing moveto_pos to a safe default value when first creating the patrol.

Suggested change
if (patrol.leader.moveto_pos.stl_x ~= target.stl_x or patrol.leader.moveto_pos.stl_y ~= target.stl_y) then
local moveto_pos = patrol.leader.moveto_pos
if (moveto_pos == nil
or moveto_pos.stl_x ~= target.stl_x
or moveto_pos.stl_y ~= target.stl_y) then

Copilot uses AI. Check for mistakes.
patrol.leader:walk_to(target.stl_x,target.stl_y)
patrol.leader.state = "MoveToPosition"
patrol.leader.continue_state = "GoodDoingNothing"
end
end

function UpdatePatrols()
for _, patrol in ipairs(Game.patrols) do
UpdatePatrol(patrol)
end
end


local function InitializePatrols()
RegisterTimerEvent(UpdatePatrols, 17, true)
Game.patrols = {}
end


function LeaderDeath(eventData,triggerData)
local patrol = Game.patrols[triggerData.patrol_idx]
if patrol == nil then
return
end

if patrol.partybackup then
patrol.leader = patrol.partybackup.party[1]
if patrol.leader == nil then
patrol.leader = patrol.partybackup
end
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

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

In the LeaderDeath function, when a new leader is assigned and a new backup is set, the old trigger for the deceased leader is not removed before registering a new one. This creates orphaned triggers in the Game.triggers table. The trigger is only removed in the else branch at line 69 when there's no backup, but when there is a backup (line 56), a new trigger is created without removing the old one first, causing trigger accumulation.

Suggested change
end
end
-- Remove old leader death trigger before registering a new one
RemoveTrigger(triggerData.trigger)

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +55
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

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

The succession logic is confusing. According to Creature.lua line 9, party[1] is always the leader of the party. So patrol.partybackup.party[1] would be partybackup itself if partybackup is a party leader. The condition at line 53 will always be false if partybackup has a party (since party[1] == partybackup), making this check unnecessary. The intent appears to be to promote partybackup to the new leader, which should be done directly without the intermediate check.

Suggested change
patrol.leader = patrol.partybackup.party[1]
if patrol.leader == nil then
patrol.leader = patrol.partybackup
end
-- Promote the backup unit directly to be the new leader
patrol.leader = patrol.partybackup

Copilot uses AI. Check for mistakes.
local trigger = RegisterCreatureDeathEvent(LeaderDeath, patrol.leader)
trigger.triggerData.patrol_idx = triggerData.patrol_idx

-- Update backup to the next party member
if patrol.leader.party[2] then
patrol.partybackup = patrol.leader.party[2]
Comment on lines +60 to +61
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

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

Potential nil access when checking patrol.leader.party. If patrol.leader exists but doesn't have a party field (or party is nil), accessing party[2] will cause an error. Although Creature.lua defines party as a field, it may not always be initialized for all creatures. Consider adding a check to ensure party exists before accessing party[2].

Copilot uses AI. Check for mistakes.
local trigger2 = RegisterCreatureDeathEvent(BackupDeath, patrol.partybackup)
trigger2.triggerData.patrol_idx = triggerData.patrol_idx
else
patrol.partybackup = nil
end
else

RemoveTrigger(triggerData.trigger)
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

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

The code attempts to remove the trigger using triggerData.trigger, but triggerData.trigger is never set anywhere in this file. The trigger system doesn't automatically populate triggerData with a reference to the trigger object. This means RemoveTrigger is being called with nil, which will fail to remove the trigger and cause a memory leak. The trigger reference needs to be stored in triggerData when it's created, or the trigger should be removed using a different mechanism.

Copilot uses AI. Check for mistakes.
end
Comment on lines +67 to +70
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

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

When the leader dies and there is no backup, the old trigger for the dead leader is removed but the patrol remains in the Game.patrols table. The UpdatePatrol function will continue to be called for this patrol, but will do nothing since patrol.leader is nil. However, the patrol entry remains in memory indefinitely. Consider removing the patrol from the Game.patrols table when there are no more creatures to continue it, to prevent accumulation of dead patrol entries.

Copilot uses AI. Check for mistakes.
end

function BackupDeath(eventData,triggerData)
local patrol = Game.patrols[triggerData.patrol_idx]
if patrol == nil then
return
end

if triggerData.unit == patrol.leader then
-- Leader died, ignore backup death
return
end

if patrol.leader and patrol.leader.party[2] then
patrol.partybackup = patrol.leader.party[2]
Comment on lines +84 to +85
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

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

Potential nil access when checking patrol.leader.party. If patrol.leader exists but doesn't have a party field (or party is nil), accessing party[2] will cause an error. Although Creature.lua defines party as a field, it may not always be initialized for all creatures. Consider adding a check to ensure party exists before accessing party[2].

Copilot uses AI. Check for mistakes.
local trigger2 = RegisterCreatureDeathEvent(BackupDeath, patrol.partybackup)
trigger2.triggerData.patrol_idx = triggerData.patrol_idx
Comment on lines +84 to +87
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

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

When the old backup creature dies and a new backup is assigned from the leader's party, the old trigger for the deceased backup is not removed. This will cause the BackupDeath trigger to remain in the Game.triggers table even though the creature is dead, leading to a memory leak of unused triggers. The trigger should be removed before registering a new one, similar to how it's handled in the LeaderDeath function at line 69.

Copilot uses AI. Check for mistakes.
else
patrol.partybackup = nil
end

end
Comment on lines +73 to +92
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

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

The BackupDeath trigger is never removed when the backup dies. Unlike LeaderDeath which removes the trigger at line 69 when there's no backup left, BackupDeath doesn't clean up its own trigger. This means when a backup creature dies, their death trigger remains in the Game.triggers table indefinitely, causing a memory leak. The trigger should be removed using RemoveTrigger(triggerData.trigger) before potentially registering a new one or when setting partybackup to nil.

Copilot uses AI. Check for mistakes.

---makes a party patrol between given points
---@param leader Creature
---@param patrolPoints table list of {stl_x = integer, stl_y = integer}
---@param next_post? integer if the patrol should start at a different point than 1
---@param patrol_name? string optional name for the patrol
function RegisterPatrol(leader, patrolPoints,next_post,patrol_name)
if Game.patrols == nil then
InitializePatrols()
end

if next_post == nil then
next_post = 1
end

if patrol_name == nil then
patrol_name = "Patrol "..tostring(#Game.patrols + 1)
end

table.insert(Game.patrols, { leader = leader, positions = patrolPoints, next_post = next_post, name = patrol_name } )

local trigger = RegisterCreatureDeathEvent(LeaderDeath, leader)
trigger.triggerData.patrol_idx = #Game.patrols

if leader.party[2] ~= nil then
local partybackup = leader.party[2]
Comment on lines +117 to +118
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

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

Potential nil access when checking leader.party. If leader doesn't have a party field (or party is nil), accessing party[2] will cause an error. Although Creature.lua defines party as a field, it may not always be initialized for all creatures. Consider adding a check to ensure party exists before accessing party[2].

Copilot uses AI. Check for mistakes.
local trigger2 = RegisterCreatureDeathEvent(BackupDeath, partybackup)
Game.patrols[#Game.patrols].partybackup = partybackup
trigger2.triggerData.patrol_idx = #Game.patrols
end
end