Skip to content

reimplement: AICState methods that are 100% - #261

Merged
TheRedDaemon merged 10 commits into
mainfrom
reimpl/AICState-100
Oct 18, 2026
Merged

TheRedDaemon merged 10 commits into
mainfrom
reimpl/AICState-100

Conversation

@gynt

@gynt gynt commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

No description provided.

@gynt
gynt changed the base branch from main to reimpl/AICState-leftover October 14, 2026 07:49
@gynt
gynt changed the base branch from reimpl/AICState-leftover to main October 14, 2026 07:50
@gynt
gynt force-pushed the reimpl/AICState-100 branch from 959b9a9 to a8f7dae Compare October 14, 2026 08:15
Comment thread src/OpenSHC/AI/AICState/aiCommandTribe13DefensiveWave.cpp
Comment thread src/OpenSHC/AI/AICState/aiFarmChoice.cpp
Comment thread src/OpenSHC/AI/AICState/aiRequiresExtraOxtethers.cpp Outdated
Comment thread src/OpenSHC/AI/AICState/assignRequiredIdleEngineersToNewTribe.cpp Outdated
Comment thread src/OpenSHC/AI/AICState/countSiegeEngineUnits.cpp
Comment thread src/OpenSHC/AI/AICState/destroyBuildingIfNoWorker.cpp Outdated

@TheRedDaemon TheRedDaemon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

First batch. Will continue review later.

Comment thread src/OpenSHC/AI/AICState/aiUpdateTaxesAndRations.cpp
Comment thread src/OpenSHC/AI/AICState/aiUpdateTaxesAndRations.cpp
Comment thread src/OpenSHC/AI/AICState/aiRequiresExtraOxtethers.cpp Outdated
Comment thread src/OpenSHC/AI/AICState/assignRequiredIdleEngineersToNewTribe.cpp Outdated
Comment thread src/OpenSHC/AI/AICState/checksAndGenerateAITribesForPlayerIfNotExisting.cpp Outdated
Comment thread src/OpenSHC/AI/AICState/playAnger1BikFromPlayer.cpp Outdated
Comment thread src/OpenSHC/AI/AICState/playAnger2BikFromPlayerToPlayer.cpp Outdated
Comment thread src/OpenSHC/AI/AICState/playFriendlyVictoryBikFromPlayer.cpp Outdated
Comment thread src/OpenSHC/AI/AICState/playGoodsNotSentBikFromPlayer.cpp Outdated
Comment thread src/OpenSHC/AI/AICState/playGoodsSentBikFromPlayerToPlayer.cpp Outdated

@TheRedDaemon TheRedDaemon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Went through all.

Comment thread src/OpenSHC/AI/AICState/playTauntToAttackedPlayerBikFromPlayer.cpp
Comment thread src/OpenSHC/AI/AICState/playThanksBikFromPlayerToPlayer.cpp Outdated
Comment thread src/OpenSHC/AI/AICState/sendUnitsToAttackBreachedCastle.cpp Outdated
Comment thread src/OpenSHC/AI/AICState/shareGoldAmongTeamMembers.cpp Outdated
Comment thread src/OpenSHC/AI/AICState/shareGoldAmongTeamMembers.cpp Outdated
Comment thread src/OpenSHC/AI/AICState/someChatMessageSelection.cpp Outdated
Comment thread src/OpenSHC/AI/AICState/wipeAICMemory.cpp Outdated
Comment thread status/addresses-SHC-3BB0A8C1.txt
@gynt
gynt requested a review from TheRedDaemon October 18, 2026 14:48
Comment thread src/OpenSHC/AI/AICState/aiUpdateTaxesAndRations.cpp
Comment thread src/OpenSHC/AI/AICState/aiUpdateTaxesAndRations.cpp
Comment thread src/OpenSHC/AI/AICState/getUnitTypeForUnitBehaviourType.cpp
Comment thread src/OpenSHC/AI/AICState/getUnitTypeIndexForUnitID.cpp
Comment thread src/OpenSHC/AI/AICState/removeOrganismsAndSetMoveDestinationPairs.cpp Outdated
gynt and others added 10 commits October 18, 2026 23:22
…ents

