Skip to content

Conversation

@lL1l1
Copy link
Contributor

@lL1l1 lL1l1 commented Aug 22, 2025

Reverts #6835

Desyncs still happen, so this must not be the source of the desyncs. It would be nice to have the changes anyway.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed factories not immediately starting queued builds after completing an upgrade.
    • Fixed factories not resuming builds when a currently building land or navy unit is cancelled.
    • Factories now become operational immediately after upgrades or cancellations, avoiding build delays and improving build flow.

✏️ Tip: You can customize this high-level summary in your review settings.

@github-actions github-actions bot marked this pull request as draft August 22, 2025 03:40
@coderabbitai
Copy link

coderabbitai bot commented Nov 27, 2025

Walkthrough

Adds a changelog entry and updates factory upgrade handling by adding an UpgradingState to FactoryUnit, clearing build-area collision shapes for completed or failed builds, and destroying the old factory instance to allow immediate operation of the upgraded factory.

Changes

Cohort / File(s) Summary
Changelog Entry
changelog/snippets/fix.6912.md
Adds a changelog entry documenting the re-application of a fix for factory build-queue behavior around upgrades and cancelled builds.
Factory upgrade & build-area handling
lua/sim/units/FactoryUnit.lua
Adds UpgradingState (derived from StructureUnit.UpgradingState) with an OnStopBuild override that finalizes upgrades, clears collision shapes, plays sounds, stops effects, and destroys the old factory; updates OnFailedToBuild to clear collision shapes immediately when a building unit is dead/destroyed.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Areas to focus on:
    • UpgradingState.OnStopBuild — correctness of state transitions, calls to base methods, and sequence of effects/sound/destruction.
    • Collision shape clearing — race conditions with concurrent build/cancel operations and synchronization with the new factory instance.
    • Interaction with StructureUnit base behavior — ensure no regressions in other upgrade paths.

Suggested labels

type: bug, area: sim

Poem

🐰
I nibbled through code, hop-hop, with glee,
Cleared the build spots so factories breathe free,
Old shells vanish, new gears start to hum,
Queues flow smooth — a spring in my thumb,
Hooray for upgrades, now let's all run!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description check ⚠️ Warning The description is incomplete. While it provides context about reverting a previous revert, it lacks detail on testing, lacks proper issue linking, and doesn't follow the template structure with required sections. Add a 'Description of proposed changes' section explaining the fix, include 'Testing done' with verification steps, and properly link related issues using the format 'Fixes #XXXX' or 'Closes #XXXX'.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: reapplying a fix for factory behavior after upgrades or unit cancellations.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch revert-6835-revert-6692-Fix/factory-upgrade-delayed-building

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fed99bb and d8dc669.

