Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
21 changes: 21 additions & 0 deletions Info.txt
Original file line number Diff line number Diff line change
Expand Up @@ -265,5 +265,26 @@ const int _Moho_SSTICommandIssueData_Destructor = 0x0057ABB0;
// MSVCR80.dll
#define _memmove_s = 0x00A824E7;

// Navigator / Build Range
005F7440 CUnitMobileBuildTask::TaskTick(this)
005AE2D0 CAiPathNavigator::UpdateCurrentPosition(this, pos)
006190A0 NewMoveTask(SNavGoal* edi, CCommandTask* esi, SOCellPos, CUnitCommand*, int)
005A2CB0 SNavGoal::SNavGoal(this, SOCellPos)
005A3ED0 CAiNavigatorLand::SetGoal(this, SNavGoal*)
0062B780 Unit::PrepareMove(Unit*, Vector3f*, Rect2f*, int, char)

// Unit+0x54C = IAiNavigator* (NOT 0x4B4)
// CAiNavigatorLand+0x68 = CAiPathNavigator*
// CAiPathNavigator offsets:
// +0x0C mNext (0=arrived, 5/6=moving)
// +0x24 mCurrentPos (HPathCell: x=int16 low, z=int16 high)
// +0x28 mTargetPos
// +0x30 mGoal.mPos1.x0 (set by SetGoal)
// +0x34 mGoal.mPos1.z0
// +0x40 mGoal.mPos2.x0 (unused by engine, available for tagging)
// +0x44 mGoal.mPos2.z0
// +0x48 mGoal.mPos2.x1
// +0x64 v26 counter

// New unit categories.
const char* sCQUEMOV = "CQUEMOV";
12 changes: 12 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,18 @@ These don't matter except for other assembly patches

## Gameplay

- Engineers start building at max build range instead of walking to the build center

Engineers now begin construction as soon as they enter MaxBuildDistance of the
build site. Uses a circular arrival check in the navigator (dx²+dz² ≤ range²)
that works uniformly in all directions. Scales with unit type (ACU, T1-T3 engineers).

- hooks/HEngineerBuildRange.cpp
- hooks/HNavigatorCircularArrival.cpp
- section/EngineerBuildRange.cpp
- section/NavigatorCircularArrival.cpp
- include/MovementConfig.h

- Change tick intel update interval from every 30 ticks to every 1 tick

- hooks/IntelUpdate.cpp
Expand Down
1,950 changes: 1,950 additions & 0 deletions debug.log

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions hooks/HEngineerBuildRange.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#include "../define.h"
asm(
".section h0; .set h0,0x5F7754;"
"jmp "QU(EngineerBuildRangeHook)";"
".byte 0x90;"
);
6 changes: 6 additions & 0 deletions hooks/HNavigatorCircularArrival.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#include "../define.h"
asm(
".section h0; .set h0,0x5AE485;"
"jmp "QU(CircularArrivalCheck)";"
".byte 0x90;"
);
37 changes: 37 additions & 0 deletions include/MovementConfig.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#pragma once

// ==========================================================================
// MovementConfig.h — Engine offsets and shared helpers
// ==========================================================================

// Engine offsets (IDA-verified)
#define OFF_STEERING_UNIT 0x1C
#define OFF_UNIT_ID 0x70
#define OFF_UNIT_BLUEPRINT 0x74
#define OFF_UNIT_SIM 0x150
#define OFF_UNIT_POS 0x160
#define OFF_UNIT_MOTION 0x4B0
#define OFF_UNIT_NAVIGATOR 0x54C // verified from NewMoveTask disasm (NOT 0x4B4)
#define OFF_SIM_CURTICK 0x900

// Blueprint economy
#define OFF_BP_MAXBUILDDIST 0x564 // float MaxBuildDistance

// CAiNavigatorLand (IDA-verified from SetGoal disasm)
#define OFF_NAVLAND_PATHNAV 0x68 // CAiPathNavigator*

