-
Notifications
You must be signed in to change notification settings - Fork 10
Engineers start building at max build range #148
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 5 commits
d069d11
ae284b8
98ac36a
c0a71a4
a2b4bf4
bfbeef4
7ec1c90
3761f10
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| 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;" | ||
| ); |
| 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;" | ||
| ); |
| 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); | ||
| } |
| 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 | ||
|
|
||
| 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 | ||
| 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" | ||
| ); |
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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 -20Repository: 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.cppRepository: FAForever/FA-Binary-Patches Length of output: 5147 Clear The function has 7 early returns without clearing the tag field. Most critically, line 81 returns early for short moves without clearing 🤖 Prompt for AI Agents |
||
|
|
||
| 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; | ||
|
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" | ||
| ); | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
INITvariable on line 4 is also unused.Proposed fix
Also ensure the file is saved with CRLF line endings (e.g., via
unix2dos run_test.bator editor settings).📝 Committable suggestion
🧰 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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.