Tutorial 1: Reimplementation¶
For this tutorial, I chose to reimplement the function: OpenSHC::AI::AICState::setFoodBuyPlan, which is a member function of class AICState. This function doesn’t contain any calls to other functions, and I understand the Ghidra decompilation for this function, which makes reimplementation simpler.
Step 1: the groundwork¶
Every file in OpenSHC has a deterministic location. The reimplementation for function OpenSHC::AI::AICState::setFoodBuyPlan should be placed in OpenSHC/AI/AICState/setFoodBuyPlan.cpp. Everything in OpenSHC is also namespaced.
Therefore, we create file: OpenSHC/AI/AICState/setFoodBuyPlan.cpp with the following contents
1#include "OpenSHC/AI/AICState.hpp"
2
3namespace OpenSHC {
4namespace AI {
5 void AICState::setFoodBuyPlan(int playerID) { };
6}
7}
Now, for reccmp to be able to associate this definition with the one in the original binary, we insert an annotation containing the address of this function in the original binary. You can find this address in OpenSHC/AI/AICState.func.hpp, which contains this:
1 MACRO_FUNCTION_RESOLVER(
2 void (AICState::*)(int), false, Address::SHC_3BB0A8C1_0x004CB060, &AICState::setFoodBuyPlan)
3 setFoodBuyPlan;
(If you use an IDE like VSCode it is easiest to just search for AICState::setFoodBuyPlan)
Let’s insert the annotation:
1#include "OpenSHC/AI/AICState.hpp"
2
3namespace OpenSHC {
4namespace AI {
5 // FUNCTION: STRONGHOLDCRUSADER 0x004CB060
6 void AICState::setFoodBuyPlan(int playerID) { };
7}
8}
Now, for the compiler to include this .cpp file in the compilation, we add it to cmake/openshc-sources.txt.local (use the .local file for local development):
cmake/openshc-sources.txt.local:
src/OpenSHC/AI/AICState/setFoodBuyPlan.cpp
Now we are ready to compile:
build.bat RelWithDebInfo OpenSHC.dll
Hopefully the compiler output ends with:
[ 88%] Building CXX object CMakeFiles/OpenSHC.dll.dir/src/OpenSHC/AI/AICState/setFoodBuyPlan.cpp.obj
setFoodBuyPlan.cpp
[100%] Linking CXX shared library DLL\OpenSHC.dll
[100%] Built target OpenSHC.dll
Build completed successfully for preset "RelWithDebInfo" target "OpenSHC.dll".
Step 2: getting an initial reimplementation¶
Given succesful compilation, we can now use reccmp to inspect any differences with the original. Note that we specify which function to give verbose output for (an assembly diff) by address:
reccmp/dll/run reccmp-reccmp --target STRONGHOLDCRUSADER --verbose 0x004CB060
Output:
1---
2+++
3@@ -,0 +0x1002e4d0,1 @@
4 : +ret 4 (setFoodBuyPlan.cpp:6)
5
6
7OpenSHC::AI::AICState::setFoodBuyPlan is only 0.00% similar to the original, diff above
Note that reccmp processes our function until it finds the last return statement (ret in assembly). Since our reimplementation begins with ret, we don’t get useful output. We need to find the assembly through other means.
I use Ghidra, which given the current state of the database gives:
1void __thiscall OpenSHC::AI::AICState::setFoodBuyPlan(AICState *this, int playerID)
2
3{
4 int iVar1;
5 int iVar2;
6
7 iVar1 = DAT_GameState.playerDataArray[playerID].aiType;
8 if (iVar1 != 0) {
9 iVar2 = (iVar1 + -1) * 0x2a4;
10 iVar1 = *(int *)((int)&DAT_AICState + iVar2 + 0x84);
11 if ((-1 < iVar1) && (DAT_GameState.playerDataArray[playerID].currentResources[0xd] < iVar1)) {
12 DAT_GameState.playerDataArray[playerID].resourcesToAcquireArray[0xd] =
13 *(int *)((int)&DAT_AICState + iVar2 + 0x98);
14 }
15 iVar1 = *(int *)((int)&DAT_AICState + iVar2 + 0x88);
16 if ((0 < iVar1) && (DAT_GameState.playerDataArray[playerID].currentResources[0xb] < iVar1)) {
17 DAT_GameState.playerDataArray[playerID].resourcesToAcquireArray[0xb] =
18 *(int *)((int)&DAT_AICState + iVar2 + 0x98);
19 }
20 iVar1 = *(int *)((int)&DAT_AICState + iVar2 + 0x8c);
21 if ((0 < iVar1) && (DAT_GameState.playerDataArray[playerID].currentResources[10] < iVar1)) {
22 DAT_GameState.playerDataArray[playerID].resourcesToAcquireArray[10] =
23 *(int *)((int)&DAT_AICState + iVar2 + 0x98);
24 }
25 iVar1 = *(int *)((int)&DAT_AICState + iVar2 + 0x90);
26 if ((0 < iVar1) && (DAT_GameState.playerDataArray[playerID].currentResources[9] < iVar1)) {
27 DAT_GameState.playerDataArray[playerID].resourcesToAcquireArray[9] =
28 *(int *)((int)&DAT_AICState + iVar2 + 0x98);
29 }
30 iVar1 = *(int *)((int)&DAT_AICState + iVar2 + 0x94);
31 if ((0 < iVar1) && (DAT_GameState.playerDataArray[playerID].currentResources[3] < iVar1)) {
32 DAT_GameState.playerDataArray[playerID].resourcesToAcquireArray[3] =
33 *(int *)((int)&DAT_AICState + iVar2 + 0x98);
34 }
35 }
36 return;
37}
We could try to compile this directly, which would be a good starting point. Because I will use AI later on, I want to give the AI as much context as I can. Therefore, let’s clean up the raw output from Ghidra by renaming the local variables.
Step 3: understanding and tidying the basic reimplementation¶
1void __thiscall OpenSHC::AI::AICState::setFoodBuyPlan(AICState *this, int playerID)
2
3{
4 int _offset;
5 int _aiType;
6 int _preferredStock;
7
8 _aiType = DAT_GameState.playerDataArray[playerID].aiType;
9 if (_aiType != 0) {
10 _offset = (_aiType + -1) * 0x2a4;
11 _preferredStock = *(int *)((int)&DAT_AICState + _offset + 0x84);
12 if ((-1 < _preferredStock) &&
13 (DAT_GameState.playerDataArray[playerID].currentResources[0xd] < _preferredStock)) {
14 DAT_GameState.playerDataArray[playerID].resourcesToAcquireArray[0xd] =
15 *(int *)((int)&DAT_AICState + _offset + 0x98);
16 }
17 _preferredStock = *(int *)((int)&DAT_AICState + _offset + 0x88);
18 if ((0 < _preferredStock) &&
19 (DAT_GameState.playerDataArray[playerID].currentResources[0xb] < _preferredStock)) {
20 DAT_GameState.playerDataArray[playerID].resourcesToAcquireArray[0xb] =
21 *(int *)((int)&DAT_AICState + _offset + 0x98);
22 }
23 _preferredStock = *(int *)((int)&DAT_AICState + _offset + 0x8c);
24 if ((0 < _preferredStock) &&
25 (DAT_GameState.playerDataArray[playerID].currentResources[10] < _preferredStock)) {
26 DAT_GameState.playerDataArray[playerID].resourcesToAcquireArray[10] =
27 *(int *)((int)&DAT_AICState + _offset + 0x98);
28 }
29 _preferredStock = *(int *)((int)&DAT_AICState + _offset + 0x90);
30 if ((0 < _preferredStock) &&
31 (DAT_GameState.playerDataArray[playerID].currentResources[9] < _preferredStock)) {
32 DAT_GameState.playerDataArray[playerID].resourcesToAcquireArray[9] =
33 *(int *)((int)&DAT_AICState + _offset + 0x98);
34 }
35 _preferredStock = *(int *)((int)&DAT_AICState + _offset + 0x94);
36 if ((0 < _preferredStock) &&
37 (DAT_GameState.playerDataArray[playerID].currentResources[3] < _preferredStock)) {
38 DAT_GameState.playerDataArray[playerID].resourcesToAcquireArray[3] =
39 *(int *)((int)&DAT_AICState + _offset + 0x98);
40 }
41 }
42 return;
43}
Line 10-15 look quite ugly, they use binary offsets instead of accessing fields. Probably the compiler optimized memory access. Luckily, Ghidra is of help here by giving hints about which memory is accessed, but let’s do some calculation the manual way for sake of explanation.
Finding fields¶
Assuming _aiType is 1, which is a valid ai type (the Rat), then _offset is 0. Then _preferredStock simplifies to &DAT_AICState + 0x84 which gives us the field DAT_AICState.minimumApples. This last step can be figured out like so:
Look for
DAT_AICStatein the project. Or visitsrc/OpenSHC/Globalsand findDAT_AICState.hpp:
1#pragma once
2
3#include "OpenSHC/AI/AICState.hpp"
4namespace OpenSHC {
5
6using OpenSHC::AI::AICState;
7
8MACRO_STRUCT_RESOLVER(AICState, false, Address::SHC_3BB0A8C1_0x023FC8E8) DAT_AICState;
9} // namespace OpenSHC
We see that DAT_AICState is of type AICState, which is defined in OpenSHC/AI/AICState.hpp as can we be seen from the #include statement (in this case this is actually the same class for which we are reimplementing a member function).
Looking at the struct part of the class declaration, we can see some helpful annotations to trace offset
+ 0x84.
1// SIZE: 0x00006D90
2class AICState {
3public:
4 AICSpecification DAT_AICArray[20]; // 0x00000000 length: 13520
5 undefined4 aiBorderTilesIndex; // 0x000034D0 length: 4
6 TileDistancePair aiBorderTiles[1000]; // 0x000034D4 length: 8000
7 byte unused01[512]; // 0x00005414 length: 512
8 short tribeIDArray[1000]; // 0x00005614 length: 2000
9 int tribeUIDArray[1000]; // 0x00005DE4 length: 4000
10 undefined4 DAT_SomeTime; // 0x00006D84 length: 4
11 byte unused02[8]; // 0x00006D88 length: 8
Note that + 0x84 is smaller than the first field’s offset which is DAT_AICArray[20] with offsets 0. We have to look into AICSpecification in which we find minimumApples at offset 0x84.
// SIZE: 0x000002A4
typedef struct AICSpecification {
dword flagType; // 0x00000000 length: 4
dword unknown001; // 0x00000004 length: 4
dword unknown002; // 0x00000008 length: 4
... // ommitted for tutorial brevity
dword minimumApples; // 0x00000084 length: 4
dword minimumCheese; // 0x00000088 length: 4
dword minimumBread; // 0x0000008C length: 4
dword minimumWheat; // 0x00000090 length: 4
dword minimumHop; // 0x00000094 length: 4
dword tradeAmountFood; // 0x00000098 length: 4
So the code in the function is accessing minimum apples.
Let’s rewrite our reimplementation (line 2 below, note
_offsetbecomes useless):
1_offset = (_aiType + -1) * 0x2a4;
2_preferredStock = *(int*)((int)&DAT_AICState.DAT_AICArray[_aiType - 1].minimumApples);
3if ((-1 < _preferredStock)
4 && (DAT_GameState.playerDataArray[playerID].currentResources[0xd] < _preferredStock)) {
5 DAT_GameState.playerDataArray[playerID].resourcesToAcquireArray[0xd]
6 = *(int*)((int)&DAT_AICState + _offset + 0x98);
7}
Substituting enum values¶
At line 4 we see the usage of 0xd, which we can guess is the resource type for apples. Let’s check in src/OpenSHC/Game/Resources/ResourceType.hpp to see if apples is associated with integer 0xd. Yes, see line 15:
1typedef enum ResourceType {
2
3 RT_LOGS = 1, // 0x00000001
4 RT_WOOD = 2, // 0x00000002
5 RT_HOPS = 3, // 0x00000003
6 RT_STONE = 4, // 0x00000004
7 RT_PARTIALSTONE = 5, // 0x00000005
8 RT_IRON = 6, // 0x00000006
9 RT_PITCH = 7, // 0x00000007
10 RT_PARTIALPITCH = 8, // 0x00000008
11 RT_WHEAT = 9, // 0x00000009
12 RT_BREAD = 10, // 0x0000000A
13 RT_CHEESE = 11, // 0x0000000B
14 RT_MEAT = 12, // 0x0000000C
15 RT_APPLE = 13, // 0x0000000D
Furthermore, we can trace offset +0x98 in line 6 to tradeAmountFood. Cleaning up our reimplementation (include ResourceType.hpp!):
1#include "OpenSHC/AI/AICState.hpp"
2#include "OpenSHC/Game/Resources/ResourceType.hpp"
3
4namespace OpenSHC {
5namespace AI {
6
7 using OpenSHC::Game::Resources::ResourceType;
8
9 // FUNCTION: STRONGHOLDCRUSADER 0x004CB060
10 void AICState::setFoodBuyPlan(int playerID)
11 {
12 int _offset;
13 int _aiType;
14 int _preferredStock;
15
16 _aiType = DAT_GameState.playerDataArray[playerID].aiType;
17 if (_aiType != 0) {
18 _offset = (_aiType + -1) * 0x2a4;
19 _preferredStock = *(int*)((int)&DAT_AICState.DAT_AICArray[_aiType - 1].minimumApples);
20 if ((-1 < _preferredStock)
21 && (DAT_GameState.playerDataArray[playerID].currentResources[RT_APPLE] < _preferredStock)) {
22 DAT_GameState.playerDataArray[playerID].resourcesToAcquireArray[RT_APPLE]
23 = *(int*)((int)&DAT_AICState.DAT_AICArray[_aiType - 1].tradeAmountFood);
24 }
Step 4: Correct references to global variables¶
Before we recompile, we need to change how globals are referred to. OpenSHC refers to the global memory in the exe, so we use pointer access for structs -> instead of .. Also, we need to explicitly mention we want the pointer with ::ptr:
1#include "OpenSHC/AI/AICState.hpp"
2#include "OpenSHC/Game/Resources/ResourceType.hpp"
3
4namespace OpenSHC {
5namespace AI {
6
7 using OpenSHC::Game::Resources::ResourceType;
8
9 // FUNCTION: STRONGHOLDCRUSADER 0x004CB060
10 void AICState::setFoodBuyPlan(int playerID)
11 {
12 int _offset;
13 int _aiType;
14 int _preferredStock;
15
16 _aiType = DAT_GameState::ptr->playerDataArray[playerID].aiType;
17 if (_aiType != 0) {
18 _offset = (_aiType + -1) * 0x2a4;
19 _preferredStock = DAT_AICState::ptr->DAT_AICArray[_aiType - 1].minimumApples;
20 if ((-1 < _preferredStock)
21 && (DAT_GameState::ptr->playerDataArray[playerID].currentResources[RT_APPLE] < _preferredStock)) {
22 DAT_GameState::ptr->playerDataArray[playerID].resourcesToAcquireArray[RT_APPLE]
23 = DAT_AICState::ptr->DAT_AICArray[_aiType - 1].tradeAmountFood;
24 }
Also note we use DAT_GameState, so make sure to include that too:
#include "OpenSHC/Globals/DAT_GameState.hpp"
Step 5: compiling the reimplementation and comparing it to the original¶
Patching everything up, we get something that looks like good code and that actually compiles with .\build.bat RelWithDebInfo OpenSHC.dll:
1#include "OpenSHC/AI/AICState.hpp"
2#include "OpenSHC/Game/Resources/ResourceType.hpp"
3#include "OpenSHC/Globals/DAT_AICState.hpp"
4#include "OpenSHC/Globals/DAT_GameState.hpp"
5
6namespace OpenSHC {
7namespace AI {
8
9 using OpenSHC::Game::Resources::ResourceType;
10
11 // FUNCTION: STRONGHOLDCRUSADER 0x004CB060
12 void AICState::setFoodBuyPlan(int playerID)
13 {
14 // int _offset;
15 int _aiType;
16 int _preferredStock;
17
18 _aiType = DAT_GameState::ptr->playerDataArray[playerID].aiType;
19 if (_aiType != 0) {
20 // _offset = (_aiType + -1) * 0x2a4;
21 _preferredStock = DAT_AICState::ptr->DAT_AICArray[_aiType - 1].minimumApples;
22 if ((-1 < _preferredStock)
23 && (DAT_GameState::ptr->playerDataArray[playerID].currentResources[ResourceType::RT_APPLE]
24 < _preferredStock)) {
25 DAT_GameState::ptr->playerDataArray[playerID].resourcesToAcquireArray[ResourceType::RT_APPLE]
26 = DAT_AICState::ptr->DAT_AICArray[_aiType - 1].tradeAmountFood;
27 }
28 _preferredStock = DAT_AICState::ptr->DAT_AICArray[_aiType - 1].minimumCheese;
29 if ((0 < _preferredStock)
30 && (DAT_GameState::ptr->playerDataArray[playerID].currentResources[ResourceType::RT_CHEESE]
31 < _preferredStock)) {
32 DAT_GameState::ptr->playerDataArray[playerID].resourcesToAcquireArray[ResourceType::RT_CHEESE]
33 = DAT_AICState::ptr->DAT_AICArray[_aiType - 1].tradeAmountFood;
34 }
35 _preferredStock = DAT_AICState::ptr->DAT_AICArray[_aiType - 1].minimumBread;
36 if ((0 < _preferredStock)
37 && (DAT_GameState::ptr->playerDataArray[playerID].currentResources[ResourceType::RT_BREAD]
38 < _preferredStock)) {
39 DAT_GameState::ptr->playerDataArray[playerID].resourcesToAcquireArray[ResourceType::RT_BREAD]
40 = DAT_AICState::ptr->DAT_AICArray[_aiType - 1].tradeAmountFood;
41 }
42 _preferredStock = DAT_AICState::ptr->DAT_AICArray[_aiType - 1].minimumWheat;
43 if ((0 < _preferredStock)
44 && (DAT_GameState::ptr->playerDataArray[playerID].currentResources[ResourceType::RT_WHEAT]
45 < _preferredStock)) {
46 DAT_GameState::ptr->playerDataArray[playerID].resourcesToAcquireArray[ResourceType::RT_WHEAT]
47 = DAT_AICState::ptr->DAT_AICArray[_aiType - 1].tradeAmountFood;
48 }
49 _preferredStock = DAT_AICState::ptr->DAT_AICArray[_aiType - 1].minimumHop;
50 if ((0 < _preferredStock)
51 && (DAT_GameState::ptr->playerDataArray[playerID].currentResources[ResourceType::RT_HOPS]
52 < _preferredStock)) {
53 DAT_GameState::ptr->playerDataArray[playerID].resourcesToAcquireArray[ResourceType::RT_HOPS]
54 = DAT_AICState::ptr->DAT_AICArray[_aiType - 1].tradeAmountFood;
55 }
56 }
57 return;
58 };
59}
60}
Here is our first reccmp:
reccmp/dll/run reccmp-reccmp --target STRONGHOLDCRUSADER --verbose 0x004CB060
Output:
1---
2+++
3@@ -0x4cb060,42 +0x1002e4d0,42 @@
40x4cb060 : -mov edx, dword ptr [esp + 4]
50x4cb064 : -imul edx, edx, 0x39f4
60x4cb06a : -mov eax, dword ptr [edx + 0x115e0f8]
7 : +mov ecx, dword ptr [esp + 4] (setFoodBuyPlan.cpp:18)
8 : +imul ecx, ecx, 0x39f4
9 : +mov eax, dword ptr [ecx + 0x115e0f8]
100x4cb070 : test eax, eax (setFoodBuyPlan.cpp:19)
110x4cb072 : -je 0xa1
120x4cb078 : -add eax, -1
13 : +je 0x9d
140x4cb07b : imul eax, eax, 0x2a4 (setFoodBuyPlan.cpp:21)
150x4cb081 : -add eax, ecx
160x4cb083 : -mov ecx, dword ptr [eax + 0x84]
170x4cb089 : -test ecx, ecx
180x4cb08b : -jl 0x14
190x4cb08d : -cmp dword ptr [edx + 0x115c2fc], ecx
20 : +mov edx, dword ptr [eax + 0x23fc6c8]
21 : +cmp edx, -1 (setFoodBuyPlan.cpp:24)
22 : +jle 0x14
23 : +cmp dword ptr [ecx + 0x115c2fc], edx
240x4cb093 : jge 0xc
250x4cb095 : -mov ecx, dword ptr [eax + 0x98]
260x4cb09b : -mov dword ptr [edx + 0x115e89c], ecx
270x4cb0a1 : -mov ecx, dword ptr [eax + 0x88]
280x4cb0a7 : -test ecx, ecx
29 : +mov edx, dword ptr [eax + 0x23fc6dc] (setFoodBuyPlan.cpp:26)
30 : +mov dword ptr [ecx + 0x115e89c], edx
31 : +mov edx, dword ptr [eax + 0x23fc6cc] (setFoodBuyPlan.cpp:28)
32 : +test edx, edx (setFoodBuyPlan.cpp:31)
330x4cb0a9 : jle 0x14
340x4cb0ab : -cmp dword ptr [edx + 0x115c2f4], ecx
35 : +cmp dword ptr [ecx + 0x115c2f4], edx
360x4cb0b1 : jge 0xc
370x4cb0b3 : -mov ecx, dword ptr [eax + 0x98]
380x4cb0b9 : -mov dword ptr [edx + 0x115e894], ecx
390x4cb0bf : -mov ecx, dword ptr [eax + 0x8c]
400x4cb0c5 : -test ecx, ecx
41 : +mov edx, dword ptr [eax + 0x23fc6dc] (setFoodBuyPlan.cpp:33)
42 : +mov dword ptr [ecx + 0x115e894], edx
43 : +mov edx, dword ptr [eax + 0x23fc6d0] (setFoodBuyPlan.cpp:35)
44 : +test edx, edx (setFoodBuyPlan.cpp:38)
450x4cb0c7 : jle 0x14
460x4cb0c9 : -cmp dword ptr [edx + 0x115c2f0], ecx
47 : +cmp dword ptr [ecx + 0x115c2f0], edx
480x4cb0cf : jge 0xc
490x4cb0d1 : -mov ecx, dword ptr [eax + 0x98]
500x4cb0d7 : -mov dword ptr [edx + 0x115e890], ecx
510x4cb0dd : -mov ecx, dword ptr [eax + 0x90]
520x4cb0e3 : -test ecx, ecx
53 : +mov edx, dword ptr [eax + 0x23fc6dc] (setFoodBuyPlan.cpp:40)
54 : +mov dword ptr [ecx + 0x115e890], edx
55 : +mov edx, dword ptr [eax + 0x23fc6d4] (setFoodBuyPlan.cpp:42)
56 : +test edx, edx (setFoodBuyPlan.cpp:45)
570x4cb0e5 : jle 0x14
580x4cb0e7 : -cmp dword ptr [edx + 0x115c2ec], ecx
59 : +cmp dword ptr [ecx + 0x115c2ec], edx
600x4cb0ed : jge 0xc
610x4cb0ef : -mov ecx, dword ptr [eax + 0x98]
620x4cb0f5 : -mov dword ptr [edx + 0x115e88c], ecx
630x4cb0fb : -mov ecx, dword ptr [eax + 0x94]
640x4cb101 : -test ecx, ecx
65 : +mov edx, dword ptr [eax + 0x23fc6dc] (setFoodBuyPlan.cpp:47)
66 : +mov dword ptr [ecx + 0x115e88c], edx
67 : +mov edx, dword ptr [eax + 0x23fc6d8] (setFoodBuyPlan.cpp:49)
68 : +test edx, edx (setFoodBuyPlan.cpp:52)
690x4cb103 : jle 0x14
700x4cb105 : -cmp dword ptr [edx + 0x115c2d4], ecx
71 : +cmp dword ptr [ecx + 0x115c2d4], edx
720x4cb10b : jge 0xc
730x4cb10d : -mov eax, dword ptr [eax + 0x98]
74 : +mov eax, dword ptr [eax + 0x23fc6dc] (setFoodBuyPlan.cpp:54)
75 : +mov dword ptr [ecx + 0x115e874], eax
76 : +ret 4 (setFoodBuyPlan.cpp:58)
77
78
79OpenSHC::AI::AICState::setFoodBuyPlan is only 26.19% similar to the original, diff above
Oof, only 26%! Now this is a great moment to start using AI to get better results.
Step 6: improving the reimplementation¶
Let’s try manual improvements first:
Line 13 suggests _aiType - 1 is cached in a register. Let’s do that and insert that line after the if (_aiType != 0) check.
13int _aiType0 = _aiType - 1;
Great, 27% and we fixed this line specifically.
Global access versus this pointer¶
Note however dat we used DAT_AICState even though we could have used this as we are implementing a member function for AICState class.
That change to our code (DAT_AICState::ptr-> to this->) actually made the match worse: 22%! but it is much more likely the developers used this from a programmer’s mindset point of view.
Checking for functional reimplementation versus exact reimplementation¶
Also note that the difference is in registry usage not really in terms of functionality, e.g. our code uses edx instead of eax and the other way around. Perhaps we can try reordering statements such that the right variable goes in the right register.
Step 7: Use AI to improve the quality¶
At this point, I used Claude with the MCP tools provided in the OpenSHC repo to iteratively improve (see this tutorial on how to set that up).
AI prompt:
Using the available MCP tools, improve function
OpenSHC::AI::AICState::setFoodBuyPlan
After two iterations, it found a 100% matching solution and it documented our source code by itself:
1#include "OpenSHC/AI/AICState.hpp"
2#include "OpenSHC/Game/Resources/ResourceType.hpp"
3#include "OpenSHC/Globals/DAT_AICState.hpp"
4#include "OpenSHC/Globals/DAT_GameState.hpp"
5
6namespace OpenSHC {
7namespace AI {
8
9 using OpenSHC::Game::Resources::ResourceType;
10
11 // FUNCTION: STRONGHOLDCRUSADER 0x004CB060
12 void AICState::setFoodBuyPlan(int playerID)
13 {
14 int aiType;
15 int aiConfigIndex;
16 int minimumStock;
17
18 aiType = DAT_GameState::ptr->playerDataArray[playerID].aiType;
19
20 // Early return if not an AI player (human player has aiType == 0)
21 if (aiType == 0) {
22 return;
23 }
24
25 // Get AI configuration index (convert 1-based to 0-based index)
26 aiConfigIndex = aiType - 1;
27
28 // Check each food type and queue purchase if current stock is below minimum
29
30 // Apples: special handling with >= 0 check (allows -1 to disable)
31 minimumStock = this->DAT_AICArray[aiConfigIndex].minimumApples;
32 if (minimumStock >= 0
33 && DAT_GameState::ptr->playerDataArray[playerID].currentResources[ResourceType::RT_APPLE] < minimumStock) {
34 DAT_GameState::ptr->playerDataArray[playerID].resourcesToAcquireArray[ResourceType::RT_APPLE]
35 = this->DAT_AICArray[aiConfigIndex].tradeAmountFood;
36 }
37
38 // Cheese: standard check with > 0
39 minimumStock = this->DAT_AICArray[aiConfigIndex].minimumCheese;
40 if (minimumStock > 0
41 && DAT_GameState::ptr->playerDataArray[playerID].currentResources[ResourceType::RT_CHEESE] < minimumStock) {
42 DAT_GameState::ptr->playerDataArray[playerID].resourcesToAcquireArray[ResourceType::RT_CHEESE]
43 = this->DAT_AICArray[aiConfigIndex].tradeAmountFood;
44 }
45
46 // Bread: standard check with > 0
47 minimumStock = this->DAT_AICArray[aiConfigIndex].minimumBread;
48 if (minimumStock > 0
49 && DAT_GameState::ptr->playerDataArray[playerID].currentResources[ResourceType::RT_BREAD] < minimumStock) {
50 DAT_GameState::ptr->playerDataArray[playerID].resourcesToAcquireArray[ResourceType::RT_BREAD]
51 = this->DAT_AICArray[aiConfigIndex].tradeAmountFood;
52 }
53
54 // Wheat: standard check with > 0
55 minimumStock = this->DAT_AICArray[aiConfigIndex].minimumWheat;
56 if (minimumStock > 0
57 && DAT_GameState::ptr->playerDataArray[playerID].currentResources[ResourceType::RT_WHEAT] < minimumStock) {
58 DAT_GameState::ptr->playerDataArray[playerID].resourcesToAcquireArray[ResourceType::RT_WHEAT]
59 = this->DAT_AICArray[aiConfigIndex].tradeAmountFood;
60 }
61
62 // Hops: standard check with > 0
63 minimumStock = this->DAT_AICArray[aiConfigIndex].minimumHop;
64 if (minimumStock > 0
65 && DAT_GameState::ptr->playerDataArray[playerID].currentResources[ResourceType::RT_HOPS] < minimumStock) {
66 DAT_GameState::ptr->playerDataArray[playerID].resourcesToAcquireArray[ResourceType::RT_HOPS]
67 = this->DAT_AICArray[aiConfigIndex].tradeAmountFood;
68 }
69 }
70}
71}
OpenSHC> reccmp/dll/run reccmp-reccmp --target STRONGHOLDCRUSADER --verbose 0x004CB060
...
0x4cb060: OpenSHC::AI::AICState::setFoodBuyPlan 100% match.
✨ OK! ✨
Improving code style¶
While the code is 100% matching, it can be improved in terms of style. Let’s apply some code style changes that don’t affect matching (always check!).
1#include "OpenSHC/AI/AICState.hpp"
2#include "OpenSHC/AI/AITypeA.hpp"
3#include "OpenSHC/Game/Resources/ResourceType.hpp"
4#include "OpenSHC/Globals/DAT_AICState.hpp"
5#include "OpenSHC/Globals/DAT_GameState.hpp"
6
7namespace OpenSHC {
8namespace AI {
9
10 using OpenSHC::AI::AITypeA;
11 using OpenSHC::Game::Resources::ResourceType;
12
13 // FUNCTION: STRONGHOLDCRUSADER 0x004CB060
14 void AICState::setFoodBuyPlan(int playerID)
15 {
16
17 int aiType = DAT_GameState::ptr->playerDataArray[playerID].aiType;
18
19 // Early return if not an AI player (human player has aiType == 0)
20 if (aiType == AITypeA::AITA_NULL) {
21 return;
22 }
23
24 // Get AI configuration index (convert 1-based to 0-based index)
25 int aiConfigIndex = aiType - 1;
26
27 // Check each food type and queue purchase if current stock is below minimum
28
29 // Apples: special handling with >= 0 check (allows -1 to disable)
30 int minimumApples = this->DAT_AICArray[aiConfigIndex].minimumApples;
31 int* currentResources = DAT_GameState::ptr->playerDataArray[playerID].currentResources;
32 if (minimumApples >= 0 && currentResources[ResourceType::RT_APPLE] < minimumApples) {
33 DAT_GameState::ptr->playerDataArray[playerID].resourcesToAcquireArray[ResourceType::RT_APPLE]
34 = this->DAT_AICArray[aiConfigIndex].tradeAmountFood;
35 }
36
37 // Cheese: standard check with > 0
38 int minimumCheese = this->DAT_AICArray[aiConfigIndex].minimumCheese;
39 if (minimumCheese > 0 && currentResources[ResourceType::RT_CHEESE] < minimumCheese) {
40 DAT_GameState::ptr->playerDataArray[playerID].resourcesToAcquireArray[ResourceType::RT_CHEESE]
41 = this->DAT_AICArray[aiConfigIndex].tradeAmountFood;
42 }
43
44 // Bread: standard check with > 0
45 int minimumBread = this->DAT_AICArray[aiConfigIndex].minimumBread;
46 if (minimumBread > 0 && currentResources[ResourceType::RT_BREAD] < minimumBread) {
47 DAT_GameState::ptr->playerDataArray[playerID].resourcesToAcquireArray[ResourceType::RT_BREAD]
48 = this->DAT_AICArray[aiConfigIndex].tradeAmountFood;
49 }
50
51 // Wheat: standard check with > 0
52 int minimumWheat = this->DAT_AICArray[aiConfigIndex].minimumWheat;
53 if (minimumWheat > 0 && currentResources[ResourceType::RT_WHEAT] < minimumWheat) {
54 DAT_GameState::ptr->playerDataArray[playerID].resourcesToAcquireArray[ResourceType::RT_WHEAT]
55 = this->DAT_AICArray[aiConfigIndex].tradeAmountFood;
56 }
57
58 // Hops: standard check with > 0
59 int minimumHop = this->DAT_AICArray[aiConfigIndex].minimumHop;
60 if (minimumHop > 0 && currentResources[ResourceType::RT_HOPS] < minimumHop) {
61 DAT_GameState::ptr->playerDataArray[playerID].resourcesToAcquireArray[ResourceType::RT_HOPS]
62 = this->DAT_AICArray[aiConfigIndex].tradeAmountFood;
63 }
64 }
65}
66}
Step 8: Documentation and bug hunting¶
Note that this 100% reimplementation also revealed a bug or quirk (or feature?): minimumApples permits 0 as a valid value for minimum stock, which doesn’t make sense as can be seen one line later as currentResources being smaller than 0 doesn’t make any sense (unless the AI cheats with negative stock values?).
It is best practice to note down such oddities in code comments.
Step 9: Preparing a Pull Request¶
In the file status/addresses-SHC-3BB0A8C1.txt, we set entry SHC_3BB0A8C1_0x004CB060 to 100% and change Pending into Completed. If we didn’t achieve 100% in Step 7, we note here what prevented full reimplementation. For example, if the reimplementation appears to be functionally identical but optimizations that are only applied to exe files hinder 100%, write: Functional reimplementation: compiler optimization prevented identical reimplementation.
Step 10: open the Pull Request¶
The Pull Request should contain the changes to the .cpp file and the update to the txt file found under status/. The body of your Pull Request should contain the status of the reimplementation and the output of reccmp if not 100% (run reccmp without --verbose if you file multiple functions in the PR).
Conclusion¶
Happy reimplementing!