// CAiPathNavigator (IDA-verified from UpdateCurrentPosition disasm)
#define OFF_PATHNAV_NEXT 0x0C // mNext (0 = arrived)
#define OFF_PATHNAV_CURPOS 0x24 // mCurrentPos (HPathCell: x=int16 low, z=int16 high)
#define OFF_PATHNAV_TARGETPOS 0x28 // mTargetPos
#define OFF_PATHNAV_GOAL_X 0x30 // mGoal.mPos1.x0 (set by SetGoal)
#define OFF_PATHNAV_GOAL_Z 0x34 // mGoal.mPos1.z0
#define OFF_PATHNAV_TAG_X 0x40 // mGoal.mPos2.x0 — build center X (our tag)
#define OFF_PATHNAV_TAG_Z 0x44 // mGoal.mPos2.z0 — build center Z (our tag)
#define OFF_PATHNAV_TAG_RANGE 0x48 // mGoal.mPos2.x1 — rangeCells (0=inactive)
#define OFF_PATHNAV_V26 0x64 // v26 counter (set to 0 on arrival)

// Shared pointer validation
static inline bool IsValidPtr(uint32_t ptr) {
return (ptr >= 0x00400000 && ptr < 0x3F000000 && (ptr & 3) == 0);
}
16 changes: 16 additions & 0 deletions run_test.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
@echo off
set SRC=%~dp0ForgedAlliance_exxt.exe
set DST=C:\ProgramData\FAForever\bin\ForgedAlliance_exxt.exe
set INIT=init_faf.lua
set LOG=%~dp0debug.log

Comment on lines +1 to +6
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Mar 31, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Convert to Windows line endings (CRLF) and quote SET values.

The batch file uses Unix line endings (LF), which can cause parsing failures and script malfunction in Windows cmd.exe due to label handling bugs near 512-byte boundaries. Additionally, SET values should be quoted for safety with paths containing spaces. The INIT variable on line 4 is also unused.

Proposed fix
-set SRC=%~dp0ForgedAlliance_exxt.exe
-set DST=C:\ProgramData\FAForever\bin\ForgedAlliance_exxt.exe
-set INIT=init_faf.lua
-set LOG=%~dp0debug.log
+set "SRC=%~dp0ForgedAlliance_exxt.exe"
+set "DST=C:\ProgramData\FAForever\bin\ForgedAlliance_exxt.exe"
+set "LOG=%~dp0debug.log"

Also ensure the file is saved with CRLF line endings (e.g., via unix2dos run_test.bat or editor settings).

📝 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
@echo off
set SRC=%~dp0ForgedAlliance_exxt.exe
set DST=C:\ProgramData\FAForever\bin\ForgedAlliance_exxt.exe
set INIT=init_faf.lua
set LOG=%~dp0debug.log
`@echo` off
set "SRC=%~dp0ForgedAlliance_exxt.exe"
set "DST=C:\ProgramData\FAForever\bin\ForgedAlliance_exxt.exe"
set "LOG=%~dp0debug.log"
🧰 Tools
🪛 Blinter (1.0.112)

[error] 1-1: Unix line endings detected. Explanation: Batch file uses Unix line endings (LF-only) which can cause GOTO/CALL label parsing failures and script malfunction due to Windows batch parser 512-byte boundary bugs. Recommendation: Convert file to Windows line endings (CRLF). Use tools like dos2unix, notepad++, or configure git with 'git config core.autocrlf true'. Context: File uses Unix line endings (LF-only) - 16 LF sequences found

(E018)


[error] 2-2: Unsafe SET command usage. Explanation: SET commands without proper validation or quoting can cause security issues. Recommendation: Always quote SET values and validate input: SET "var=safe value". Context: SET command value should be quoted for safety

(SEC002)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@run_test.bat` around lines 1 - 6, The batch file uses Unix LF endings and
unquoted SET values; convert the file to Windows CRLF line endings and update
the SET lines (SRC, DST, LOG) to quote their values (e.g., SET SRC="...") so
paths with spaces are handled correctly; remove the unused INIT variable (or
quote and use it if intended) to avoid dead code and save the file with CRLF
(e.g., via editor settings or unix2dos) to prevent cmd.exe parsing issues.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

echo Copying patched exe to FAForever bin...
copy /Y "%SRC%" "%DST%"

echo Starting from FAForever bin...
echo Log: %LOG%

cd /d C:\ProgramData\FAForever\bin
"%DST%" /log "%LOG%" /nomovie
echo Exit code: %ERRORLEVEL%
pause
121 changes: 121 additions & 0 deletions section/EngineerBuildRange.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// ==========================================================================
// EngineerBuildRange.cpp — Tag navigator for circular build range arrival
//
// Hook 1 of 2: At 0x5F7754 (inside the move-issuing if-block in TaskTick),
// we replace the push 0;push 0;push 0 + NewMoveTask call sequence.
// We execute NewMoveTask ourselves, then tag pathNavigator.mGoal.mPos2
// with the build center + MaxBuildDistance.
//
// Must tag AFTER NewMoveTask because SetGoal zeros mPos2 for 1x1 goals.
//
// IMPORTANT: 0x5F776B (++mTaskState) is a SHARED instruction reached by
// both the move and no-move paths. We must NOT overwrite it.
// Our hook at 0x5F7754 (6 bytes) is safely inside the if-block.
// ==========================================================================

#include "moho.h"
#include "global.h"
#include "MovementConfig.h"

// All navigator offsets are in MovementConfig.h

extern "C" void __cdecl TagNavigatorForBuild(uint8_t* task)
{
uint32_t unitAddr = *(uint32_t*)(task + 0x1C);
if (!IsValidPtr(unitAddr)) return;

uint32_t bpAddr = *(uint32_t*)(unitAddr + OFF_UNIT_BLUEPRINT);
if (!IsValidPtr(bpAddr)) return;
float maxBD = *(float*)(bpAddr + OFF_BP_MAXBUILDDIST);
if (maxBD <= 2.0f) return;

uint32_t navLand = *(uint32_t*)(unitAddr + OFF_UNIT_NAVIGATOR);
if (!IsValidPtr(navLand)) return;
uint32_t pathNav = *(uint32_t*)(navLand + OFF_NAVLAND_PATHNAV);
if (!IsValidPtr(pathNav)) return;

// ── Tuning ───────────────────────────────────────────────────
// rangeCells: circular arrival radius in cells (1 cell = 1 world unit)
// higher = engineer stops further from center (more time saved)
// too high = State 1 check fails on diagonals (build cancelled!)
// 70% of maxBD: scales with unit type, safe for diagonals
// ACU (maxBD=10): rangeCells=7, diagonal=9.9 ✓
// T1 eng (maxBD=5): rangeCells=3, diagonal=4.2 ✓
int rangeCells = (int)(maxBD * 0.9f); //"Martin" 0.9 works fine at max range
// ────────────────────────────────────────────────────────────
if (rangeCells < 2) return;

// Distance check using CELL COORDINATES from the pathNavigator.
// These are always current (SetGoal just ran), unlike world coords
// which can be stale for shift-click build queues.
int goalX = *(int*)(pathNav + OFF_PATHNAV_GOAL_X);
int goalZ = *(int*)(pathNav + OFF_PATHNAV_GOAL_Z);
uint32_t curPos = *(uint32_t*)(pathNav + OFF_PATHNAV_CURPOS);
int curX = (int)(short)(curPos & 0xFFFF);
int curZ = (int)(short)(curPos >> 16);
int cdx = goalX - curX;
int cdz = goalZ - curZ;
int cellDistSq = cdx * cdx + cdz * cdz;

// Only tag if the move is long (unit needs to walk far).
// Short moves (on-site clearing, build queues) use default engine behavior.
// Unit must be at least rangeCells+3 cells away from goal.
int tagThreshold = rangeCells + 3; // = 10 for rangeCells=7
int tagThresholdSq = tagThreshold * tagThreshold; // = 100
if (cellDistSq <= tagThresholdSq) return;

// Tag pathNavigator with build center (cell coords) + range.
// Must use cell coords because Hook 2 compares with mCurrentPos (also in cells).
*(int*)(pathNav + OFF_PATHNAV_TAG_X) = goalX;
*(int*)(pathNav + OFF_PATHNAV_TAG_Z) = goalZ;
*(int*)(pathNav + OFF_PATHNAV_TAG_RANGE) = rangeCells;
}

// ==========================================================================
// ASM wrapper — top-level asm, no function prologue.
//
// Hook at 0x5F7754 (the 3x push 0 before NewMoveTask, 6 bytes):
// Safely inside the if-block (only reached when move is needed).
// We reproduce the entire NewMoveTask call sequence, then tag navigator.
//
// Original code being replaced/reproduced:
// 0x5F7754: push 0; push 0; push 0 (6 bytes, OVERWRITTEN)
// 0x5F775A: lea edi, [esp+0x10C] (reproduced in hook)
// 0x5F7761: mov esi, ebp (reproduced in hook)
// 0x5F7763: call NewMoveTask (reproduced in hook)
// 0x5F7768: add esp, 0xC (reproduced in hook)
// → then we tag navigator
// → then JMP to 0x5F776B (shared ++mTaskState)
// ==========================================================================

asm(
".global _EngineerBuildRangeHook\n"
"_EngineerBuildRangeHook:\n"

// Reproduce: push 0; push 0; push 0 (args for NewMoveTask)
"push 0\n"
"push 0\n"
"push 0\n"

// Reproduce: lea edi, [esp+0x10C] (navGoal on stack, adjusted for 3 pushes)
"lea edi, [esp+0x10C]\n"

// Reproduce: mov esi, ebp (task pointer)
"mov esi, ebp\n"

// Reproduce: call NewMoveTask
"call 0x6190A0\n"

// Reproduce: add esp, 0xC (cleanup 3 push args)
"add esp, 0x0C\n"

// Now tag the navigator (NewMoveTask + SetGoal already ran)
"pushad\n"
"push ebp\n"
"call _TagNavigatorForBuild\n"
"add esp, 4\n"
"popad\n"

// JMP to shared ++mTaskState (NOT overwritten, safely reached)
"jmp 0x5F776B\n"
);
80 changes: 80 additions & 0 deletions section/NavigatorCircularArrival.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// ==========================================================================
// NavigatorCircularArrival.cpp — Circular build range arrival check
//
// Hook 2 of 2: In CAiPathNavigator::UpdateCurrentPosition (0x5AE2D0),
// before the normal cell-equality arrival check, test if the unit is
// within the circular build range (tagged in mGoal.mPos2 by Hook 1).
// If within range, signal "arrived" — the unit stops and building starts.
// ==========================================================================

#include "moho.h"
#include "global.h"
#include "MovementConfig.h"

// Called from the asm hook to do the circular distance check
extern "C" int __cdecl CheckCircularArrival(uint8_t* pathNav)
{
int rangeCells = *(int*)(pathNav + OFF_PATHNAV_TAG_RANGE);
if (rangeCells <= 0) return 0; // not a build move

int buildX = *(int*)(pathNav + OFF_PATHNAV_TAG_X);
int buildZ = *(int*)(pathNav + OFF_PATHNAV_TAG_Z);

// Validate tag matches the active goal. If the goal changed (e.g. new order
// interrupted the build move without SetGoal zeroing mPos2), clear stale tag.
int goalX = *(int*)(pathNav + OFF_PATHNAV_GOAL_X);
int goalZ = *(int*)(pathNav + OFF_PATHNAV_GOAL_Z);
if (buildX != goalX || buildZ != goalZ) {
*(int*)(pathNav + OFF_PATHNAV_TAG_RANGE) = 0;
return 0;
}
Comment on lines +23 to +30
Copy link
Copy Markdown

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
set -euo pipefail

file="$(fd -a 'EngineerBuildRange.cpp$' | head -n1)"
if [ -z "$file" ]; then
  echo "EngineerBuildRange.cpp not found" >&2
  exit 1
fi

echo "== TagNavigatorForBuild definition =="
rg -nP -C60 '\bTagNavigatorForBuild\s*\(' "$file"

echo
echo "== OFF_PATHNAV_TAG_RANGE writes and nearby returns =="
rg -n -C3 'OFF_PATHNAV_TAG_RANGE|return\s*;' "$file"

Repository: FAForever/FA-Binary-Patches

Length of output: 108


🏁 Script executed:

# First, check if section/ directory exists and list its contents
echo "=== Files in section/ directory ==="
fd -a . section/ --max-depth 1 2>/dev/null | head -20 || echo "section/ not found or empty"

echo
echo "=== Search for any files matching '*EngineerBuild*' ==="
fd -a 'EngineerBuild' . 2>/dev/null || echo "No matches"

echo
echo "=== Search for NavigatorCircularArrival.cpp ==="
fd -a 'NavigatorCircularArrival' . 2>/dev/null || echo "No matches"

echo
echo "=== Repository root structure ==="
ls -la . | head -20

Repository: FAForever/FA-Binary-Patches

Length of output: 2307


🏁 Script executed:

# Search for TagNavigatorForBuild in EngineerBuildRange.cpp
echo "=== Searching for TagNavigatorForBuild ==="
rg 'TagNavigatorForBuild' section/EngineerBuildRange.cpp -C 60

echo
echo "=== File size check ==="
wc -l section/EngineerBuildRange.cpp

Repository: FAForever/FA-Binary-Patches

Length of output: 5147


Clear OFF_PATHNAV_TAG_RANGE on every non-tagging return in TagNavigatorForBuild().

The function has 7 early returns without clearing the tag field. Most critically, line 81 returns early for short moves without clearing OFF_PATHNAV_TAG_RANGE. Combined with the goal-coordinate check in NavigatorCircularArrival.cpp (which only clears on goal change), this allows a short move to the same goal to inherit a stale tag from a previous longer move. Every early return must clear the tag before returning, or use an unconditional clear at the start before any validation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@section/NavigatorCircularArrival.cpp` around lines 23 - 30,
TagNavigatorForBuild can return early from multiple paths without clearing the
shared tag field, which lets stale tags persist (e.g., short moves that return
without clearing OFF_PATHNAV_TAG_RANGE); ensure the tag is cleared on every
non-tagging return by either writing *(int*)(pathNav + OFF_PATHNAV_TAG_RANGE) =
0 before each early return in TagNavigatorForBuild or perform a single
unconditional clear of OFF_PATHNAV_TAG_RANGE at the start of
TagNavigatorForBuild (before the validations that cause early returns);
reference TagNavigatorForBuild, pathNav, OFF_PATHNAV_TAG_RANGE and the goal
fields OFF_PATHNAV_GOAL_X / OFF_PATHNAV_GOAL_Z when applying the fix.