Two generated-header changes requested in the PR review, kept separate from
the .cpp work as AGENTS.md asks:

- AICState::sendUnitsToAttackBreachedCastle returns BOOLEnum instead of
  undefined4; the body returns TRUE/FALSE accordingly.
- The 0x00EB0B18 global is named TIME_LastThanksBikPlaybackTime instead of
  DWORD_00eb0b18, which is what playThanksBikFromPlayerToPlayer stores into.

quick_diff holds at its baseline for both functions (1.0000 and 0.9762).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Include hygiene (the reviewer's "wrong import", checked across all 137 files):
every AICState .cpp now opens with the relative "../AICState.func.hpp" instead
of the absolute "OpenSHC/AI/AICState.hpp", matching the files that already did.
63 files changed; the func header is a superset, so no other include moved.

Magic values and style:
- assignRequiredIdleEngineersToNewTribe: drop the stale (int) cast on
  maxUnitCount (already int in the header), and test `dying` for truthiness.
- checksAndGenerateAITribesForPlayerIfNotExisting: `if (checkOnly)` instead of
  `if (checkOnly != FALSE)`.
- playGoodsSentBikFromPlayerToPlayer: `if (!getAliveLordForPlayer(...))`.
- getUnitTypeIndexForUnitID: the Caliph check now reads
  `aiType - 1 != AITA_CALIPH`, and the loop bound 0x14 is written as 20.
- wipeAICMemory: sizeof(this->aics[i]) instead of the literal 676.
- shareGoldAmongTeamMembers: drop the obsolete FIXME about the int return
  (the header already declares void), and use s__d__s___005a7674 from
  string-literals.hpp for the "%d %s - " format.

Measured, not assumed:
- aiRequiresExtraOxtethers: the (short) on quarryStockpileID is load-bearing
  (removing it drops quick_diff 1.0000 -> 0.9901), so it stays, with a comment.
- someChatMessageSelection: rolling the unrolled player scan into a plain 1..8
  loop drops quick_diff 0.9198 -> 0.6745, so the unroll is in the original
  source. Kept, with the measurement recorded in the comment.
- computeNervousness: isNotNervousByEnemyTroopValue is an int field storing
  0/1, which per AGENTS.md is an int and not a bool, so it is left as is.

status/addresses: reverted the 56 changed lines that belong to functions
outside this PR; the remaining 121 all map to a `// FUNCTION:` address in
src/OpenSHC/AI/AICState.

No regressions: quick_diff is unchanged from baseline on every touched
function.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ested loop

The 4 hand-written copies of the candidate test are replaced by an inner
`for (offset = 0; offset < 4; offset++)` inside the existing step-by-4 outer
loop. The generated instruction stream is identical to the unrolled source --
132 instructions, same opcodes and operands, differing only in MSVC's internal
label numbering -- so quick_diff stays at 0.9198 (118 vs the original's 119).

This corrects the reasoning in the previous commit. The compiler does unroll
here, contrary to the "MSVC1400 at /O2 never unrolls a loop" note in AGENTS.md:
writing all 8 iterations out by hand compiles to byte-identical code to a plain
1..8 loop (136 instructions, 0.6745 either way). What the original's shape
actually pins down is the unroll *factor* of 4, not the absence of a loop in
its source, so the outer loop must step by 4 while the inner 4 can be left to
the compiler.

Also measured and rejected: a file-local inlined helper called 4 times in the
outer loop (identical code, but emits a dead out-of-line copy of the helper),
the same helper called 8 times (0.4000), unroll by 2 (0.7339), and a
half/offset index split (0.8220).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ction

Tried `for (int i = 1; i < 9; i++)` and three other spellings of the same loop
(reusing the function's declared `i`, a fresh name declared in the `for`, and
the pre-declared `playerID`). All four produce byte-identical assembly at
0.6745 and 154 instructions, against 132 for the nested form and 119 in the
original, so the loop variable's name, declaration site and reuse are all
irrelevant here.

The reason is visible in the diff: fully unrolling 8 iterations with a known
trip count lets MSVC constant-fold every index, so each body gets its own
absolute addresses and an immediate player number. The original instead keeps
the index in a register and computes the stored value with `lea`, which means
its source had an induction variable that survived the unroll. Stepping the
outer loop by 4 is what reproduces that.

Comment updated with the mechanism; no code change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Verified against the original binary with Ghidra rather than inferred. The
field at PlayerData + 0x3820 has 7 writes and 5 reads in the whole image, and
every write stores 0 or 1:

  004d3802  computeNervousness                xor eax, eax   -> 0
  004d383f  computeNervousness                ebx            -> 1
  004ad1c5  ProcessAllyAcceptsRequest         imm            -> 0
  004ad295  ProcessAllyDeniesRequest2         imm            -> 0
  004ad315  ProcessAllyRequestAttackDefense   eax after xor  -> 0
  004ad38e  ProcessAllyRequestAttackDefense   imm            -> 1
  004ad433  ProcessAllyRequestAttackDefense   imm            -> 0

All five reads are == 0 / != 0 truthiness tests, so the field is boolean.
BOOLEnum is already the convention for such fields in PlayerData.hpp.

This also corrects the note in the earlier commit, which argued the field had
to stay an int because the original stores it with `mov dword ptr [..], 0/1`.
That AGENTS.md rule distinguishes int from C++ `bool` (a one-byte store);
BOOLEnum is a typedef of BOOL, hence int, so the dword store is exactly what
a BOOLEnum field looks like and was never evidence against it.

Being the same underlying type, the change is purely semantic: quick_diff
stays at 1.0000 for computeNervousness, determineAIPlayerHelp and
determineAIPlayerAttackRequestResponse, the field's only users.

The two readers also lose their `!= 0` per the review's preferred if() style.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
165 locals across the AICState sources gain `const` after the type, per the
project's preferred `int const stackvariable` spelling. Applied only where the
local is initialised once at its declaration and never afterwards assigned,
incremented, decremented or address-taken; arrays, multi-declarators and
uninitialised declarations are left alone. Wrong calls would not compile, and
all 137 files were compiled.

This also restores the `const` on getAIBuildInterval's `aiType`, which was not
dropped deliberately: it was lost in a8f7dae, where the AICState sources were
re-added from the field-refactor script's output. It was the only one of the
four files predating that commit to lose it.

const is NOT codegen-neutral here, so it is applied by measurement rather than
by rule. Seven locals of the form `int tribeIndex = <literal>` had to keep
their non-const spelling: with const, MSVC1400 folds the literal into the
array index at its use site and drops 4 instructions that the original still
has, costing 0.18 to 0.50 of match in aiCommandTribe11StandGroundToGate,
aiCommandTribe13DefensiveWave, sendAITribe_0xf_toAttackPatrol, sendTribeToAttack,
setTribe0x11ToAggressiveAndAttackIfConditionMet, setTribe0xCtoAggressiveAndAttack
and setTribe0xbToAggressiveAndAttack. const on a local initialised from a memory
read or a call is neutral, which is every other case here.

Verified by a full before/after quick_diff sweep of all 137 functions: every
one is identical to its baseline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Restore status/addresses-SHC-3BB0A8C1.txt from origin/main and re-apply
only the entries for addresses carrying a FUNCTION marker in the .cpp
files added or changed on this branch (120 entries). Drops the unrelated
0x004CC000 percentage change for buyGoods.cpp, which this branch does not
touch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gynt
gynt force-pushed the reimpl/AICState-100 branch from 08895be to 21ede33 Compare October 18, 2026 21:23
@TheRedDaemon
TheRedDaemon merged commit 4d58b43 into main Oct 18, 2026
1 check passed
@TheRedDaemon
TheRedDaemon deleted the reimpl/AICState-100 branch October 18, 2026 21:57
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