Skip to content

bugfix: Fix issue where builders could resume completed tasks after being disabled#2793

Open
Stubbjax wants to merge 8 commits into
TheSuperHackers:mainfrom
Stubbjax:fix-previous-dozer-task
Open

bugfix: Fix issue where builders could resume completed tasks after being disabled#2793
Stubbjax wants to merge 8 commits into
TheSuperHackers:mainfrom
Stubbjax:fix-previous-dozer-task

Conversation

@Stubbjax

Copy link
Copy Markdown

This change fixes an issue introduced by #1870 that allows builders to resume an already completed task after being disabled.

When assigning a new build task to a builder, if the target building is an already-completed building, then the new task is ignored.

Before

BEFORE.mp4

After

AFTER.mp4

@Stubbjax Stubbjax self-assigned this Jun 14, 2026
@Stubbjax Stubbjax added Bug Something is not working right, typically is user facing Major Severity: Minor < Major < Critical < Blocker Gen Relates to Generals ZH Relates to Zero Hour labels Jun 14, 2026
@greptile-apps

greptile-apps Bot commented Jun 14, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes a bug from #1870 where a builder (Dozer/Worker) could resume a completed build task after recovering from EMP, underpowered, or hacked states. The fix moves the "save current task" responsibility out of internalCancelTask and into Object::setDisabledUntil, and adds a setPreviousTask API with a guard against DOZER_TASK_INVALID.

  • Core fix is sound: removing m_previousTask = task from internalCancelTask prevents a completed-build's ID from ever being stored as the task to resume, which closes the reported regression.
  • setPreviousTask guard is correct: the new method returns early on DOZER_TASK_INVALID, eliminating the previously-flagged out-of-bounds array access when a builder is idle at disable time.
  • Build-resume condition is inverted: in all four resumePreviousTask implementations, !target->testStatus(OBJECT_STATUS_UNDER_CONSTRUCTION) evaluates true for a completed building (status cleared at 100%) and false while still under construction — meaning a builder EMP'd mid-build will stay idle after recovery instead of resuming.

Confidence Score: 3/5

The primary structural change correctly prevents the reported regression, but all four resumePreviousTask implementations carry an inverted boolean guard that will prevent builders from resuming an in-progress build after EMP recovery.

The root-cause fix is architecturally correct and closes the completed-build resume bug. However, the !testStatus(OBJECT_STATUS_UNDER_CONSTRUCTION) condition in every resumePreviousTask is backwards: it allows resuming a completed building and blocks resuming an incomplete one — a direct regression of the EMP-during-build scenario that the old code handled correctly. This needs a one-character fix (! removal) in all four implementation files before merging.

All four resumePreviousTask implementations: Generals/.../DozerAIUpdate.cpp, Generals/.../WorkerAIUpdate.cpp, GeneralsMD/.../DozerAIUpdate.cpp, GeneralsMD/.../WorkerAIUpdate.cpp.

Important Files Changed

Filename Overview
Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/DozerAIUpdate.cpp Adds setPreviousTask (correctly guards against INVALID) and refactors resumePreviousTask, but the build-resume guard !testStatus(OBJECT_STATUS_UNDER_CONSTRUCTION) is inverted — newTask fires for complete buildings and is skipped for incomplete ones.
Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/WorkerAIUpdate.cpp Mirrors DozerAIUpdate changes; carries the same inverted !testStatus guard in resumePreviousTask.
GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/DozerAIUpdate.cpp Zero Hour mirror of the Generals Dozer fix; same inverted !testStatus condition present.
GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/WorkerAIUpdate.cpp Zero Hour mirror of the Generals Worker fix; same inverted !testStatus condition present.
Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp Correctly captures the current task via setPreviousTask at the moment of first EMP/underpowered/hacked disable, guarded against double-fire by the existing !isDisabledByType check.
GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp Identical, correct change to Generals/Object.cpp for Zero Hour.
Generals/Code/GameEngine/Include/GameLogic/Module/DozerAIUpdate.h Adds setPreviousTask pure virtual and override declarations; no issues.
Generals/Code/GameEngine/Include/GameLogic/Module/WorkerAIUpdate.h Adds setPreviousTask override declaration; no issues.
GeneralsMD/Code/GameEngine/Include/GameLogic/Module/DozerAIUpdate.h Zero Hour mirror of DozerAIUpdate.h changes; no issues.
GeneralsMD/Code/GameEngine/Include/GameLogic/Module/WorkerAIUpdate.h Zero Hour mirror of WorkerAIUpdate.h changes; no issues.