📒 Files selected for processing (1)
  • lua/sim/units/FactoryUnit.lua (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
lua/sim/units/FactoryUnit.lua (3)
engine/Core.lua (1)
  • IsDestroyed (237-238)
lua/system/class.lua (1)
  • State (138-166)
engine/Sim.lua (1)
  • NotifyUpgrade (1050-1051)
🔇 Additional comments (2)
lua/sim/units/FactoryUnit.lua (2)

2-3: LGTM!

The imports follow the existing pattern in this file for upvalue optimization and are required by the new UpgradingState.OnStopBuild implementation.


475-497: LGTM - Well-structured upgrade state handling.

The implementation correctly:

  1. Extends StructureUnit.UpgradingState using the State() pattern
  2. Calls UnitOnStopBuild (intentionally bypassing StructureUnit's version to customize behavior)
  3. Clears the old factory's collision shape before destroying it, unblocking the build area for the new factory

This properly addresses the PR objective of allowing the upgraded factory to immediately start building queued units.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@lL1l1 lL1l1 marked this pull request as ready for review November 27, 2025 22:41
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a32f1c4 and fed99bb.

📒 Files selected for processing (2)
  • changelog/snippets/fix.6912.md (1 hunks)
  • lua/sim/units/FactoryUnit.lua (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
lua/sim/units/FactoryUnit.lua (2)
lua/system/class.lua (1)
  • State (138-166)
engine/Sim.lua (1)
  • NotifyUpgrade (1050-1051)
🔇 Additional comments (3)
changelog/snippets/fix.6912.md (1)

1-1: LGTM!

The changelog entry is clear, concise, and provides appropriate context about the fix being re-applied and the desync investigation findings.

lua/sim/units/FactoryUnit.lua (2)

2-3: LGTM!

The imports follow the established pattern in this file for upvaluing parent class methods for performance, and UnitOnStopBuild is appropriately used in the new UpgradingState.OnStopBuild implementation.


472-494: I'm unable to clone the repository to verify the review comment through code inspection. However, I can provide guidance based on what we know:

The review comment raises a valid defensive programming concern about calling unitBuilding:GetFractionComplete() without prior nil/destroyed checks. This is a reasonable concern in game engine code where units can be destroyed or cancelled mid-upgrade.

However, without access to the codebase, I cannot:

  1. Verify how StructureUnit.UpgradingState.OnStopBuild handles this
  2. Confirm if IsDestroyed() is actually used in this codebase
  3. Check what defensive patterns are used elsewhere in FactoryUnit.lua

Rewritten review comment:

Guard against nil/destroyed unitBuilding before calling GetFractionComplete().

At line 481, unitBuilding:GetFractionComplete() is called without verifying the unit is still valid. While normal upgrade completion is safe, edge cases like upgrade cancellation or unit destruction during the transition could cause crashes.

         OnStopBuild = function(self, unitBuilding, order)
             UnitOnStopBuild(self, unitBuilding, order)
             self:EnableDefaultToggleCaps()

-            if unitBuilding:GetFractionComplete() == 1 then
+            if unitBuilding and not IsDestroyed(unitBuilding)
+                and unitBuilding:GetFractionComplete() == 1
+            then
                 NotifyUpgrade(self, unitBuilding)

Verify this mirrors defensive patterns used elsewhere in this file and confirm IsDestroyed() is the correct utility function for this codebase.

Comment on lines 204 to 205
-- Instantly clear the build area so the next build can start, since unit `Destroy` doesn't do so.
self.UnitBeingBuilt:SetCollisionShape('None')
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let me examine the FactoryUnit.lua file to understand the context around lines 204-205
cd /tmp && git clone --depth 1 https://github.com/FAForever/fa.git 2>/dev/null || true
cat -n lua/sim/units/FactoryUnit.lua | sed -n '195,215p'

Repository: FAForever/fa

Length of output: 168


🏁 Script executed:

#!/bin/bash
# Search for OnFailedToBuild definition and where it's called from
rg -n "OnFailedToBuild" --type lua -B 3 -A 10

Repository: FAForever/fa

Length of output: 168


🏁 Script executed:

#!/bin/bash
# Check how UnitBeingBuilt is assigned and cleared in this file
rg -n "UnitBeingBuilt\s*=" --type lua -B 2 -A 2

Repository: FAForever/fa

Length of output: 168


🏁 Script executed:

#!/bin/bash
# Look for Destroy calls on UnitBeingBuilt to understand when it might become nil
rg -n "UnitBeingBuilt.*Destroy\|Destroy.*UnitBeingBuilt" --type lua -B 3 -A 3

Repository: FAForever/fa

Length of output: 38


🏁 Script executed:

#!/bin/bash
# Check if there's a repository already cloned/available
pwd
ls -la
git remote -v 2>/dev/null || echo "Not a git repo in current directory"

Repository: FAForever/fa

Length of output: 3049


🏁 Script executed:

#!/bin/bash
# Search for FactoryUnit.lua in standard locations
fd "FactoryUnit.lua" / 2>/dev/null | head -20

Repository: FAForever/fa

Length of output: 1207


🏁 Script executed:

#!/bin/bash
# Read the context around lines 204-205 in FactoryUnit.lua
cat -n /home/jailuser/git/lua/sim/units/FactoryUnit.lua | sed -n '195,220p'

Repository: FAForever/fa

Length of output: 1250


🏁 Script executed:

#!/bin/bash
# Find the OnFailedToBuild function definition
rg -n "OnFailedToBuild\s*=" /home/jailuser/git/lua/sim/units/FactoryUnit.lua -A 15

Repository: FAForever/fa

Length of output: 1403


🏁 Script executed:

#!/bin/bash
# Check all places where UnitBeingBuilt is set to nil
rg -n "UnitBeingBuilt\s*=" /home/jailuser/git/lua/sim/units/FactoryUnit.lua -B 2 -A 2

Repository: FAForever/fa

Length of output: 666


🏁 Script executed:

#!/bin/bash
# Look for the Build and OnBuildEnd functions to understand lifecycle
rg -n "function.*Build\|OnBuild" /home/jailuser/git/lua/sim/units/FactoryUnit.lua -B 1 -A 5 | head -100

Repository: FAForever/fa

Length of output: 38


🏁 Script executed:

#!/bin/bash
# Find where UnitBeingBuilt is assigned
rg -n "self\.UnitBeingBuilt\s*=" /home/jailuser/git/lua/sim/units/FactoryUnit.lua -B 1 -A 1

Repository: FAForever/fa

Length of output: 38


🏁 Script executed:

#!/bin/bash
# Look for the Build function or StartBuild
rg -n "function.*Build\|Build\s*=\s*function" /home/jailuser/git/lua/sim/units/FactoryUnit.lua -B 1 -A 10 | head -80

Repository: FAForever/fa

Length of output: 38


🏁 Script executed:

#!/bin/bash
# Search for when UnitBeingBuilt could be nil or cleared
rg -n "UnitBeingBuilt.*nil\|nil.*UnitBeingBuilt" /home/jailuser/git/lua/sim/units/FactoryUnit.lua -B 2 -A 2

Repository: FAForever/fa

Length of output: 38


🏁 Script executed:

#!/bin/bash
# Look at the full OnBuildEnd and related functions to understand the build lifecycle
cat -n /home/jailuser/git/lua/sim/units/FactoryUnit.lua | sed -n '300,400p'

Repository: FAForever/fa

Length of output: 4119


🏁 Script executed:

#!/bin/bash
# Search for where UnitBeingBuilt is first assigned or created
rg -n "UnitBeingBuilt" /home/jailuser/git/lua/sim/units/FactoryUnit.lua | head -40

Repository: FAForever/fa

Length of output: 857


🏁 Script executed:

#!/bin/bash
# Look at lines 100-160 to find Build or related functions
cat -n /home/jailuser/git/lua/sim/units/FactoryUnit.lua | sed -n '100,160p'

Repository: FAForever/fa

Length of output: 3002


🏁 Script executed:

#!/bin/bash
# Search parent class StructureUnit for OnFailedToBuild
rg -n "OnFailedToBuild" /home/jailuser/git/lua/sim/units/StructureUnit.lua -B 2 -A 10

Repository: FAForever/fa

Length of output: 1166


🏁 Script executed:

#!/bin/bash
# Check what StructureUnitOnFailedToBuild does
rg -n "OnFailedToBuild" /home/jailuser/git/lua/sim/units/ --type lua -B 2 -A 5 | grep -A 10 "StructureUnit"

Repository: FAForever/fa

Length of output: 6950


🏁 Script executed:

#!/bin/bash
# Search for calls to OnFailedToBuild across the codebase
rg -n "OnFailedToBuild\(" /home/jailuser/git/lua/sim/ --type lua -B 3 -A 1 | head -100

Repository: FAForever/fa

Length of output: 7371


🏁 Script executed:

#!/bin/bash
# Look at the parent class Unit's OnFailedToBuild
rg -n "OnFailedToBuild" /home/jailuser/git/lua/sim/unit.lua -B 2 -A 10 | head -50

Repository: FAForever/fa

Length of output: 131


🏁 Script executed:

#!/bin/bash
# Check if there's a Build function in FactoryUnit or parent classes that sets UnitBeingBuilt
rg -n "function.*Build\s*\(" /home/jailuser/git/lua/sim/units/FactoryUnit.lua -B 2 -A 15 | head -150

Repository: FAForever/fa

Length of output: 38


🏁 Script executed:

#!/bin/bash
# Look for where the build thread fails and calls OnFailedToBuild
rg -n "Build\|Failed" /home/jailuser/git/lua/sim/units/FactoryUnit.lua | grep -i "failed\|function" | head -30

Repository: FAForever/fa

Length of output: 38


🏁 Script executed:

#!/bin/bash
# Look for where UnitBeingBuilt is first assigned in StructureUnit or factory
rg -n "self\.UnitBeingBuilt\s*=" /home/jailuser/git/lua/sim/units/StructureUnit.lua -B 3 -A 1 | head -100

Repository: FAForever/fa

Length of output: 224


🏁 Script executed:

#!/bin/bash
# Look for the Build method in StructureUnit
cat -n /home/jailuser/git/lua/sim/units/StructureUnit.lua | sed -n '1,100p'

Repository: FAForever/fa

Length of output: 4907


🏁 Script executed:

#!/bin/bash
# Search for Build in StructureUnit 
rg -n "Build\s*=\s*function" /home/jailuser/git/lua/sim/units/StructureUnit.lua -A 30 | head -100

Repository: FAForever/fa

Length of output: 2748


🏁 Script executed:

#!/bin/bash
# Look more broadly for build-related functions that set UnitBeingBuilt
rg -n "UnitBeingBuilt" /home/jailuser/git/lua/sim/units/StructureUnit.lua | head -50

Repository: FAForever/fa

Length of output: 241


🏁 Script executed:

#!/bin/bash
# Look at Unit.OnStartBuild around line 2849 to see the flow
rg -n "OnStartBuild.*function\|OnFailedToBuild" /home/jailuser/git/lua/sim/Unit.lua | grep -A 2 -B 2 "2849\|OnStartBuild"

Repository: FAForever/fa

Length of output: 38


🏁 Script executed:

#!/bin/bash
# Read Unit.lua around line 2849
cat -n /home/jailuser/git/lua/sim/Unit.lua | sed -n '2830,2870p'

Repository: FAForever/fa

Length of output: 2473


🏁 Script executed:

#!/bin/bash
# Search for where OnFailedToBuild is called in Unit.lua
rg -n "OnFailedToBuild\|OnStartBuild" /home/jailuser/git/lua/sim/Unit.lua | head -20

Repository: FAForever/fa

Length of output: 38


🏁 Script executed:

#!/bin/bash
# Confirm the flow: when does OnFailedToBuild get called vs when UnitBeingBuilt is assigned
cat -n /home/jailuser/git/lua/sim/units/StructureUnit.lua | sed -n '593,650p'

Repository: FAForever/fa

Length of output: 3025


🏁 Script executed:

#!/bin/bash
# Check if there are any other early returns or conditions in StructureUnit.OnStartBuild
cat -n /home/jailuser/git/lua/sim/units/StructureUnit.lua | sed -n '593,650p' | grep -E "return|if|end"

Repository: FAForever/fa

Length of output: 1425


Based on my verification, I have confirmed the issue. The execution path is:

  1. Unit.OnStartBuild (Unit.lua line 2849) can call self:OnFailedToBuild() when a restricted unit is attempted
  2. This returns false (line 2852)
  3. StructureUnit.OnStartBuild (line 595) detects this false return and immediately exits at line 596
  4. Critically, self.UnitBeingBuilt is only assigned at line 598, which is NEVER reached when the function returns early
  5. FactoryUnit.OnFailedToBuild (line 205) then tries to access self.UnitBeingBuilt:SetCollisionShape('None') on a nil value

This is a real and critical nil access bug. The review comment's concern is valid and the suggested fix is appropriate.

Nil access to self.UnitBeingBuilt is possible when OnFailedToBuild is called via restricted unit build failure.

When a restricted unit build attempt is made, Unit.OnStartBuild() calls OnFailedToBuild() before self.UnitBeingBuilt is ever assigned in StructureUnit.OnStartBuild() (line 598). The code at line 205 attempts to call SetCollisionShape('None') on an uninitialized value, causing a crash.

Add the nil check as suggested:

    OnFailedToBuild = function(self)
        -- Instantly clear the build area so the next build can start, since unit `Destroy` doesn't do so.
+       local unitBeingBuilt = self.UnitBeingBuilt
+       if unitBeingBuilt and not IsDestroyed(unitBeingBuilt) then
+           unitBeingBuilt:SetCollisionShape('None')
+       end
-       self.UnitBeingBuilt:SetCollisionShape('None')
        StructureUnitOnFailedToBuild(self)
        self.FactoryBuildFailed = true
        self:StopBuildFx()
        ChangeState(self, self.IdleState)
    end,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
-- Instantly clear the build area so the next build can start, since unit `Destroy` doesn't do so.
self.UnitBeingBuilt:SetCollisionShape('None')
OnFailedToBuild = function(self)
-- Instantly clear the build area so the next build can start, since unit `Destroy` doesn't do so.
local unitBeingBuilt = self.UnitBeingBuilt
if unitBeingBuilt and not IsDestroyed(unitBeingBuilt) then
unitBeingBuilt:SetCollisionShape('None')
end
StructureUnitOnFailedToBuild(self)
self.FactoryBuildFailed = true
self:StopBuildFx()
ChangeState(self, self.IdleState)
end,
🤖 Prompt for AI Agents
In lua/sim/units/FactoryUnit.lua around lines 204-205,
FactoryUnit.OnFailedToBuild may call
self.UnitBeingBuilt:SetCollisionShape('None') when self.UnitBeingBuilt is nil
(build failed before assignment); add a nil-check before accessing
UnitBeingBuilt and only call SetCollisionShape('None') if self.UnitBeingBuilt is
non-nil (e.g., if self.UnitBeingBuilt then
self.UnitBeingBuilt:SetCollisionShape('None') end), ensuring no other logic
assumes UnitBeingBuilt exists when it may not.

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.

2 participants