uint32_t curPos = *(uint32_t*)(pathNav + OFF_PATHNAV_CURPOS);
int curX = (int)(short)(curPos & 0xFFFF);
int curZ = (int)(short)(curPos >> 16);

int dx = curX - buildX;
int dz = curZ - buildZ;
int distSq = dx * dx + dz * dz;
int rangeSq = rangeCells * rangeCells;

if (distSq <= rangeSq) {
*(int*)(pathNav + OFF_PATHNAV_TAG_RANGE) = 0; // clear tag (one-shot)
return 1; // signal arrival
}
return 0;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

// Top-level asm hook at 0x5AE485
// EBX = CAiPathNavigator*
// Replaces 6 bytes: mov ecx,[ebx+1C]; mov eax,[ebx+24]

asm(
".global _CircularArrivalCheck\n"
"_CircularArrivalCheck:\n"

"pushad\n"
"push ebx\n"
"call _CheckCircularArrival\n"
"add esp, 4\n"
"test eax, eax\n"
"popad\n"
"jz .Lca_not_build\n"

// IN RANGE: signal arrival
"xor ecx, ecx\n"
"mov dword ptr [ebx+0x0C], ecx\n"
"mov eax, dword ptr [ebx+0x24]\n"
"mov dword ptr [ebx+0x28], eax\n"
"mov dword ptr [ebx+0x64], ecx\n"
"pop edi\n"
"pop esi\n"
"mov esp, ebp\n"
"pop ebp\n"
"ret 4\n"

".Lca_not_build:\n"
"mov ecx, dword ptr [ebx+0x1C]\n"
"mov eax, dword ptr [ebx+0x24]\n"
"jmp 0x5AE48B\n"
);