Sequence Diagram

sequenceDiagram
    participant OBJ as Object::setDisabledUntil
    participant DAU as DozerAIUpdate
    participant ICT as internalCancelTask
    participant RPT as resumePreviousTask

    Note over OBJ: Builder hit by EMP/underpowered/hacked
    OBJ->>DAU: setPreviousTask(getCurrentTask())
    Note over DAU: Guards DOZER_TASK_INVALID,<br/>saves m_previousTask + m_task[task]

    Note over ICT: Task cancelled due to disable
    ICT->>ICT: internalTaskCompleteOrCancelled(task)
    Note over ICT: m_previousTask no longer set here<br/>(removed by this PR)

    Note over RPT: Builder re-enables
    RPT->>RPT: "m_previousTask == INVALID? return"
    RPT->>RPT: "m_previousTask == DOZER_TASK_BUILD?"
    RPT->>RPT: findObjectByID(target)
    Note over RPT: ⚠ Inverted guard: !testStatus(UNDER_CONSTRUCTION)<br/>true=complete → calls newTask (wrong)<br/>false=incomplete → skips newTask (wrong)
    RPT->>DAU: newTask(BUILD, target)
    RPT->>RPT: "Reset m_previousTask = INVALID"
Loading

Reviews (6): Last reviewed commit: "fix: Reverse condition" | Re-trigger Greptile

@xezon xezon added the ThisProject The issue was introduced by this project, or this task is specific to this project label Jun 14, 2026
if( task == DOZER_TASK_BUILD )
{
// TheSuperHackers @bugfix Stubbjax 15/06/2026 Ignore the build task if the building is already complete.
if (target->getConstructionPercent() == CONSTRUCTION_COMPLETE)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Should this perhaps go into DozerAIUpdate::resumePreviousTask and here it should be an assert?

@Stubbjax Stubbjax Jun 14, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good point. I suppose it could seeing as this is the only way it can happen. Having to resolve the target from the task info does make the code a bit more complex though.

@Stubbjax Stubbjax force-pushed the fix-previous-dozer-task branch from 657688e to a0fbee6 Compare June 14, 2026 15:59
@Stubbjax Stubbjax force-pushed the fix-previous-dozer-task branch from de2f3f1 to bdf1ab1 Compare June 14, 2026 16:17
return;

newTask(m_previousTask, TheGameLogic->findObjectByID(m_previousTaskInfo.m_targetObjectID));
m_previousTask = DOZER_TASK_INVALID;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Now m_previousTask is no longer cleared if not DOZER_TASK_BUILD or already completed.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

It is still not cleared if task is not DOZER_TASK_BUILD


DozerAIInterface* dozerAI = getAI() ? getAI()->getDozerAIInterface() : nullptr;
if (dozerAI)
dozerAI->setPreviousTask(dozerAI->getCurrentTask());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This looks a bit strange. Wasn't cancelTask supposed to take of this? For example from Object::onDisabledEdge.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yes, but there was no distinction on how the disabling happened here. A unit entering a transport causes it to become disabled, and thus it would attempt to resume the task after exiting the transport.

@Stubbjax Stubbjax force-pushed the fix-previous-dozer-task branch from 74beb35 to a4ffdb5 Compare June 14, 2026 16:52
{
newTask(m_previousTask, TheGameLogic->findObjectByID(m_previousTaskInfo.m_targetObjectID));
Object* target = TheGameLogic->findObjectByID(m_previousTaskInfo.m_targetObjectID);
if (!target || target->testStatus(OBJECT_STATUS_UNDER_CONSTRUCTION))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why is !target a valid condition for new task?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I should just go to bed. 😅

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

Labels

Bug Something is not working right, typically is user facing Gen Relates to Generals Major Severity: Minor < Major < Critical < Blocker ThisProject The issue was introduced by this project, or this task is specific to this project ZH Relates to Zero Hour

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants