From e9beb9520d01b13d8e8ec511a177518f4edcbff6 Mon Sep 17 00:00:00 2001 From: Gynt Date: Mon, 19 Oct 2026 21:38:59 +0200 Subject: [PATCH] reimplement: AIVState methods that are 100% --- IMPLEMENTATION_CHEAT_SHEET.md | 194 +++++------ cmake/compiler-flags-gl.txt | 5 +- .../AI/AIVState/Constructor_AIVState.cpp | 14 + .../AI/AIVState/aiPlaceAIVBuilding.cpp | 321 ++++++++++++++++++ .../AI/AIVState/aiPlaceAdditionalOxtether.cpp | 59 ++++ src/OpenSHC/AI/AIVState/aiPlaceBrazier.cpp | 49 +++ src/OpenSHC/AI/AIVState/aiPlaceFarm.cpp | 31 ++ src/OpenSHC/AI/AIVState/aiPlaceIronMine.cpp | 38 +++ src/OpenSHC/AI/AIVState/aiPlacePitchrig.cpp | 38 +++ .../AI/AIVState/aiPlaceQuarryAndOxtether.cpp | 77 +++++ src/OpenSHC/AI/AIVState/clearTheHeatmaps.cpp | 18 + .../AI/AIVState/computeAIVPlacementFit.cpp | 68 ++++ ...rtAIVBuildingTypeToCommandBuildingType.cpp | 48 +++ ...ateGridLocationForResourceTypeBuilding.cpp | 147 ++++++++ .../findAppropriateWoodCutterGridLocation.cpp | 102 ++++++ .../AIVState/findSpotNearEngineersGuild.cpp | 30 ++ .../findSuitableFarmLocationAndPlaceFarm.cpp | 119 +++++++ ...indSuitableGridLocationFittingCriteria.cpp | 74 ++++ .../incrementStructureHeatMapTile.cpp | 19 ++ .../recomputeAIAvailableGridTiles.cpp | 125 +++++++ .../AI/AIVState/resetCountdownInHeatMap.cpp | 14 + .../AIVState/selectBestAIVwithRandomStart.cpp | 62 ++++ .../setAIVFilePresenceByFileHashArray.cpp | 47 +++ src/OpenSHC/AI/AIVState/setAvailableAIV.cpp | 40 +++ .../AIVState/setKeepOffsetAndOrientation.cpp | 35 ++ .../syncAIPlayerNamesAndBuildIntervals.cpp | 35 ++ .../tryPlaceAIVAndReturnFitPercentage.cpp | 44 +++ .../AI/AIVState/wipeAIVsAndHeatMaps.cpp | 22 ++ status/addresses-SHC-3BB0A8C1.txt | 76 ++--- 29 files changed, 1797 insertions(+), 154 deletions(-) create mode 100644 src/OpenSHC/AI/AIVState/Constructor_AIVState.cpp create mode 100644 src/OpenSHC/AI/AIVState/aiPlaceAIVBuilding.cpp create mode 100644 src/OpenSHC/AI/AIVState/aiPlaceAdditionalOxtether.cpp create mode 100644 src/OpenSHC/AI/AIVState/aiPlaceBrazier.cpp create mode 100644 src/OpenSHC/AI/AIVState/aiPlaceFarm.cpp create mode 100644 src/OpenSHC/AI/AIVState/aiPlaceIronMine.cpp create mode 100644 src/OpenSHC/AI/AIVState/aiPlacePitchrig.cpp create mode 100644 src/OpenSHC/AI/AIVState/aiPlaceQuarryAndOxtether.cpp create mode 100644 src/OpenSHC/AI/AIVState/clearTheHeatmaps.cpp create mode 100644 src/OpenSHC/AI/AIVState/computeAIVPlacementFit.cpp create mode 100644 src/OpenSHC/AI/AIVState/convertAIVBuildingTypeToCommandBuildingType.cpp create mode 100644 src/OpenSHC/AI/AIVState/findAppropriateGridLocationForResourceTypeBuilding.cpp create mode 100644 src/OpenSHC/AI/AIVState/findAppropriateWoodCutterGridLocation.cpp create mode 100644 src/OpenSHC/AI/AIVState/findSpotNearEngineersGuild.cpp create mode 100644 src/OpenSHC/AI/AIVState/findSuitableFarmLocationAndPlaceFarm.cpp create mode 100644 src/OpenSHC/AI/AIVState/findSuitableGridLocationFittingCriteria.cpp create mode 100644 src/OpenSHC/AI/AIVState/incrementStructureHeatMapTile.cpp create mode 100644 src/OpenSHC/AI/AIVState/recomputeAIAvailableGridTiles.cpp create mode 100644 src/OpenSHC/AI/AIVState/resetCountdownInHeatMap.cpp create mode 100644 src/OpenSHC/AI/AIVState/selectBestAIVwithRandomStart.cpp create mode 100644 src/OpenSHC/AI/AIVState/setAIVFilePresenceByFileHashArray.cpp create mode 100644 src/OpenSHC/AI/AIVState/setAvailableAIV.cpp create mode 100644 src/OpenSHC/AI/AIVState/setKeepOffsetAndOrientation.cpp create mode 100644 src/OpenSHC/AI/AIVState/syncAIPlayerNamesAndBuildIntervals.cpp create mode 100644 src/OpenSHC/AI/AIVState/tryPlaceAIVAndReturnFitPercentage.cpp create mode 100644 src/OpenSHC/AI/AIVState/wipeAIVsAndHeatMaps.cpp diff --git a/IMPLEMENTATION_CHEAT_SHEET.md b/IMPLEMENTATION_CHEAT_SHEET.md index e29a3b222..1e436f53c 100644 --- a/IMPLEMENTATION_CHEAT_SHEET.md +++ b/IMPLEMENTATION_CHEAT_SHEET.md @@ -4,6 +4,13 @@ This file is meant to contain general behaviors, patterns and oddities of the co It should be used as a reference for the reimplementation process by providing possible options on how the code can be changed to produce different assembly while keeping the logic flow. Update whenever new information is discovered! +reccmp renders the same bytes differently on the two sides, so a diff line is not automatically a mismatch. The +original's side resolves addresses through the `StructResolver`/`trampoline` symbols and prints `__security_cookie` +as `StructResolver::Instance::instance`, and it will print a plain immediate such as +`0xffffff` as whatever global happens to live at that address. Eleven functions in `GameSynchronyState` score +between 84% and 98% while being byte-identical for this reason alone. Before working on a function, canonicalise +the `-`/`+` lines (symbols to one token, call/jump targets to one token) and check they are not already equal. + Note, that Ghidra and reccmp use slightly different disassembly styles, so there might be different opcodes generated. Example: JZ (Jump if Zero) and JE (Jump if Equal) mean the same and are the same instruction. So do not get confused. ## Boundaries @@ -98,6 +105,18 @@ This also applies when putting instructions inside the condition branches. Naturally, the logic flow needs to be kept, so the update of a variable can not move before or after another usage of the same variable. +The order in which **initialised** declarations appear decides which register each one gets. `countPlayersInSameTeam` +matched instruction for instruction except that the parameter was loaded into `eax` instead of `edx`; moving +`int iVar2 = 0;` in front of `int iVar1 = DAT_GameState::instance.mapAndTime.playerTeams[playerID];` freed `edx` +for the parameter load and took it from 95.7% to 100%. Uninitialised declarations do not have this effect - a +sweep of `quick_reorder.py -s` over the declaration block of 27 functions found no win at all. + +Stack *slot* assignment for spilled locals is a different matter and is not reachable from the source. Where the +original lays its locals out in Ghidra order (`local_1c` at the lowest `esp` offset through `local_4` at the +highest) and our build permutes them, neither declaration order, initialisation order, nor renaming changes +anything; this was tried on `setupSkirmishLobby`, `initMultiplayerLobbyState`, `initializeFinalResultsForActivePlayers` +and `createCompoundAddressBasedOnSelectedProvider`. Spend the effort elsewhere. + ### Statement Order The order of instructions in the assembly is not the order of the statements in the original source. @@ -168,6 +187,11 @@ SEC_RNG::ptr->currentNumber1 % 4 ### Variables +A 16-bit field saved into a `short` temp and written straight back is loaded with `movzx`; the same field saved into +an `int` temp is loaded with `movsx`. When the original sign-extends a `short` field it only reads and restores, +widen the temp rather than changing the struct (`recomputeHashesAndSendResync`, six entity fields, +0.6%). + + - Ghidra declares all variables at the start of a function. Please try to declare them when needed. Helps the readability. - Always prefer direct usage of parameters and member variables over the creation of a local temporary on first attempts. Real local variables are rare. - This may even include cases that can get pretty long in our style, like a variable access providing the index for an array. @@ -253,20 +277,6 @@ SEC_RNG::ptr->currentNumber1 % 4 `unitID / 16` and `unitID % 16` into locals before the compound assignment. If a `|=`/`&=` statement with computed index and mask does not match, give the index and the mask a local each and keep the declaration order the assembly shows. -- **Ghidra's `_nextIndex`/`_index2` temporaries with a `LAB: index = _nextIndex;` tail are an in-place `i++`.** Every - branch of the body assigning a "next" copy that one shared label writes back is how the decompiler renders a loop - that updates its own index. Deleting the temporaries and incrementing in place removed two stack slots and brought - `handleCommandLineArguments` to the original's `sub esp, 0x40c` (14.6% -> 31.3%). -- A local written just before a call and read back just after it (`local_408 = _candidateIndex; ... call ...; - _candidateIndex = local_408;`) is usually not a variable at all but the spill slot of the one it copies. Deleting it - and using the original variable throughout was worth 5% on the same function. -- A flag the decompiler types as `bool` is often an `int`. `bool` gives `mov bl, 1` / `xor bl, bl` / `test bl, bl` - where the original uses a full register (`getCommandIDFromCommandSelectionStuff` 43.8% -> 50.3%). Measure it, the - reverse happens too (`checkSkirmishGameDefeat` came out 0.3% worse as `int`). -- An element the original addresses with one `lea` and then several displacements off that register wants a pointer - local, not repeated `array[index]`: `GameCommand* _pCommand = &this->DAT_GameCommandArray[id];` replacing two - indexed accesses took `scheduleReceivedCommand` 90.5% -> 96.9%. - ### Conditionals - Ghidra's decompiler sometimes uses a number off by one for the condition compared to the assembly. Use the assembly as reference in this case. @@ -319,22 +329,6 @@ SEC_RNG::ptr->currentNumber1 % 4 } ``` Consider this, should a switch structure arise with strange fallthrough and loops, like SHC_3BB0A8C1_0x004870B0. -- Non-consecutive case labels will likely result in a mixture of if-else and switch cases, sometimes even only if-elses. These are hard to spot. One sign, outside of weird decompiler artifacts, is that logic might be put inside a lot of conditions that feature only a single variable. Another can be a lot of GOTOs. - -- Write a comparison with its operands in the order the assembly shows. `if (a > b)` gives `cmp eax, ecx` where - `if (b < a)` gives `cmp ecx, eax`; flipping seven `iVar6 < lag[n].average1` to `lag[n].average1 > iVar6` took - `broadcastDesyncResyncCommands` 90.7% -> 95.8%, and four of them took `determineGameTicksToPerform` 46.2% -> 64.7%. - Use the literal the assembly shows as well: `<= 0x2710` rather than `< 0x2711`, `0 <= x` rather than `-1 < x`. -- **A call reached from several branches with its arguments in variables was written once per branch.** When the - original pushes literals in each arm (`push 0xfffd` / `push 0x601`) where we build `_dwFlags`/`_dwPriority` and - share one call site, duplicate the whole call into every arm with the constants spelled out: `transmitCommand` - 72.1% -> 98.5%. This is the opposite of *Several exits that end in the same statement* under **Loops**: MSVC - copies a shared tail of plain stores into each predecessor by itself, but a call whose constant arguments differ - per arm was written out per arm. Measure both. -- `(-(uint)(c) & MASK) + BASE` has three candidate forms and they do not rank the same way twice: leaving the mask, - an `if` that overwrites a default, and a ternary. `broadcastDesyncResyncCommands` wanted the ternary - (92.3% mask / 92.1% `if` / 93.8% ternary), `renderDebugDataSplitInfo` wanted the `if` - (31.5% / 51.8% / 31.0%). Try all three. ### Loops @@ -387,8 +381,6 @@ you might have found an unrolled loop. Therefore, try to reproduce the logic in If GOTOs are present that clearly jump to the start of a loop, but the logic does not allow to do this without a GOTO, for example from a loop inside a loop, you might be able to move the continue or break condition to the outside. Methods could be placing a fitting condition related to the contained loop conditions after the loop or using a boolean flag that then functions as conditional. Both can sometimes be optimized away. -If an explicit loop condition is removed from the compiler, it is able to prove all paths through the loop. If such a case happens where the condition is actually eliminated from the re-implementation, the re-implementation might be missing a condition that is present in the logic somewhere, but removed from the loop condition itself. The optimization pass failed to detect the resulting predictable loop in this latter case and the condition remains. - A global used as a running counter alongside the loop index is usually incremented inside the loop, even when its final value is a constant. Ghidra shows the folded result: `DAT_CurrentUnitSlotID = 2500;` placed around the loop is really `DAT_CurrentUnitSlotID = 1;` before it and `DAT_CurrentUnitSlotID += 1;` as the first statement of the body. This was @@ -425,27 +417,28 @@ unrolled body, write `for (int i = 1; i != 2500; ++i)` instead of `i < 2500` (SH function needed `int i = 0;` declared *first*, before the other zero-initialised locals, to get the original's `xor` order - declaration order did not matter anywhere else it was tried. +- When a loop walks a pointer **and** counts an index, the two initialisers are not interchangeable. The original + of `broadcastDesyncResyncCommands` sets the index first (`xor edi, edi`) and the row pointer second, where our + source had the pointer first; swapping the two statements was worth 0.3%. In the same function the original + starts the walker one element in and advances it at the **tail**, next to the index increment, where the source + had `do { p = p + 1; ... }` - moving the `+ 1` into the initialiser and the advance to the loop tail took it from + 96.1% to 97.4%. +- A Ghidra temp reused as the index of several unrelated loops costs a spill. `recomputeHashesAndSendResync` shared + `iVar39` between its hash accumulator and the climb-data loop; giving that loop its own `int _climbIndex` kept the + index in a register as the original does (58.4% -> 58.8%). Splitting the *other* loops that share the same temp + changed nothing, so split the loop that the diff points at rather than all of them. +- An early `return ;` is sometimes a fall-through in the original. `addPlayerToCurrentPlayerArray` ends its + last search loop with `return 0;`, but the original zeroes the slot index and falls into the shared store, writing + the id into slot 0 before returning it. The tell is a store between the `pop`s of the epilogue + (`xor eax, eax; pop esi; pop edi; mov [ecx + eax*4 + ...], ebx; pop ebx; ret`) - one epilogue where the + reimplementation has two. + - `mov edx, esi; add esi, 1` at the **top** of a loop body, with the body then indexing by the copy, is a `while` loop that takes the index and advances the counter first: `while (i < count) { int index = i; i++; if (...[index]...) { ...; return; } }`. A `for (...; i++)` with the same body does not produce it. Together with `int i = 0;` declared where the original's `xor esi, esi` sits (here before an earlier early-return test, which then compares against that zero register) this took `generateSiegeCreationInformation` from 39.4% to 100%. -- **Ghidra's `while (cond) { i++; if (N <= i) goto LAB; }` is a `for` loop whose body holds the whole block that - follows it, ending in `break`.** The decompiler lifts the one interesting iteration out of the loop and leaves a - goto into the code after it. Putting that code back inside the `if` in the body, replacing the `goto` with `break` - and keeping whatever genuinely follows the loop after it was the highest-yield rewrite found in - `GameSynchronyState`: `checkLagAndSyncStatus` 36.8% -> 100%, `advanceSyncStatusAndKickLaggers` 59.4% -> 100%, - `checkGameLagStatusAndKickAccordingly` 34.1% -> 100%. Read the function from the raw disassembly and write it out - fresh rather than patching the decompiler's control flow. -- A pointer walked over a member array is compared **signed** in the original (`jl`), so write - `(int)p < (int)(array + 9)`; a plain pointer `<` emits `jb` and costs the last two percent - (`checkGameLagStatusAndKickAccordingly` 98.0% -> 100%). -- An early exit out of a loop nest that the decompiler renders as `goto` to a shared tail is often a `return` - duplicated into every site. Inlining the three statements plus `return` at all 13 `goto` sites took - `sendPendingResyncCommandsInBudget` 57.5% -> 63.2%, and it fixed a real bug: the shared tail fell through and - carried on looping where the original returns. - ### GOTO A function may contain multiple GOTOs. @@ -487,37 +480,37 @@ case is a function that returns the ID it was passed: changing `void EntityState to `int` with a trailing `return entityID;` took SHC_3BB0A8C1_0x004039B0 from 36.4% to 100%. Fix the signature in the generated header rather than working around the difference. -### Link-Time Code Generation +### Per-file optimization pragmas -A volatile register (usually ECX or EDX) that stays live across a call in the original, without being reloaded, means -that call was compiled with LTCG. This is fixable, not a permanent blocker: add the `.cpp` to `cmake/compiler-flags-gl.txt` -so it is built with `/GL`. SHC_3BB0A8C1_0x00504EE0 went from 38.5% to 100% and SHC_3BB0A8C1_0x0045B7F0 to 100% that way. +`/O2` is a bundle (`/Og /Oi /Ot /Oy /Ob2 /GF /Gy`), and a few translation units in the original were plainly not +built with all of it. Two of its parts are worth testing per file when a function is stuck and the diff shows +structural noise rather than a source bug: -`/GL` on both sides is not sufficient on its own. The callee also has to be **enabled in its `.func.hpp`**, i.e. -`MACRO_FUNCTION_RESOLVER(..., true, Address::...)`, because with `false` the `MACRO_CALL_MEMBER` dispatches to the -original binary and the compiler has nothing to analyse - it cannot know the callee leaves `ecx` alone, so the caller -spills `this`. Enabling the callee is only legitimate once that callee is itself at 100%. +- `#pragma inline_depth(1)` - `/Ob2` inlines things the original did not. `disconnectDPlay` 66.0% -> 87.4% and + `createCompoundAddressBasedOnSelectedProvider` 54.1% -> 61.2%, `checkMultiplayerLaggingPlayers` 90.6% -> 91.0%. + Depth 1 beat depth 0 by about a point on both of the first two; depths 2 and 3 are the same as the default. +- `#pragma optimize("y", off)` - with `/Oy` the optimizer may use `ebp` as a general register and cross-jump + identical epilogues into one. `waitForMultiplayerHost` has two `DAT_HostAnnounced = 1` returns that the original + keeps separate: 87.0% -> 98.9%. Note that putting `/Oy-` on the command line through + `set_source_files_properties` does **not** reproduce this; only the pragma does. -`processSyncPacket` sat at 26.2% for exactly this reason, with the whole diff being `mov esi, ecx` and the register -rename it causes. Flipping `translateMultiplayerIDsIntoPlayerIDs` (already 100%) to `true` took it to 79.2% -immediately, and the rest was an `abs()`. Enabling all 23 finished functions of `GameSynchronyState` moved the -namespace average 84.6% -> 85.7% with no function getting worse. +Sweep them rather than guess: patch one file at a time, rebuild, and keep the ones that win. Over the 25 +non-matching functions of `GameSynchronyState` only four files wanted either pragma, and +`optimize("s", on)`, `optimize("g", off)`, `optimize("t", off)` and `auto_inline(off)` never won anywhere. +Toggling a file in or out of `cmake/compiler-flags-gl.txt` is worth the same sweep - it found one win +(`receiveAllTransmittedCommands`, +1.3) and showed that the `/GL` entries for `determineGameTicksToPerform` and +`computeLatencyAdjustmentFromMatchTimes` currently measure as no-ops. -So the measurement setup is two switches, not one: `Enable-Reimplemented-Data.ps1` / -`enable_reimplemented_data.py` for the struct resolvers **and** `Enable-Reimplemented-Functions.ps1` for the -function resolvers. Neither belongs in a commit. Measuring with only the data half makes every cross-function call -look worse than it is. +### Link-Time Code Generation -Explanations that were tested on this pair and are **not** the cause, so do not spend time on them: the callee being -exported from the DLL (`OpenSHC.dll` exports one symbol, `luaopen_OpenSHC`); its address being taken by -`MACRO_FUNCTION_RESOLVER`; the other callers not being in the LTCG set; and caller and callee not sharing a -translation unit. +A volatile register (usually ECX or EDX) that stays live across a call in the original, without being reloaded, means +that call was compiled with LTCG. This is fixable, not a permanent blocker: add the `.cpp` to `cmake/compiler-flags-gl.txt` +so it is built with `/GL`. SHC_3BB0A8C1_0x00504EE0 went from 38.5% to 100% and SHC_3BB0A8C1_0x0045B7F0 to 100% that way. ### Parameters Parameters might not be pushed like normal in certain cases. -Usually, if two functions are followed by each other, the parameters are pushed for the first function, then the call is executed and then parameters for the second function are pushed. If parameters for the second function are pushed before the first call, it might indicate that the first call was executed in place of a variable, to directly feed the return into the second function. -This possibility is actually universal. If a computation of a value happens between pushes of parameters, it makes it likely the value was computed in-place. The other way around is also possible. If a computed value is created completely or partially before any push, it might have been a temporary. +Usually, if two functions are followed by each other, the parameters are pushed for the first function, then the call is executed and then parameters for the second function are pushed. If parameters for the second function are pushed before the first call, it might indicate that the first call was executed in place of a variable, to directly feed the return into the second function. ### Implicit functions @@ -546,36 +539,25 @@ They are replaced by specific assembly instructions or otherwise inlined. A list of all intrinsics can be found in `intrin.h` in the std library, but these are mainly very low level instructions. However, there are also C functions that can be replaced by intrinsics. The following attempts to extract these from the header: -| Intrinsic | Does | -|---------------------------|----------------------------------------------------------------------------------------------------------------------| -| `memcpy` | Copies a memory block. **Does not support overlapping regions** (use `memmove` for overlap). | -| `memset` | Fills a memory block with a byte value. | -| `memcmp` | Compares two memory blocks byte-by-byte. | -| `memchr` | Searches memory for the first occurrence of a byte. | -| `strcpy` | Copies a null-terminated C string. | -| `strlen` | Returns the length of a null-terminated string (excluding `\0`). | -| `strcmp` | Compares two null-terminated C strings. | -| `strcat` | Appends one null-terminated C string to another. | -| `strncpy` | Copies up to N characters from a string; **may not null-terminate** if the source is too long. | -| `strncmp` | Compares up to N characters of two strings. | -| `strset` | Sets every character in a null-terminated string to the specified character. | -| `_strset` | Microsoft-specific version of `strset`; sets every character in a null-terminated string to the specified character. | -| `wcscpy` | Copies a null-terminated wide-character string. | -| `wcslen` | Returns the length of a null-terminated wide string. | -| `wcscmp` | Compares two null-terminated wide-character strings. | -| `wcscat` | Appends one null-terminated wide-character string to another. | -| `wcsncpy` | Copies up to N wide characters; **may not null-terminate** if the source is too long. | -| `wcsncmp` | Compares up to N wide characters of two strings. | -| `_wcsset` | Sets every character in a null-terminated wide string to the specified wide character. | -| `ceil` | Returns the smallest integral floating-point value greater than or equal to the argument. | -| `abs` | Returns the absolute value of an `int`. | -| `labs` | Returns the absolute value of a `long`. | -| `longjmp` | Restores a saved execution context created by `setjmp`, continuing execution from that point. | -| `_setjmp` | Saves the current execution context for later restoration with `longjmp`. | -| `_AddressOfReturnAddress` | Returns the address of the current function's return-address storage. | -| `_WriteBarrier` | Prevents certain compiler memory-write reorderings across the barrier. | -| `__trap` | Generates a processor/compiler trap, optionally with arguments. | - +| Intrinsic | Does | +| --------- | ---------------------------------------------------------------------------------------------- | +| `memcpy` | Copies a memory block. **Does not support overlapping regions** (use `memmove` for overlap). | +| `memset` | Fills a memory block with a byte value. | +| `memcmp` | Compares two memory blocks byte-by-byte. | +| `memchr` | Searches memory for the first occurrence of a byte. | +| `strcpy` | Copies a null-terminated C string. | +| `strlen` | Returns the length of a null-terminated string (excluding `\0`). | +| `strcmp` | Compares two null-terminated C strings. | +| `strcat` | Appends one null-terminated C string to another. | +| `strncpy` | Copies up to N characters from a string; **may not null-terminate** if the source is too long. | +| `strncmp` | Compares up to N characters of two strings. | +| `wcscpy` | Copies a null-terminated wide-character string. | +| `wcslen` | Returns the length of a null-terminated wide string. | +| `ceil` | Rounds a floating-point value upward to the nearest integer value. | +| `abs` | Returns the absolute value of an `int`. | +| `labs` | Returns the absolute value of a `long`. | +| `longjmp` | Restores a saved execution context created by `setjmp`, continuing execution from that point. | +| `_setjmp` | Saves the current execution context for later restoration with `longjmp`. | In cases where the decompiler seems to perform one of these actions via simple instructions, one can also try one of these functions. @@ -606,6 +588,12 @@ do { #### Known special cases +- `memset` emits its stores from the **lowest** address upwards. When the original clears two adjacent fields from a + zero register in the other order, the `memset` is wrong even though the effect is identical: in + `recomputeHashesAndSendResync` the original clears `field309_0x420` before `field308_0x41c`, and replacing + `memset(&unit.field308_0x41c, 0, 8)` with those two stores in that order was worth 4 points (54.4% -> 58.4%). + Check the store order in the diff before reaching for `memset`, and keep it where the order does match - the two + other `memset`s in the same function both lose 3-9 points when expanded. - `memset` so far was seen optimizing a "set all bytes to zero" case, if it was smaller then a unknown amount. In all known cases it used `EAX` for this. Would still recommend to try `memset` for cases with a register filling multiple **byte-sized** memory locations. ### CRT Functions @@ -616,14 +604,6 @@ since the memory management in the std library is rather complex. For other std functions, mostly the math functions, we decided to just use the std library directly. -- A run of adjacent single-byte zero stores in our output against dword and word stores in the original is an inlined - `memset`. Thirty `this->connectTarget[n] = '\0';` became seven `mov dword ptr ..., eax` plus one `mov word` when - written `memset(this->connectTarget, 0, 30);`, which also freed the register MSVC had been holding the zero in - (`handleCommandLineArguments` 35.7% -> 39.0%). -- MSVC folds a two-character `strcpy(p, "\\")` to `mov word ptr [p], 0x5c`. Where the original loads the literal - instead (`mov dx, word ptr [str]; mov word ptr [edi], dx`) the source passed the string through a `char*` - variable (`readGameSpyConfig` 53.3% -> 58.5%). - ### Copy Elision and Return Value Optimization The compiler may use copy elision and return value optimization. diff --git a/cmake/compiler-flags-gl.txt b/cmake/compiler-flags-gl.txt index d0c81b8d5..162955856 100644 --- a/cmake/compiler-flags-gl.txt +++ b/cmake/compiler-flags-gl.txt @@ -16,4 +16,7 @@ src/OpenSHC/Audio/SFX/SFXState/playUnitSpeech.cpp src/OpenSHC/Game/GameStateStructures/getSellResourceAmount.cpp src/OpenSHC/Map/TileMapState/determineBuildingPlacementRotation.cpp src/OpenSHC/Map/Navigation/DirectionAlgorithmState/getMouseVectorLengthBasedOnDirection.cpp -src/OpenSHC/Input/MouseState/updateRightDragCameraControl.cpp \ No newline at end of file +src/OpenSHC/Input/MouseState/updateRightDragCameraControl.cpp + +src/OpenSHC/AI/AIVState/executeDefaultCastleAIV.cpp +src/OpenSHC/AI/AIVState/findAppropriateGridLocationForBuilding.cpp diff --git a/src/OpenSHC/AI/AIVState/Constructor_AIVState.cpp b/src/OpenSHC/AI/AIVState/Constructor_AIVState.cpp new file mode 100644 index 000000000..6e2dd4d6c --- /dev/null +++ b/src/OpenSHC/AI/AIVState/Constructor_AIVState.cpp @@ -0,0 +1,14 @@ +#include "../AIVState.func.hpp" + +namespace OpenSHC { +namespace AI { + + // FUNCTION: STRONGHOLDCRUSADER 0x004F13E0 + AIVState* AIVState::Constructor_AIVState() + { + MACRO_CALL_MEMBER(AIVState_Func::wipeAIVsAndHeatMaps, this)(); + return this; + } + +} +} diff --git a/src/OpenSHC/AI/AIVState/aiPlaceAIVBuilding.cpp b/src/OpenSHC/AI/AIVState/aiPlaceAIVBuilding.cpp new file mode 100644 index 000000000..a4f565788 --- /dev/null +++ b/src/OpenSHC/AI/AIVState/aiPlaceAIVBuilding.cpp @@ -0,0 +1,321 @@ +#include "../AIVState.func.hpp" + +#include "OpenSHC/AI/AICState.func.hpp" +#include "OpenSHC/Game/GameStateStructures.func.hpp" +#include "OpenSHC/Map/Buildings/BuildingsState.func.hpp" +#include "OpenSHC/Map/TileMapState.func.hpp" + +#include "OpenSHC/Globals/DAT_AICState.hpp" +#include "OpenSHC/Globals/DAT_BuildingsState.hpp" +#include "OpenSHC/Globals/DAT_GameState.hpp" +#include "OpenSHC/Globals/DAT_TerrainDefinedData.hpp" +#include "OpenSHC/Globals/DAT_TileMapState.hpp" +#include "OpenSHC/Globals/DAT_ViewportRenderState.hpp" + +namespace OpenSHC { +namespace AI { + + // FUNCTION: STRONGHOLDCRUSADER 0x004ED410 + BOOLEnum AIVState::aiPlaceAIVBuilding(int playerID, int step, int isPausing) + { + int const aivID = DAT_GameState::instance.playerDataArray[playerID].aivID; + int x; + int y; + MappersEnum const mapper = (MappersEnum)this->aivs[aivID].aivBuildingSteps[step].buildingType; + + // when pausing, only hovels are built + if (isPausing && mapper != Commands::M_MAPPER_HOVEL) { + return FALSE; + } + // disabled steps and steps without room are never built + if (this->aivs[aivID].aivBuildingSteps[step].buildStatus == AIVBSS_disabled) { + return FALSE; + } + if (this->aivs[aivID].aivBuildingSteps[step].buildStatus == AIVBSS_insufficient_roomUnk) { + return FALSE; + } + if (this->aivs[aivID].aivBuildingSteps[step].buildStatus == AIVBSS_insufficient_resources + && this->aivs[aivID].aivBuildingSteps[step].wait > 0) { + // wait for more resources + --this->aivs[aivID].aivBuildingSteps[step].wait; + return FALSE; + } + if (this->aivs[aivID].aivBuildingSteps[step].buildStatus == AIVBSS_built) { + if (DAT_GameState::instance.playerDataArray[playerID].closestEnemyDistanceToTheLord < 120) { + return FALSE; + } + } else if (!MACRO_CALL_MEMBER(Game::GameStateStructures_Func::checkRequiredResourcesForBuildingOrPlanToBuy, + DAT_GameState::ptr)(mapper, playerID, FALSE)) { + return FALSE; + } + if (DAT_BuildingsState::instance.unknownCountdown01 < 20) { + return FALSE; + } + if (MACRO_CALL_MEMBER(AICState_Func::mapperIsWoodCuttersHutWhileNoWoodAvailable, DAT_AICState::ptr)( + playerID, mapper)) { + return FALSE; + } + + if (mapper == Commands::M_MAPPER_MOAT) { + if (this->aivs[aivID].aivBuildingSteps[step].buildStatus == AIVBSS_built + && this->aivs[aivID].aivBuildingSteps[step].wait > 0) { + --this->aivs[aivID].aivBuildingSteps[step].wait; + return FALSE; + } + for (int i = 0; i < this->aivs[aivID].aivBuildingSteps[step].quantity; ++i) { + int const tile + = this->aivs[aivID].locationsArray[this->aivs[aivID].aivBuildingSteps[step].location.tile.tile + i]; + MACRO_CALL_MEMBER(Map::TileMapState_Func::planMoat, DAT_TileMapState::ptr)( + playerID, tile, DAT_ViewportRenderState::instance.tileTranslationMatrix_YComponent[tile]); + } + this->aivs[aivID].aivBuildingSteps[step].buildStatus = AIVBSS_built; + this->aivs[aivID].aivBuildingSteps[step].wait = 50; + return FALSE; + } + + if (mapper == Commands::M_MAPPER_PITCH_DITCH) { + if (this->aivs[aivID].aivBuildingSteps[step].buildStatus == AIVBSS_built) { + if (this->aivs[aivID].aivBuildingSteps[step].wait > 0) { + --this->aivs[aivID].aivBuildingSteps[step].wait; + return FALSE; + } + if (!MACRO_CALL_MEMBER(Game::GameStateStructures_Func::checkRequiredResourcesForBuildingOrPlanToBuy, + DAT_GameState::ptr)(Commands::M_MAPPER_PITCH_DITCH, playerID, FALSE)) { + return FALSE; + } + } + for (int i = 0; i < this->aivs[aivID].aivBuildingSteps[step].quantity; ++i) { + int const tile + = this->aivs[aivID].locationsArray[this->aivs[aivID].aivBuildingSteps[step].location.tile.tile + i]; + y = DAT_ViewportRenderState::instance.tileTranslationMatrix_YComponent[tile]; + x = tile - DAT_ViewportRenderState::instance.translationMatrix[y].addXgetTile; + MACRO_CALL_MEMBER(Map::TileMapState_Func::placeBuilding, DAT_TileMapState::ptr)(playerID, x, y, + Commands::M_MAPPER_PITCH_DITCH, + MACRO_CALL_MEMBER(Map::TileMapState_Func::getBuildingSizeForCommandBuildingType, + DAT_TileMapState::ptr)(Commands::M_MAPPER_PITCH_DITCH), + // rotation 15 is a magic value: the building ends up with rotation 0 + // unless DAT_TileMapState::skipPlacementCheck is set + 15); + } + this->aivs[aivID].aivBuildingSteps[step].buildStatus = AIVBSS_built; + this->aivs[aivID].aivBuildingSteps[step].wait = 200; + return FALSE; + } + + // walls + if (mapper == Commands::M_MAPPER_WALL || mapper == Commands::M_MAPPER_WOODWALL + || mapper == Commands::M_MAPPER_CRENAL || mapper == Commands::M_MAPPER_CRENAL2) { + if (this->aivs[aivID].aivBuildingSteps[step].buildStatus == AIVBSS_built) { + if (this->aivs[aivID].aivBuildingSteps[step].wait > 0) { + --this->aivs[aivID].aivBuildingSteps[step].wait; + return FALSE; + } + if (DAT_GameState::instance.playerDataArray[playerID].currentResources[Game::Resources::RT_STONE] + <= 0) { + // no stone, request some + if (DAT_GameState::instance.playerDataArray[playerID] + .resourcesToAcquireArray[Game::Resources::RT_STONE] + > 0) { + return FALSE; + } + DAT_GameState::instance.playerDataArray[playerID].resourcesToAcquireArray[Game::Resources::RT_STONE] + = 5; + return FALSE; + } + } + for (int i = 0; i < this->aivs[aivID].aivBuildingSteps[step].quantity; ++i) { + int const tile + = this->aivs[aivID].locationsArray[this->aivs[aivID].aivBuildingSteps[step].location.tile.tile + i]; + y = DAT_ViewportRenderState::instance.tileTranslationMatrix_YComponent[tile]; + x = tile - DAT_ViewportRenderState::instance.translationMatrix[y].addXgetTile; + MACRO_CALL_MEMBER(Map::TileMapState_Func::prepareAreaForBuildingPlacement, DAT_TileMapState::ptr)( + playerID, x, y, mapper, 1); + MACRO_CALL_MEMBER(Map::TileMapState_Func::placeDefensiveStructureTile, DAT_TileMapState::ptr)( + playerID, x, y, mapper); + } + this->aivs[aivID].aivBuildingSteps[step].buildStatus = AIVBSS_built; + this->aivs[aivID].aivBuildingSteps[step].wait = 20; + if (!DAT_TileMapState::instance.illegalBuild) { + return TRUE; + } + } else if (mapper == Commands::M_MAPPER_STAIR1 || mapper == Commands::M_MAPPER_STAIR2 + || mapper == Commands::M_MAPPER_STAIR3 || mapper == Commands::M_MAPPER_STAIR4 + || mapper == Commands::M_MAPPER_STAIR5 || mapper == Commands::M_MAPPER_STAIR6) { + if (this->aivs[aivID].aivBuildingSteps[step].buildStatus == AIVBSS_built + && this->aivs[aivID].aivBuildingSteps[step].wait > 0) { + --this->aivs[aivID].aivBuildingSteps[step].wait; + return FALSE; + } + y = DAT_ViewportRenderState::instance + .tileTranslationMatrix_YComponent[this->aivs[aivID].aivBuildingSteps[step].location.tile.tile]; + x = this->aivs[aivID].aivBuildingSteps[step].location.tile.tile + - DAT_ViewportRenderState::instance.translationMatrix[y].addXgetTile; + MACRO_CALL_MEMBER(Map::TileMapState_Func::prepareAreaForBuildingPlacement, DAT_TileMapState::ptr)( + playerID, x, y, mapper, 1); + MACRO_CALL_MEMBER(Map::TileMapState_Func::placeDefensiveStructureTile, DAT_TileMapState::ptr)( + playerID, x, y, mapper); + this->aivs[aivID].aivBuildingSteps[step].buildStatus = AIVBSS_built; + this->aivs[aivID].aivBuildingSteps[step].wait = 1; + if (!DAT_TileMapState::instance.illegalBuild) { + return TRUE; + } + this->aivs[aivID].aivBuildingSteps[step].buildStatus = AIVBSS_insufficient_resources; + this->aivs[aivID].aivBuildingSteps[step].wait = 4; + } else { + if (MACRO_CALL_MEMBER(AICState_Func::shouldNotBeBuildingGranary, DAT_AICState::ptr)(playerID, mapper)) { + return FALSE; + } + if (MACRO_CALL_MEMBER(AICState_Func::shouldNotBeBuildingArmoury, DAT_AICState::ptr)(playerID, mapper)) { + return FALSE; + } + if (MACRO_CALL_MEMBER(AICState_Func::shouldNotBeBuildingHovel, DAT_AICState::ptr)(playerID, mapper)) { + return FALSE; + } + if (MACRO_CALL_MEMBER(AICState_Func::shouldNotBuildMarketArmoryGranaryBarracksMercenarypost, + DAT_AICState::ptr)(playerID, mapper)) { + return FALSE; + } + if (MACRO_CALL_MEMBER(AICState_Func::shouldNotBuildKillingPits, DAT_AICState::ptr)(playerID, mapper)) { + return FALSE; + } + + int const tile = this->aivs[aivID].aivBuildingSteps[step].location.tile.tile; + y = DAT_ViewportRenderState::instance.tileTranslationMatrix_YComponent[tile]; + x = tile - DAT_ViewportRenderState::instance.translationMatrix[y].addXgetTile; + int orientation = 15; + if (mapper == Commands::M_MAPPER_BARRACKS_ARAB || mapper == Commands::M_MAPPER_BARRACKS_EURO + || mapper == Commands::M_MAPPER_ENGINEERS_GUILD || mapper == Commands::M_MAPPER_TUNNELERS_GUILD + || mapper == Commands::M_MAPPER_OIL_SMELTER) { + orientation = this->aivs[aivID].keepOrientation; + } else if (mapper == Commands::M_MAPPER_GATE_STONE1A || mapper == Commands::M_MAPPER_GATE_STONE2A) { + if (this->aivs[aivID].keepOrientation == 0 || this->aivs[aivID].keepOrientation == 4) { + orientation = 81; + } else { + orientation = 80; + } + } else if (mapper == Commands::M_MAPPER_GATE_STONE1B || mapper == Commands::M_MAPPER_GATE_STONE2B) { + if (this->aivs[aivID].keepOrientation == 2 || this->aivs[aivID].keepOrientation == 6) { + orientation = 81; + } else { + orientation = 80; + } + } else if (mapper == Commands::M_MAPPER_POLETURNER || mapper == Commands::M_MAPPER_BLACKSMITH + || mapper == Commands::M_MAPPER_ARMOURER || mapper == Commands::M_MAPPER_TANNER + || mapper == Commands::M_MAPPER_FLETCHER || mapper == Commands::M_MAPPER_BAKER + || mapper == Commands::M_MAPPER_BREWER) { + orientation = 8; + } else if (mapper == Commands::M_MAPPER_DRAWBRIDGE) { + DAT_TileMapState::instance.buildingPlacementFail = FALSE; + MACRO_CALL_MEMBER(Map::TileMapState_Func::checkDrawbridgePlacement, DAT_TileMapState::ptr)(x, y); + if (DAT_TileMapState::instance.buildingPlacementFail) { + return FALSE; + } + orientation = DAT_TileMapState::instance.uiBuildingRotation; + } + + if (this->aivs[aivID].aivBuildingSteps[step].buildStatus == AIVBSS_built) { + // rebuild a destroyed building + int const buildingID = (short)DAT_TileMapState::instance.BuildingLayer[tile]; + if ((short)DAT_BuildingsState::instance.buildings[buildingID].buildingType + == MACRO_CALL_MEMBER(Map::Buildings::BuildingsState_Func::convertCommandBuildingTypeToBuildingType, + DAT_BuildingsState::ptr)(mapper)) { + return FALSE; + } + if (DAT_GameState::instance.playerDataArray[playerID].aiBuildingDestroyChoiceTracker == 0 + && DAT_GameState::instance.playerDataArray[playerID].aiNervousActionsTracker == 0 + && this->aivs[aivID].aivBuildingSteps[step].wait > 0) { + --this->aivs[aivID].aivBuildingSteps[step].wait; + return FALSE; + } + if (!MACRO_CALL_MEMBER(Game::GameStateStructures_Func::checkRequiredResourcesForBuildingOrPlanToBuy, + DAT_GameState::ptr)(mapper, playerID, FALSE)) { + return FALSE; + } + } + if (MACRO_CALL_MEMBER(AICState_Func::shouldWaitWithBuildingAPositiveFearFactor, DAT_AICState::ptr)( + playerID, mapper)) { + return FALSE; + } + if (MACRO_CALL_MEMBER(AICState_Func::shouldWaitWithBuildingNegativeFearFactor, DAT_AICState::ptr)( + playerID, mapper)) { + return FALSE; + } + + if (orientation == 15) { + DAT_TileMapState::instance.DAT_TempBuildingRotation = 0; + } else { + DAT_TileMapState::instance.DAT_TempBuildingRotation = orientation / 2; + } + int const buildingSize = MACRO_CALL_MEMBER( + Map::TileMapState_Func::getBuildingSizeForCommandBuildingType, DAT_TileMapState::ptr)(mapper); + MACRO_CALL_MEMBER(Map::TileMapState_Func::prepareAreaForBuildingPlacement, DAT_TileMapState::ptr)( + playerID, x, y, mapper, buildingSize); + // keep orientation is 0, 2, 4 or 6, halving it gives the index of the part offsets + if (mapper == Commands::M_MAPPER_BARRACKS_EURO || mapper == Commands::M_MAPPER_BARRACKS_ARAB) { + // the three fields in front of the barracks + MACRO_CALL_MEMBER(Map::TileMapState_Func::prepareAreaForBuildingPlacement, DAT_TileMapState::ptr)( + playerID, + DAT_TerrainDefinedData::instance.BuildingPartsOffsets[this->aivs[aivID].keepOrientation / 2][0].x + + x, + DAT_TerrainDefinedData::instance.BuildingPartsOffsets[this->aivs[aivID].keepOrientation / 2][0].y + + y, + mapper, buildingSize); + MACRO_CALL_MEMBER(Map::TileMapState_Func::prepareAreaForBuildingPlacement, DAT_TileMapState::ptr)( + playerID, + DAT_TerrainDefinedData::instance.BuildingPartsOffsets[this->aivs[aivID].keepOrientation / 2][1].x + + x, + DAT_TerrainDefinedData::instance.BuildingPartsOffsets[this->aivs[aivID].keepOrientation / 2][1].y + + y, + mapper, buildingSize); + MACRO_CALL_MEMBER(Map::TileMapState_Func::prepareAreaForBuildingPlacement, DAT_TileMapState::ptr)( + playerID, + DAT_TerrainDefinedData::instance.BuildingPartsOffsets[this->aivs[aivID].keepOrientation / 2][2].x + + x, + DAT_TerrainDefinedData::instance.BuildingPartsOffsets[this->aivs[aivID].keepOrientation / 2][2].y + + y, + mapper, buildingSize); + } else if (mapper == Commands::M_MAPPER_ENGINEERS_GUILD || mapper == Commands::M_MAPPER_TUNNELERS_GUILD) { + // the guild field + MACRO_CALL_MEMBER(Map::TileMapState_Func::prepareAreaForBuildingPlacement, DAT_TileMapState::ptr)( + playerID, + DAT_TerrainDefinedData::instance.BuildingPartsOffsets[this->aivs[aivID].keepOrientation / 2][1].x + + x, + DAT_TerrainDefinedData::instance.BuildingPartsOffsets[this->aivs[aivID].keepOrientation / 2][1].y + + y, + mapper, buildingSize); + } else if (mapper == Commands::M_MAPPER_OIL_SMELTER) { + MACRO_CALL_MEMBER(Map::TileMapState_Func::prepareAreaForBuildingPlacement, DAT_TileMapState::ptr)( + playerID, + DAT_TerrainDefinedData::instance.field63_0x19c[this->aivs[aivID].keepOrientation / 2].x + x, + DAT_TerrainDefinedData::instance.field63_0x19c[this->aivs[aivID].keepOrientation / 2].y + y, mapper, + buildingSize); + } + + MACRO_CALL_MEMBER(Map::TileMapState_Func::placeBuilding, DAT_TileMapState::ptr)(playerID, x, y, mapper, + MACRO_CALL_MEMBER(Map::TileMapState_Func::getBuildingSizeForCommandBuildingType, DAT_TileMapState::ptr)( + mapper), + orientation); + int const placedID = DAT_TileMapState::instance.placedBuildingID; + if (!DAT_TileMapState::instance.buildingPlacementFail) { + DAT_BuildingsState::instance.buildings[placedID].field240_0x2c8 = 1; + MACRO_CALL_MEMBER(AICState_Func::setupWorkshopProductionType, DAT_AICState::ptr)(playerID, placedID); + } + if (!DAT_TileMapState::instance.buildingPlacementFail) { + this->aivs[aivID].aivBuildingSteps[step].buildStatus = AIVBSS_built; + } else if (mapper == Commands::M_MAPPER_WHEATFARM || mapper == Commands::M_MAPPER_HOPSFARM + || mapper == Commands::M_MAPPER_APPLEFARM || mapper == Commands::M_MAPPER_CATTLEFARM) { + this->aivs[aivID].aivBuildingSteps[step].buildStatus = AIVBSS_insufficient_roomUnk; + } else { + this->aivs[aivID].aivBuildingSteps[step].buildStatus = AIVBSS_insufficient_resources; + } + this->aivs[aivID].aivBuildingSteps[step].wait + = this->aivs[aivID].aivBuildingSteps[step].buildStatus == AIVBSS_built ? 10 : 16; + if (!DAT_TileMapState::instance.buildingPlacementFail) { + return TRUE; + } + } + return FALSE; + } + +} +} diff --git a/src/OpenSHC/AI/AIVState/aiPlaceAdditionalOxtether.cpp b/src/OpenSHC/AI/AIVState/aiPlaceAdditionalOxtether.cpp new file mode 100644 index 000000000..b57bed7a2 --- /dev/null +++ b/src/OpenSHC/AI/AIVState/aiPlaceAdditionalOxtether.cpp @@ -0,0 +1,59 @@ +#include "../AIVState.func.hpp" + +#include "OpenSHC/Game/GameStateStructures.func.hpp" +#include "OpenSHC/Map/TileMapState.func.hpp" + +#include "OpenSHC/Globals/DAT_AIVDefinedData.hpp" +#include "OpenSHC/Globals/DAT_BuildingsState.hpp" +#include "OpenSHC/Globals/DAT_GameState.hpp" +#include "OpenSHC/Globals/DAT_TileMapState.hpp" + +namespace OpenSHC { +namespace AI { + + // FUNCTION: STRONGHOLDCRUSADER 0x004EFC30 + void AIVState::aiPlaceAdditionalOxtether(int playerID) + { + if (!MACRO_CALL_MEMBER(Game::GameStateStructures_Func::checkRequiredResourcesForBuildingOrPlanToBuy, + DAT_GameState::ptr)(Commands::M_MAPPER_OXENBASE, playerID, FALSE) + || DAT_BuildingsState::instance.unknownCountdown01 < 20) { + return; + } + + int const quarryID = DAT_GameState::instance.playerDataArray[playerID].highestLoadedQuarryUnk; + MACRO_CALL_MEMBER(AIVState_Func::findSuitableGridLocationFittingCriteria, this)( + (short)DAT_BuildingsState::instance.buildings[quarryID].x / 5 + + DAT_AIVDefinedData::instance + .OffsetsForFiveByFiveAreaSpotCheck[this->mapExtraInfo.algBuildingPlacementNudge % 9] + .x, + (short)DAT_BuildingsState::instance.buildings[quarryID].y / 5 + + DAT_AIVDefinedData::instance + .OffsetsForFiveByFiveAreaSpotCheck[this->mapExtraInfo.algBuildingPlacementNudge % 9] + .y); + if (this->buildingApproriateGridXPosition == -1) { + return; + } + + ++this->heatMaps[this->buildingApproriateGridXPosition][this->buildingAppropriateGridYPosition].structureCount; + MACRO_CALL_MEMBER(Map::TileMapState_Func::placeBuilding, DAT_TileMapState::ptr)(playerID, + this->buildingApproriateGridXPosition * 5, this->buildingAppropriateGridYPosition * 5, + Commands::M_MAPPER_OXENBASE, + MACRO_CALL_MEMBER(Map::TileMapState_Func::getBuildingSizeForCommandBuildingType, DAT_TileMapState::ptr)( + Commands::M_MAPPER_OXENBASE), + // rotation 15 is a magic value: the building ends up with rotation 0 + // unless DAT_TileMapState::skipPlacementCheck is set + 15); + + // link the new ox tether to the first free slot of the quarry + int const oxTetherID = DAT_TileMapState::instance.placedBuildingID; + for (int tetherIndex = 0; tetherIndex < 3; ++tetherIndex) { + if (DAT_BuildingsState::instance.buildings[quarryID].quarryLinkedOxTethers[tetherIndex] == 0) { + DAT_BuildingsState::instance.buildings[quarryID].quarryLinkedOxTethers[tetherIndex] = (short)oxTetherID; + break; + } + } + DAT_BuildingsState::instance.buildings[oxTetherID].oxtetherLinkedQuarryID = (short)quarryID; + } + +} +} diff --git a/src/OpenSHC/AI/AIVState/aiPlaceBrazier.cpp b/src/OpenSHC/AI/AIVState/aiPlaceBrazier.cpp new file mode 100644 index 000000000..a24f693aa --- /dev/null +++ b/src/OpenSHC/AI/AIVState/aiPlaceBrazier.cpp @@ -0,0 +1,49 @@ +#include "../AIVState.func.hpp" + +#include "OpenSHC/Map/Entities/EntityState.func.hpp" +#include "OpenSHC/Map/TileMapState.func.hpp" +#include "OpenSHC/AI/AIVUnitType.hpp" + +#include "OpenSHC/Globals/DAT_EntityState.hpp" +#include "OpenSHC/Globals/DAT_GameState.hpp" +#include "OpenSHC/Globals/DAT_TileMapState.hpp" +#include "OpenSHC/Globals/DAT_ViewportRenderState.hpp" + +namespace OpenSHC { +namespace AI { + + // FUNCTION: STRONGHOLDCRUSADER 0x004ED300 + void AIVState::aiPlaceBrazier(int playerID) + { + if (DAT_GameState::instance.playerDataArray[playerID].aivUnitLocationSlotLocationCount[AIVUT_BRAZIER] <= 0) { + return; + } + for (int i = 0; i < 10; ++i) { + int const tile = DAT_GameState::instance.playerDataArray[playerID].aivUnitLocationSlots[AIVUT_BRAZIER][i]; + if (tile <= 0) { + continue; + } + int const entityID = DAT_TileMapState::instance.EntityLayer[tile]; + if (entityID != 0 + && DAT_EntityState::instance.entityArray[entityID].entityType == Map::Entities::ET_BRAZIER) { + continue; + } + uint const height + = MACRO_CALL_MEMBER(Map::TileMapState_Func::getTotalHeightAtTile, DAT_TileMapState::ptr)(tile); + if ((int)height <= 0) { + continue; + } + MACRO_CALL_MEMBER(Map::Entities::EntityState_Func::spawnProjectileEntity, DAT_EntityState::ptr)(0, playerID, + playerID, + (tile + - DAT_ViewportRenderState::instance + .translationMatrix[DAT_ViewportRenderState::instance.tileTranslationMatrix_YComponent[tile]] + .addXgetTile) + * 8, + DAT_ViewportRenderState::instance.tileTranslationMatrix_YComponent[tile] * 8, height, 0, 0, 0, + Map::Entities::ET_BRAZIER, 0); + } + } + +} +} diff --git a/src/OpenSHC/AI/AIVState/aiPlaceFarm.cpp b/src/OpenSHC/AI/AIVState/aiPlaceFarm.cpp new file mode 100644 index 000000000..2360ad494 --- /dev/null +++ b/src/OpenSHC/AI/AIVState/aiPlaceFarm.cpp @@ -0,0 +1,31 @@ +#include "../AIVState.func.hpp" + +#include "OpenSHC/Game/GameStateStructures.func.hpp" +#include "OpenSHC/Map/Buildings/BuildingsState.func.hpp" + +#include "OpenSHC/Globals/DAT_BuildingsState.hpp" +#include "OpenSHC/Globals/DAT_GameState.hpp" + +namespace OpenSHC { +namespace AI { + + // FUNCTION: STRONGHOLDCRUSADER 0x004EFA60 + BOOLEnum AIVState::aiPlaceFarm(int playerID, MappersEnum buildingType) + { + // despite its declared type, buildingType holds a BuildingType + if (buildingType <= 0) { + return FALSE; + } + if (!MACRO_CALL_MEMBER( + Game::GameStateStructures_Func::checkRequiredResourcesForBuildingOrPlanToBuy, DAT_GameState::ptr)( + MACRO_CALL_MEMBER(Map::Buildings::BuildingsState_Func::convertBuildingTypeToCommandBuildingType, + DAT_BuildingsState::ptr)((Map::Buildings::BuildingType)buildingType), + playerID, FALSE)) { + return FALSE; + } + return MACRO_CALL_MEMBER(AIVState_Func::findSuitableFarmLocationAndPlaceFarm, this)(playerID, buildingType) + != FALSE; + } + +} +} diff --git a/src/OpenSHC/AI/AIVState/aiPlaceIronMine.cpp b/src/OpenSHC/AI/AIVState/aiPlaceIronMine.cpp new file mode 100644 index 000000000..ae163f6f2 --- /dev/null +++ b/src/OpenSHC/AI/AIVState/aiPlaceIronMine.cpp @@ -0,0 +1,38 @@ +#include "../AIVState.func.hpp" + +#include "OpenSHC/Game/GameStateStructures.func.hpp" +#include "OpenSHC/Map/TileMapState.func.hpp" + +#include "OpenSHC/Globals/DAT_BuildingsState.hpp" +#include "OpenSHC/Globals/DAT_GameState.hpp" +#include "OpenSHC/Globals/DAT_TileMapState.hpp" + +namespace OpenSHC { +namespace AI { + + // FUNCTION: STRONGHOLDCRUSADER 0x004EFE30 + void AIVState::aiPlaceIronMine(int playerID) + { + if (!MACRO_CALL_MEMBER(Game::GameStateStructures_Func::checkRequiredResourcesForBuildingOrPlanToBuy, + DAT_GameState::ptr)(Commands::M_MAPPER_IRON_MINE, playerID, FALSE) + || DAT_BuildingsState::instance.unknownCountdown01 < 20) { + return; + } + MACRO_CALL_MEMBER(AIVState_Func::findAppropriateGridLocationForResourceTypeBuilding, this)( + playerID, AIGCRT_IRON); + if (this->buildingApproriateGridXPosition == -1) { + return; + } + ++this->heatMaps[this->buildingApproriateGridXPosition][this->buildingAppropriateGridYPosition].structureCount; + MACRO_CALL_MEMBER(Map::TileMapState_Func::placeBuilding, DAT_TileMapState::ptr)(playerID, + this->buildingApproriateGridXPosition * 5, this->buildingAppropriateGridYPosition * 5, + Commands::M_MAPPER_IRON_MINE, + MACRO_CALL_MEMBER(Map::TileMapState_Func::getBuildingSizeForCommandBuildingType, DAT_TileMapState::ptr)( + Commands::M_MAPPER_IRON_MINE), + // rotation 15 is a magic value: the building ends up with rotation 0 + // unless DAT_TileMapState::skipPlacementCheck is set + 15); + } + +} +} diff --git a/src/OpenSHC/AI/AIVState/aiPlacePitchrig.cpp b/src/OpenSHC/AI/AIVState/aiPlacePitchrig.cpp new file mode 100644 index 000000000..17df2851f --- /dev/null +++ b/src/OpenSHC/AI/AIVState/aiPlacePitchrig.cpp @@ -0,0 +1,38 @@ +#include "../AIVState.func.hpp" + +#include "OpenSHC/Game/GameStateStructures.func.hpp" +#include "OpenSHC/Map/TileMapState.func.hpp" + +#include "OpenSHC/Globals/DAT_BuildingsState.hpp" +#include "OpenSHC/Globals/DAT_GameState.hpp" +#include "OpenSHC/Globals/DAT_TileMapState.hpp" + +namespace OpenSHC { +namespace AI { + + // FUNCTION: STRONGHOLDCRUSADER 0x004EFD90 + void AIVState::aiPlacePitchrig(int playerID) + { + if (!MACRO_CALL_MEMBER(Game::GameStateStructures_Func::checkRequiredResourcesForBuildingOrPlanToBuy, + DAT_GameState::ptr)(Commands::M_MAPPER_PITCH_WORKINGS, playerID, FALSE) + || DAT_BuildingsState::instance.unknownCountdown01 < 20) { + return; + } + MACRO_CALL_MEMBER(AIVState_Func::findAppropriateGridLocationForResourceTypeBuilding, this)( + playerID, AIGCRT_OIL); + if (this->buildingApproriateGridXPosition == -1) { + return; + } + ++this->heatMaps[this->buildingApproriateGridXPosition][this->buildingAppropriateGridYPosition].structureCount; + MACRO_CALL_MEMBER(Map::TileMapState_Func::placeBuilding, DAT_TileMapState::ptr)(playerID, + this->buildingApproriateGridXPosition * 5, this->buildingAppropriateGridYPosition * 5, + Commands::M_MAPPER_PITCH_WORKINGS, + MACRO_CALL_MEMBER(Map::TileMapState_Func::getBuildingSizeForCommandBuildingType, DAT_TileMapState::ptr)( + Commands::M_MAPPER_PITCH_WORKINGS), + // rotation 15 is a magic value: the building ends up with rotation 0 + // unless DAT_TileMapState::skipPlacementCheck is set + 15); + } + +} +} diff --git a/src/OpenSHC/AI/AIVState/aiPlaceQuarryAndOxtether.cpp b/src/OpenSHC/AI/AIVState/aiPlaceQuarryAndOxtether.cpp new file mode 100644 index 000000000..272733d8c --- /dev/null +++ b/src/OpenSHC/AI/AIVState/aiPlaceQuarryAndOxtether.cpp @@ -0,0 +1,77 @@ +#include "../AIVState.func.hpp" + +#include "OpenSHC/Game/GameStateStructures.func.hpp" +#include "OpenSHC/Map/TileMapState.func.hpp" + +#include "OpenSHC/Globals/DAT_AIVDefinedData.hpp" +#include "OpenSHC/Globals/DAT_BuildingsState.hpp" +#include "OpenSHC/Globals/DAT_GameState.hpp" +#include "OpenSHC/Globals/DAT_TileMapState.hpp" + +namespace OpenSHC { +namespace AI { + + // FUNCTION: STRONGHOLDCRUSADER 0x004EFED0 + void AIVState::aiPlaceQuarryAndOxtether(int playerID) + { + if (!MACRO_CALL_MEMBER(Game::GameStateStructures_Func::checkRequiredResourcesForBuildingOrPlanToBuy, + DAT_GameState::ptr)(Commands::M_MAPPER_QUARRY, playerID, FALSE) + || DAT_BuildingsState::instance.unknownCountdown01 < 20) { + return; + } + MACRO_CALL_MEMBER(AIVState_Func::findAppropriateGridLocationForResourceTypeBuilding, this)( + playerID, AIGCRT_BOULDER); + if (this->buildingApproriateGridXPosition == -1) { + return; + } + + ++this->heatMaps[this->buildingApproriateGridXPosition][this->buildingAppropriateGridYPosition].structureCount; + int const quarryXOffset + = DAT_AIVDefinedData::instance + .OffsetsForClockwiseThreeByThreeAreaCheck[this->mapExtraInfo.algBuildingPlacementNudge % 9] + .x; + int const quarryYOffset + = DAT_AIVDefinedData::instance + .OffsetsForClockwiseThreeByThreeAreaCheck[this->mapExtraInfo.algBuildingPlacementNudge % 9] + .y; + MACRO_CALL_MEMBER(Map::TileMapState_Func::placeBuilding, DAT_TileMapState::ptr)(playerID, + quarryXOffset + this->buildingApproriateGridXPosition * 5, + quarryYOffset + this->buildingAppropriateGridYPosition * 5, Commands::M_MAPPER_QUARRY, + MACRO_CALL_MEMBER(Map::TileMapState_Func::getBuildingSizeForCommandBuildingType, DAT_TileMapState::ptr)( + Commands::M_MAPPER_QUARRY), + // rotation 15 is a magic value: the building ends up with rotation 0 + // unless DAT_TileMapState::skipPlacementCheck is set + 15); + if (DAT_TileMapState::instance.buildingPlacementFail) { + return; + } + int const quarryID = DAT_TileMapState::instance.placedBuildingID; + MACRO_CALL_MEMBER(AIVState_Func::findSuitableGridLocationFittingCriteria, this)( + this->buildingApproriateGridXPosition, this->buildingAppropriateGridYPosition); + if (this->buildingApproriateGridXPosition == -1) { + return; + } + + // place an ox tether on the first free side and link it to the quarry + for (int side = 0; side < 4; ++side) { + int const xOffset = DAT_AIVDefinedData::instance.OffsetsForSpotCheckThreeByThreeAreaOnTheSide[side].x; + int const yOffset = DAT_AIVDefinedData::instance.OffsetsForSpotCheckThreeByThreeAreaOnTheSide[side].y; + MACRO_CALL_MEMBER(Map::TileMapState_Func::placeBuilding, DAT_TileMapState::ptr)(playerID, + xOffset + this->buildingApproriateGridXPosition * 5, + yOffset + this->buildingAppropriateGridYPosition * 5, Commands::M_MAPPER_OXENBASE, + MACRO_CALL_MEMBER(Map::TileMapState_Func::getBuildingSizeForCommandBuildingType, DAT_TileMapState::ptr)( + Commands::M_MAPPER_OXENBASE), + // rotation 15 is a magic value: the building ends up with rotation 0 + // unless DAT_TileMapState::skipPlacementCheck is set + 15); + if (!DAT_TileMapState::instance.buildingPlacementFail) { + int const oxTetherID = DAT_TileMapState::instance.placedBuildingID; + DAT_BuildingsState::instance.buildings[quarryID].quarryLinkedOxTethers[0] = (short)oxTetherID; + DAT_BuildingsState::instance.buildings[oxTetherID].oxtetherLinkedQuarryID = (short)quarryID; + return; + } + } + } + +} +} diff --git a/src/OpenSHC/AI/AIVState/clearTheHeatmaps.cpp b/src/OpenSHC/AI/AIVState/clearTheHeatmaps.cpp new file mode 100644 index 000000000..32dd8cff3 --- /dev/null +++ b/src/OpenSHC/AI/AIVState/clearTheHeatmaps.cpp @@ -0,0 +1,18 @@ +#include "../AIVState.func.hpp" + +#include "OpenSHC/IO/LowLevelMemory.func.hpp" + +#include "OpenSHC/Globals/DAT_LowLevelMemory.hpp" + +namespace OpenSHC { +namespace AI { + + // FUNCTION: STRONGHOLDCRUSADER 0x004EDC90 + void AIVState::clearTheHeatmaps() + { + MACRO_CALL_MEMBER(IO::LowLevelMemory_Func::fillMemory_ByteValue, DAT_LowLevelMemory::ptr)( + sizeof(this->heatMaps), 0, this->heatMaps); + } + +} +} diff --git a/src/OpenSHC/AI/AIVState/computeAIVPlacementFit.cpp b/src/OpenSHC/AI/AIVState/computeAIVPlacementFit.cpp new file mode 100644 index 000000000..54dbba337 --- /dev/null +++ b/src/OpenSHC/AI/AIVState/computeAIVPlacementFit.cpp @@ -0,0 +1,68 @@ +#include "../AIVState.func.hpp" + +#include "OpenSHC/Map/Buildings/BuildingsState.func.hpp" +#include "OpenSHC/Map/TileMapState.func.hpp" + +#include "OpenSHC/Globals/DAT_BuildingDefinedData.hpp" +#include "OpenSHC/Globals/DAT_BuildingsState.hpp" +#include "OpenSHC/Globals/DAT_TileMapState.hpp" +#include "OpenSHC/Globals/DAT_ViewportRenderState.hpp" + +namespace OpenSHC { +namespace AI { + + // returns the lowest AIV step (minus one) that can not be placed, 999999 is a perfect fit + // FUNCTION: STRONGHOLDCRUSADER 0x004EF8C0 + int AIVState::computeAIVPlacementFit(int playerID) + { + int minSteps = 1000000; + DAT_TileMapState::instance.buildingPlacementProperty_7 = 0; + DAT_TileMapState::instance.buildingPlacementProperty_6 = 0; + DAT_TileMapState::instance.buildingPlacementProperty_5 = 0; + DAT_TileMapState::instance.buildingPlacementProperty_3 = 0; + for (int y = 0, index = 0; y < 100; ++y) { + for (int x = 0; x < 100; ++x, ++index) { + int const type = this->constructions[index]; + if (type == 0) { + continue; + } + if (type == 1) { + this->isHandled[index] = 1; + continue; + } + MappersEnum const mapper = MACRO_CALL_MEMBER( + AIVState_Func::convertAIVBuildingTypeToCommandBuildingType, this)((AIV::AIVBuildingType2)type); + DAT_TileMapState::instance.buildingHeightLimit + = DAT_BuildingDefinedData::instance.BuildingPlacement_HeightLimit[MACRO_CALL_MEMBER( + Map::Buildings::BuildingsState_Func::convertCommandBuildingTypeToBuildingType, + DAT_BuildingsState::ptr)(mapper)]; + DAT_TileMapState::instance.buildingMaxHeightDifference + = DAT_BuildingDefinedData::instance.BuildingPlacement_MaxHeightDifference[MACRO_CALL_MEMBER( + Map::Buildings::BuildingsState_Func::convertCommandBuildingTypeToBuildingType, + DAT_BuildingsState::ptr)(mapper)]; + ++this->buildingCounter; + uint const realX = this->aivs[playerID].keepXOffset + x; + uint const realY = this->aivs[playerID].keepYOffset + y; + if (realX > 399 || realY > 399 + || !DAT_ViewportRenderState::instance.DAT_BinaryTileMap400x400[realY * 400 + realX]) { + continue; + } + int const tile = DAT_ViewportRenderState::instance.translationMatrix[realY].addXgetTile + realX; + MACRO_CALL_MEMBER(Map::TileMapState_Func::storeMinAndMaxHeightOfArea, DAT_TileMapState::ptr)( + realX, realY, 1); + if (MACRO_CALL_MEMBER(Map::TileMapState_Func::isBuildingPlacementAllowedAtTile, DAT_TileMapState::ptr)( + tile, 0, mapper, 0)) { + this->isHandled[index] = 1; + // steps is accessed as a flat 100 * 100 array + if (this->steps[0][index] < minSteps) { + minSteps = this->steps[0][index]; + } + ++this->invalidTileCounter; + } + } + } + return minSteps - 1; + } + +} +} diff --git a/src/OpenSHC/AI/AIVState/convertAIVBuildingTypeToCommandBuildingType.cpp b/src/OpenSHC/AI/AIVState/convertAIVBuildingTypeToCommandBuildingType.cpp new file mode 100644 index 000000000..5daa3b42d --- /dev/null +++ b/src/OpenSHC/AI/AIVState/convertAIVBuildingTypeToCommandBuildingType.cpp @@ -0,0 +1,48 @@ +#include "../AIVState.func.hpp" + +#include "OpenSHC/Globals/DAT_AIVDefinedData.hpp" + +namespace OpenSHC { +namespace AI { + + // FUNCTION: STRONGHOLDCRUSADER 0x004ECFE0 + MappersEnum AIVState::convertAIVBuildingTypeToCommandBuildingType(AIVBuildingType2 aivBuildingType) + { + switch (aivBuildingType) { + case AIV::AIVBT_HIGH_WALL: + return Commands::M_MAPPER_WALL; + case AIV::AIVBT_LOW_WALL: + return Commands::M_MAPPER_WOODWALL; + case AIV::AIVBT_CRENEL: + return Commands::M_MAPPER_CRENAL; + case AIV::AIVBT_LOW_CRENEL: + return Commands::M_MAPPER_CRENAL2; + case AIV::AIVBT_STAIRS_1: + return Commands::M_MAPPER_STAIR1; + case AIV::AIVBT_STAIRS_2: + return Commands::M_MAPPER_STAIR2; + case AIV::AIVBT_STAIRS_3: + return Commands::M_MAPPER_STAIR3; + case AIV::AIVBT_STAIRS_4: + return Commands::M_MAPPER_STAIR4; + case AIV::AIVBT_STAIRS_5: + return Commands::M_MAPPER_STAIR5; + case AIV::AIVBT_STAIRS_6Unk: + return Commands::M_MAPPER_STAIR6; + case AIV::AIVBT_MOAT_A: + case AIV::AIVBT_MOAT_B: + case AIV::AIVBT_MOAT_C: + case AIV::AIVBT_MOAT_D: + return Commands::M_MAPPER_MOAT; + case AIV::AIVBT_PITCHDITCH_2: + return Commands::M_MAPPER_PITCH_DITCH; + } + // all other buildings are looked up in a table starting at the first tower + if (aivBuildingType < AIV::AIVBT_TOWER1 || aivBuildingType > AIV::AIVBT_TOWER1 + 79) { + return Commands::M_MAPPER_GENERIC; + } + return DAT_AIVDefinedData::instance.field21_0xf4[aivBuildingType - AIV::AIVBT_TOWER1]; + } + +} +} diff --git a/src/OpenSHC/AI/AIVState/findAppropriateGridLocationForResourceTypeBuilding.cpp b/src/OpenSHC/AI/AIVState/findAppropriateGridLocationForResourceTypeBuilding.cpp new file mode 100644 index 000000000..88bbd689a --- /dev/null +++ b/src/OpenSHC/AI/AIVState/findAppropriateGridLocationForResourceTypeBuilding.cpp @@ -0,0 +1,147 @@ +#include "../AIVState.func.hpp" + +#include "OpenSHC/Globals/DAT_GameState.hpp" +#include "OpenSHC/Globals/DAT_TerrainDefinedData.hpp" + +namespace OpenSHC { +namespace AI { + + // breadth first search from the campground for a grid cell with enough of the given resource + // FUNCTION: STRONGHOLDCRUSADER 0x004EE430 + void AIVState::findAppropriateGridLocationForResourceTypeBuilding( + int playerID, AIGridComputationResourceType resourceType) + { + this->visitCount = 1; + this->algAIndex = 0; + this->algBIndex = 1; + this->buildingApproriateGridXPosition = -1; + this->buildingAppropriateGridYPosition = -1; + if (resourceType == AIGCRT_OIL) { + if (DAT_GameState::instance.playerDataArray[playerID].countPitchRigs + >= DAT_GameState::instance.playerDataArray[playerID].availableOilGridTiles) { + return; + } + if (DAT_GameState::instance.playerDataArray[playerID].algoOilCooldown > 0) { + --DAT_GameState::instance.playerDataArray[playerID].algoOilCooldown; + return; + } + } else if (resourceType == AIGCRT_IRON) { + if (DAT_GameState::instance.playerDataArray[playerID].countIronMines + >= DAT_GameState::instance.playerDataArray[playerID].availableIronGridTiles) { + return; + } + if (DAT_GameState::instance.playerDataArray[playerID].algoIronCooldown > 0) { + --DAT_GameState::instance.playerDataArray[playerID].algoIronCooldown; + return; + } + } else if (resourceType == AIGCRT_BOULDER) { + if (DAT_GameState::instance.playerDataArray[playerID].countStoneQuarries + >= DAT_GameState::instance.playerDataArray[playerID].availableBoulderGridTiles) { + return; + } + if (DAT_GameState::instance.playerDataArray[playerID].algoBoulderCooldown > 0) { + --DAT_GameState::instance.playerDataArray[playerID].algoBoulderCooldown; + return; + } + } + + int const campX = DAT_GameState::instance.playerDataArray[playerID].campground.xEntry / 5; + this->heatMapXArray[0] = campX; + int const campY = DAT_GameState::instance.playerDataArray[playerID].campground.yEntry / 5; + ++this->mapExtraInfo.algorithmIterationNumber; + this->heatMapYArray[0] = campY; + this->heatMaps[campX][campY].algorithmIterationNumber = this->mapExtraInfo.algorithmIterationNumber; + this->heatMaps[campX][campY].algorithmVisitCountUnk = this->visitCount; + + while (this->algAIndex != this->algBIndex) { + int const x = this->heatMapXArray[this->algAIndex]; + int const y = this->heatMapYArray[this->algAIndex]; + this->visitCount = this->heatMaps[x][y].algorithmVisitCountUnk; + if (this->visitCount > 40) { + break; + } + // only the four cardinal directions + for (int direction = 0; direction < 8; direction += 2) { + uint const gridX + = DAT_TerrainDefinedData::instance.clockwiseCardinalTranslationMatrix[direction].int_.xOffset + x; + uint const gridY + = DAT_TerrainDefinedData::instance.clockwiseCardinalTranslationMatrix[direction].int_.yOffset + y; + if (this->heatMaps[x][y].isNotNearEdgeUnk && (gridX > 79 || gridY > 79)) { + continue; + } + if (this->heatMaps[gridX][gridY].algorithmIterationNumber + == this->mapExtraInfo.algorithmIterationNumber) { + continue; + } + this->heatMaps[gridX][gridY].algorithmIterationNumber = this->mapExtraInfo.algorithmIterationNumber; + int const blockedCount = this->heatMaps[gridX][gridY].tilesNotPartOfLargestAreaCount + - this->heatMaps[gridX][gridY].impassableCount; + if (blockedCount >= 16) { + continue; + } + int const owner = this->heatMaps[gridX][gridY].tileOwnership; + if ((blockedCount == 0 || resourceType == AIGCRT_IRON) && blockedCount <= 4 + && !this->heatMaps[gridX][gridY].structureCount + && !this->heatMaps[gridX][gridY].destructionBasedPlacementCooldown + && (!this->heatMaps[gridX][gridY].tileOwnership + || DAT_GameState::instance.mapAndTime.playerTeams[playerID] + == DAT_GameState::instance.mapAndTime.playerTeams[owner])) { + if (resourceType == AIGCRT_OIL) { + if (this->heatMaps[gridX][gridY].oilCount > 2 && this->heatMaps[gridX][gridY].marshCount >= 10 + && this->heatMaps[gridX][gridY].highestTerrainHeight + - this->heatMaps[gridX][gridY].lowestTerrainHeight + < 12) { + this->buildingApproriateGridXPosition = gridX; + this->buildingAppropriateGridYPosition = gridY; + return; + } + } else if (resourceType == AIGCRT_IRON) { + if (this->heatMaps[gridX][gridY].ironCount > 6 + && this->heatMaps[gridX][gridY].highestTerrainHeight + - this->heatMaps[gridX][gridY].lowestTerrainHeight + < 12) { + this->buildingApproriateGridXPosition = gridX; + this->buildingAppropriateGridYPosition = gridY; + return; + } + } else if (resourceType == AIGCRT_BOULDER) { + if (this->heatMaps[gridX][gridY].bouldersCount >= 8 + && this->heatMaps[gridX][gridY].highestTerrainHeight + - this->heatMaps[gridX][gridY].lowestTerrainHeight + < 40) { + this->buildingApproriateGridXPosition = gridX; + this->buildingAppropriateGridYPosition = gridY; + return; + } + } + } + this->heatMaps[gridX][gridY].algorithmVisitCountUnk = this->visitCount + 1; + this->heatMapXArray[this->algBIndex] = gridX; + this->heatMapYArray[this->algBIndex] = gridY; + ++this->algBIndex; + if (this->algBIndex >= 6400) { + this->algBIndex = 0; + } + } + ++this->algAIndex; + if (this->algAIndex >= 6400) { + this->algAIndex = 0; + } + } + + // nothing found, wait before searching again + if (resourceType == AIGCRT_OIL) { + DAT_GameState::instance.playerDataArray[playerID].algoOilCooldown = 5; + return; + } + if (resourceType == AIGCRT_IRON) { + DAT_GameState::instance.playerDataArray[playerID].algoIronCooldown = 5; + return; + } + if (resourceType == AIGCRT_BOULDER) { + DAT_GameState::instance.playerDataArray[playerID].algoBoulderCooldown = 5; + } + } + +} +} diff --git a/src/OpenSHC/AI/AIVState/findAppropriateWoodCutterGridLocation.cpp b/src/OpenSHC/AI/AIVState/findAppropriateWoodCutterGridLocation.cpp new file mode 100644 index 000000000..db49859c3 --- /dev/null +++ b/src/OpenSHC/AI/AIVState/findAppropriateWoodCutterGridLocation.cpp @@ -0,0 +1,102 @@ +#include "../AIVState.func.hpp" + +#include "OpenSHC/Globals/DAT_GameState.hpp" +#include "OpenSHC/Globals/DAT_TerrainDefinedData.hpp" + +namespace OpenSHC { +namespace AI { + + // breadth first search from the campground for the grid cell with the best tree score + // FUNCTION: STRONGHOLDCRUSADER 0x004EE140 + void AIVState::findAppropriateWoodCutterGridLocation(int playerID) + { + int bestScore = -100; + uint bestX = -1; + uint bestY = -1; + this->algAIndex = 0; + int foundCount = 0; + this->visitCount = 1; + this->algBIndex = 1; + int const campX = DAT_GameState::instance.playerDataArray[playerID].campground.xEntry / 5; + this->heatMapXArray[0] = campX; + int const campY = DAT_GameState::instance.playerDataArray[playerID].campground.yEntry / 5; + this->heatMapYArray[0] = campY; + this->buildingApproriateGridXPosition = -1; + this->buildingAppropriateGridYPosition = -1; + if (DAT_GameState::instance.playerDataArray[playerID].algoTreeCooldownUnk > 0) { + --DAT_GameState::instance.playerDataArray[playerID].algoTreeCooldownUnk; + return; + } + ++this->mapExtraInfo.algorithmIterationNumber; + this->heatMaps[campX][campY].algorithmIterationNumber = this->mapExtraInfo.algorithmIterationNumber; + this->heatMaps[campX][campY].algorithmVisitCountUnk = this->visitCount; + + while (this->algAIndex != this->algBIndex) { + int const x = this->heatMapXArray[this->algAIndex]; + int const y = this->heatMapYArray[this->algAIndex]; + this->visitCount = this->heatMaps[x][y].algorithmVisitCountUnk; + if (this->visitCount > 40) { + break; + } + // only the four cardinal directions + for (int direction = 0; direction < 8; direction += 2) { + uint const gridX + = DAT_TerrainDefinedData::instance.clockwiseCardinalTranslationMatrix[direction].int_.xOffset + x; + uint const gridY + = DAT_TerrainDefinedData::instance.clockwiseCardinalTranslationMatrix[direction].int_.yOffset + y; + if (this->heatMaps[x][y].isNotNearEdgeUnk && (gridX > 79 || gridY > 79)) { + continue; + } + if (this->heatMaps[gridX][gridY].algorithmIterationNumber + == this->mapExtraInfo.algorithmIterationNumber) { + continue; + } + this->heatMaps[gridX][gridY].algorithmIterationNumber = this->mapExtraInfo.algorithmIterationNumber; + char const notInLargestArea = this->heatMaps[gridX][gridY].tilesNotPartOfLargestAreaCount; + if (notInLargestArea >= 16 || this->heatMaps[gridX][gridY].destructionBasedPlacementCooldown) { + continue; + } + if (notInLargestArea < 6 && this->heatMaps[gridX][gridY].treeCount > 0) { + ++foundCount; + int score = this->heatMaps[gridX][gridY].treeCount * 5 - this->visitCount * 3; + // prefer cells that were not used for a woodcutter recently + if (this->heatMaps[gridX][gridY].woodRelatedCountdownTo0) { + if (score > 0) { + score /= 2; + } else { + score *= 2; + } + } + if (score > bestScore) { + bestX = gridX; + bestY = gridY; + bestScore = score; + } + if (foundCount > 10 || (foundCount > 5 && this->visitCount > 20) + || (foundCount > 0 && this->visitCount > 30)) { + break; + } + } + this->heatMaps[gridX][gridY].algorithmVisitCountUnk = this->visitCount + 1; + this->heatMapXArray[this->algBIndex] = gridX; + this->heatMapYArray[this->algBIndex] = gridY; + ++this->algBIndex; + if (this->algBIndex >= 6400) { + this->algBIndex = 0; + } + } + ++this->algAIndex; + if (this->algAIndex >= 6400) { + this->algAIndex = 0; + } + } + if (foundCount > 0) { + this->buildingApproriateGridXPosition = bestX; + this->buildingAppropriateGridYPosition = bestY; + return; + } + DAT_GameState::instance.playerDataArray[playerID].algoTreeCooldownUnk = 5; + } + +} +} diff --git a/src/OpenSHC/AI/AIVState/findSpotNearEngineersGuild.cpp b/src/OpenSHC/AI/AIVState/findSpotNearEngineersGuild.cpp new file mode 100644 index 000000000..e1034d413 --- /dev/null +++ b/src/OpenSHC/AI/AIVState/findSpotNearEngineersGuild.cpp @@ -0,0 +1,30 @@ +#include "../AIVState.func.hpp" + +#include "OpenSHC/Globals/DAT_BuildingsState.hpp" +#include "OpenSHC/Globals/DAT_GameState.hpp" + +namespace OpenSHC { +namespace AI { + + // FUNCTION: STRONGHOLDCRUSADER 0x004EFB90 + BOOLEnum AIVState::findSpotNearEngineersGuild(int playerID) + { + int const guildID = DAT_GameState::instance.playerDataArray[playerID].engineersGuild.id; + if (guildID <= 0) { + return FALSE; + } + if (DAT_BuildingsState::instance.buildings[guildID].buildingType != Map::Buildings::BT_ENGINEERSGUILD) { + return FALSE; + } + MACRO_CALL_MEMBER(AIVState_Func::findSuitableGridLocationFittingCriteria, this)( + DAT_BuildingsState::instance.buildings[guildID].x / 5, + DAT_BuildingsState::instance.buildings[guildID].y / 5); + if (this->buildingApproriateGridXPosition == -1) { + return FALSE; + } + ++this->heatMaps[this->buildingApproriateGridXPosition][this->buildingAppropriateGridYPosition].structureCount; + return TRUE; + } + +} +} diff --git a/src/OpenSHC/AI/AIVState/findSuitableFarmLocationAndPlaceFarm.cpp b/src/OpenSHC/AI/AIVState/findSuitableFarmLocationAndPlaceFarm.cpp new file mode 100644 index 000000000..1614622f3 --- /dev/null +++ b/src/OpenSHC/AI/AIVState/findSuitableFarmLocationAndPlaceFarm.cpp @@ -0,0 +1,119 @@ +#include "../AIVState.func.hpp" + +#include "OpenSHC/Map/TileMapState.func.hpp" + +#include "OpenSHC/Globals/DAT_AIVDefinedData.hpp" +#include "OpenSHC/Globals/DAT_BuildingsState.hpp" +#include "OpenSHC/Globals/DAT_GameState.hpp" +#include "OpenSHC/Globals/DAT_TerrainDefinedData.hpp" +#include "OpenSHC/Globals/DAT_TileMapState.hpp" + +namespace OpenSHC { +namespace AI { + + // breadth first search from the campground for an oasis grid cell and places the farm there + // FUNCTION: STRONGHOLDCRUSADER 0x004EDDF0 + BOOLEnum AIVState::findSuitableFarmLocationAndPlaceFarm(int playerID, MappersEnum farmType) + { + if (DAT_BuildingsState::instance.unknownCountdown01 < 20 + || DAT_GameState::instance.playerDataArray[playerID].countFarms + >= DAT_GameState::instance.playerDataArray[playerID].availableOasisGridTiles) { + return FALSE; + } + if (DAT_GameState::instance.playerDataArray[playerID].algoOasisCooldownUnk > 0) { + --DAT_GameState::instance.playerDataArray[playerID].algoOasisCooldownUnk; + return FALSE; + } + + this->visitCount = 1; + this->algAIndex = 0; + this->algBIndex = 1; + // despite its declared type, farmType holds the BuildingType of the farm + // the original leaves mapper untouched when farmType is none of the four below + MappersEnum mapper; + if (farmType == Map::Buildings::BT_WHEATFARM) { + mapper = Commands::M_MAPPER_WHEATFARM; + } else if (farmType == Map::Buildings::BT_HOPFARM) { + mapper = Commands::M_MAPPER_HOPSFARM; + } else if (farmType == Map::Buildings::BT_APPLEFARM) { + mapper = Commands::M_MAPPER_APPLEFARM; + } else if (farmType == Map::Buildings::BT_DAIRYFARM) { + mapper = Commands::M_MAPPER_CATTLEFARM; + } + int const campX = DAT_GameState::instance.playerDataArray[playerID].campground.xEntry / 5; + this->heatMapXArray[0] = campX; + int const campY = DAT_GameState::instance.playerDataArray[playerID].campground.yEntry / 5; + ++this->mapExtraInfo.algorithmIterationNumber; + this->heatMapYArray[0] = campY; + this->heatMaps[campX][campY].algorithmIterationNumber = this->mapExtraInfo.algorithmIterationNumber; + this->heatMaps[campX][campY].algorithmVisitCountUnk = this->visitCount; + + while (this->algAIndex != this->algBIndex) { + int const x = this->heatMapXArray[this->algAIndex]; + int const y = this->heatMapYArray[this->algAIndex]; + this->visitCount = this->heatMaps[x][y].algorithmVisitCountUnk; + if (this->visitCount > 40) { + break; + } + // only the four cardinal directions + for (int direction = 0; direction < 8; direction += 2) { + uint const gridX + = DAT_TerrainDefinedData::instance.clockwiseCardinalTranslationMatrix[direction].int_.xOffset + x; + uint const gridY + = DAT_TerrainDefinedData::instance.clockwiseCardinalTranslationMatrix[direction].int_.yOffset + y; + if (this->heatMaps[x][y].isNotNearEdgeUnk && (gridX > 79 || gridY > 79)) { + continue; + } + if (this->heatMaps[gridX][gridY].algorithmIterationNumber + == this->mapExtraInfo.algorithmIterationNumber) { + continue; + } + int const notInLargestArea = this->heatMaps[gridX][gridY].tilesNotPartOfLargestAreaCount; + this->heatMaps[gridX][gridY].algorithmIterationNumber = this->mapExtraInfo.algorithmIterationNumber; + if (notInLargestArea > 16) { + continue; + } + this->heatMaps[gridX][gridY].algorithmVisitCountUnk = this->visitCount + 1; + this->heatMapXArray[this->algBIndex] = gridX; + this->heatMapYArray[this->algBIndex] = gridY; + ++this->algBIndex; + if (this->algBIndex >= 6400) { + this->algBIndex = 0; + } + if (this->heatMaps[gridX][gridY].tilesNotPartOfLargestAreaCount + || this->heatMaps[gridX][gridY].structureCount || this->heatMaps[gridX][gridY].treeCount + || this->heatMaps[gridX][gridY].destructionBasedPlacementCooldown + || this->heatMaps[gridX][gridY].oasisScrubCount < 25 + || this->heatMaps[gridX][gridY].oasisThickScrubCount < 14) { + continue; + } + int const xOffset + = DAT_AIVDefinedData::instance.TilesSelfAndRightThree[this->mapExtraInfo.algBuildingPlacementNudge] + .x; + int const yOffset + = DAT_AIVDefinedData::instance.TilesSelfAndRightThree[this->mapExtraInfo.algBuildingPlacementNudge] + .y; + ++this->counter; + MACRO_CALL_MEMBER(Map::TileMapState_Func::placeBuilding, DAT_TileMapState::ptr)(playerID, + xOffset + gridX * 5, yOffset + gridY * 5, mapper, + MACRO_CALL_MEMBER( + Map::TileMapState_Func::getBuildingSizeForCommandBuildingType, DAT_TileMapState::ptr)(mapper), + // rotation 15 is a magic value: the building ends up with rotation 0 + // unless DAT_TileMapState::skipPlacementCheck is set + 15); + if (!DAT_TileMapState::instance.buildingPlacementFail) { + this->heatMaps[gridX][gridY].structureCount = 1; + return TRUE; + } + } + ++this->algAIndex; + if (this->algAIndex >= 6400) { + this->algAIndex = 0; + } + } + DAT_GameState::instance.playerDataArray[playerID].algoOasisCooldownUnk = 5; + return FALSE; + } + +} +} diff --git a/src/OpenSHC/AI/AIVState/findSuitableGridLocationFittingCriteria.cpp b/src/OpenSHC/AI/AIVState/findSuitableGridLocationFittingCriteria.cpp new file mode 100644 index 000000000..9dc093115 --- /dev/null +++ b/src/OpenSHC/AI/AIVState/findSuitableGridLocationFittingCriteria.cpp @@ -0,0 +1,74 @@ +#include "../AIVState.func.hpp" + +#include "OpenSHC/Globals/DAT_TerrainDefinedData.hpp" + +namespace OpenSHC { +namespace AI { + + // breadth first search for the closest free grid cell + // FUNCTION: STRONGHOLDCRUSADER 0x004EEBD0 + void AIVState::findSuitableGridLocationFittingCriteria(uint gridX, uint gridY) + { + this->visitCount = 1; + this->algAIndex = 0; + this->algBIndex = 1; + this->heatMapXArray[0] = gridX; + this->heatMapYArray[0] = gridY; + this->buildingApproriateGridXPosition = -1; + this->buildingAppropriateGridYPosition = -1; + if (gridX > 79 || gridY > 79 || this->heatMaps[gridX][gridY].isNotNearEdgeUnk) { + return; + } + ++this->mapExtraInfo.algorithmIterationNumber; + this->heatMaps[gridX][gridY].algorithmIterationNumber = this->mapExtraInfo.algorithmIterationNumber; + this->heatMaps[gridX][gridY].algorithmVisitCountUnk = this->visitCount; + + while (this->algAIndex != this->algBIndex) { + int const x = this->heatMapXArray[this->algAIndex]; + int const y = this->heatMapYArray[this->algAIndex]; + this->visitCount = this->heatMaps[x][y].algorithmVisitCountUnk; + for (int direction = 0; direction < 8; ++direction) { + uint const candidateX + = DAT_TerrainDefinedData::instance.clockwiseCardinalTranslationMatrix[direction].int_.xOffset + x; + uint const candidateY + = DAT_TerrainDefinedData::instance.clockwiseCardinalTranslationMatrix[direction].int_.yOffset + y; + if (this->heatMaps[x][y].isNotNearEdgeUnk && (candidateX > 79 || candidateY > 79)) { + continue; + } + int const notInLargestArea = this->heatMaps[candidateX][candidateY].tilesNotPartOfLargestAreaCount; + if (this->heatMaps[candidateX][candidateY].algorithmIterationNumber + == this->mapExtraInfo.algorithmIterationNumber) { + continue; + } + this->heatMaps[candidateX][candidateY].algorithmIterationNumber + = this->mapExtraInfo.algorithmIterationNumber; + if (notInLargestArea >= 15) { + continue; + } + if (!this->heatMaps[candidateX][candidateY].terrainDiffHigherThan12 + && !this->heatMaps[candidateX][candidateY].structureCount + && !this->heatMaps[candidateX][candidateY].isNotNearEdgeUnk + && !this->heatMaps[candidateX][candidateY].destructionBasedPlacementCooldown + && !this->heatMaps[candidateX][candidateY].treeCount + && this->heatMaps[candidateX][candidateY].bouldersCount <= 0 && !notInLargestArea) { + this->buildingApproriateGridXPosition = candidateX; + this->buildingAppropriateGridYPosition = candidateY; + return; + } + this->heatMaps[candidateX][candidateY].algorithmVisitCountUnk = this->visitCount + 1; + this->heatMapXArray[this->algBIndex] = candidateX; + this->heatMapYArray[this->algBIndex] = candidateY; + ++this->algBIndex; + if (this->algBIndex >= 6400) { + this->algBIndex = 0; + } + } + ++this->algAIndex; + if (this->algAIndex >= 6400) { + this->algAIndex = 0; + } + } + } + +} +} diff --git a/src/OpenSHC/AI/AIVState/incrementStructureHeatMapTile.cpp b/src/OpenSHC/AI/AIVState/incrementStructureHeatMapTile.cpp new file mode 100644 index 000000000..ab597b462 --- /dev/null +++ b/src/OpenSHC/AI/AIVState/incrementStructureHeatMapTile.cpp @@ -0,0 +1,19 @@ +#include "../AIVState.func.hpp" + +#include "OpenSHC/Globals/DAT_GameSynchronyState.hpp" + +namespace OpenSHC { +namespace AI { + + // records a structure in the 5x5 tile heat map cell of the given tile coordinates + // FUNCTION: STRONGHOLDCRUSADER 0x004ED3B0 + void AIVState::incrementStructureHeatMapTile(int x, int y) + { + if (DAT_GameSynchronyState::instance.currentGameMode == Game::GM_SOLITARY) { + return; + } + ++this->heatMaps[x / 5][y / 5].structureCount; + } + +} +} diff --git a/src/OpenSHC/AI/AIVState/recomputeAIAvailableGridTiles.cpp b/src/OpenSHC/AI/AIVState/recomputeAIAvailableGridTiles.cpp new file mode 100644 index 000000000..7171bd255 --- /dev/null +++ b/src/OpenSHC/AI/AIVState/recomputeAIAvailableGridTiles.cpp @@ -0,0 +1,125 @@ +#include "../AIVState.func.hpp" + +#include "OpenSHC/Globals/DAT_GameState.hpp" +#include "OpenSHC/Globals/DAT_TerrainDefinedData.hpp" + +namespace OpenSHC { +namespace AI { + + // counts the grid cells around the campground that are usable for the resource buildings + // FUNCTION: STRONGHOLDCRUSADER 0x004EE830 + void AIVState::recomputeAIAvailableGridTiles(int playerID) + { + DAT_GameState::instance.playerDataArray[playerID].algoTreeCooldownUnk = -1; + DAT_GameState::instance.playerDataArray[playerID].algoOasisCooldownUnk = -1; + DAT_GameState::instance.playerDataArray[playerID].algoBoulderCooldown = -1; + DAT_GameState::instance.playerDataArray[playerID].algoIronCooldown = -1; + DAT_GameState::instance.playerDataArray[playerID].algoOilCooldown = -1; + DAT_GameState::instance.playerDataArray[playerID].availableTreeGridTiles = 0; + DAT_GameState::instance.playerDataArray[playerID].availableOasisGridTiles = 0; + DAT_GameState::instance.playerDataArray[playerID].availableBoulderGridTiles = 0; + DAT_GameState::instance.playerDataArray[playerID].availableIronGridTiles = 0; + DAT_GameState::instance.playerDataArray[playerID].availableOilGridTiles = 0; + this->visitCount = 1; + this->algBIndex = 1; + this->algAIndex = 0; + int const campX = DAT_GameState::instance.playerDataArray[playerID].campground.xEntry / 5; + this->heatMapXArray[0] = campX; + int const campY = DAT_GameState::instance.playerDataArray[playerID].campground.yEntry / 5; + ++this->mapExtraInfo.algorithmIterationNumber; + this->heatMapYArray[0] = campY; + this->heatMaps[campX][campY].algorithmIterationNumber = this->mapExtraInfo.algorithmIterationNumber; + this->heatMaps[campX][campY].algorithmVisitCountUnk = this->visitCount; + + while (this->algAIndex != this->algBIndex) { + int const x = this->heatMapXArray[this->algAIndex]; + int const y = this->heatMapYArray[this->algAIndex]; + this->visitCount = this->heatMaps[x][y].algorithmVisitCountUnk; + if (this->visitCount > 40) { + break; + } + // only the four cardinal directions + for (int direction = 0; direction < 8; direction += 2) { + uint const gridX + = DAT_TerrainDefinedData::instance.clockwiseCardinalTranslationMatrix[direction].int_.xOffset + x; + uint const gridY + = DAT_TerrainDefinedData::instance.clockwiseCardinalTranslationMatrix[direction].int_.yOffset + y; + if (this->heatMaps[x][y].isNotNearEdgeUnk && (gridX > 79 || gridY > 79)) { + continue; + } + if (this->heatMaps[gridX][gridY].algorithmIterationNumber + == this->mapExtraInfo.algorithmIterationNumber) { + continue; + } + int const impassable = this->heatMaps[gridX][gridY].impassableCount; + this->heatMaps[gridX][gridY].algorithmIterationNumber = this->mapExtraInfo.algorithmIterationNumber; + int const blockedCount = this->heatMaps[gridX][gridY].tilesNotPartOfLargestAreaCount - impassable; + if (blockedCount >= 16) { + continue; + } + if (this->heatMaps[gridX][gridY].oilCount > 2 && this->heatMaps[gridX][gridY].marshCount >= 10 + && blockedCount == 0) { + if (this->heatMaps[gridX][gridY].highestTerrainHeight + - this->heatMaps[gridX][gridY].lowestTerrainHeight + < 12) { + ++DAT_GameState::instance.playerDataArray[playerID].availableOilGridTiles; + } + } else if (this->heatMaps[gridX][gridY].ironCount > 6 && blockedCount <= 4) { + if (this->heatMaps[gridX][gridY].highestTerrainHeight + - this->heatMaps[gridX][gridY].lowestTerrainHeight + < 12) { + ++DAT_GameState::instance.playerDataArray[playerID].availableIronGridTiles; + } + } else if (this->heatMaps[gridX][gridY].bouldersCount >= 8 && blockedCount == 0) { + if (this->heatMaps[gridX][gridY].highestTerrainHeight + - this->heatMaps[gridX][gridY].lowestTerrainHeight + < 40) { + ++DAT_GameState::instance.playerDataArray[playerID].availableBoulderGridTiles; + } + } else if (this->heatMaps[gridX][gridY].treeCount > 0 && blockedCount < 6) { + if (this->heatMaps[gridX][gridY].highestTerrainHeight + - this->heatMaps[gridX][gridY].lowestTerrainHeight + < 12) { + ++DAT_GameState::instance.playerDataArray[playerID].availableTreeGridTiles; + } + } else if (this->heatMaps[gridX][gridY].oasisScrubCount >= 25 + && this->heatMaps[gridX][gridY].oasisThickScrubCount >= 14 && blockedCount == 0 + && this->heatMaps[gridX][gridY].highestTerrainHeight + - this->heatMaps[gridX][gridY].lowestTerrainHeight + < 12) { + ++DAT_GameState::instance.playerDataArray[playerID].availableOasisGridTiles; + } + this->heatMaps[gridX][gridY].algorithmVisitCountUnk = this->visitCount + 1; + this->heatMapXArray[this->algBIndex] = gridX; + this->heatMapYArray[this->algBIndex] = gridY; + ++this->algBIndex; + if (this->algBIndex >= 6400) { + this->algBIndex = 0; + } + } + ++this->algAIndex; + if (this->algAIndex >= 6400) { + this->algAIndex = 0; + } + } + + // allow searching right away if there is anything to find + if (DAT_GameState::instance.playerDataArray[playerID].availableTreeGridTiles > 0) { + DAT_GameState::instance.playerDataArray[playerID].algoTreeCooldownUnk = 0; + } + if (DAT_GameState::instance.playerDataArray[playerID].availableOasisGridTiles > 0) { + DAT_GameState::instance.playerDataArray[playerID].algoOasisCooldownUnk = 0; + } + if (DAT_GameState::instance.playerDataArray[playerID].availableBoulderGridTiles > 0) { + DAT_GameState::instance.playerDataArray[playerID].algoBoulderCooldown = 0; + } + if (DAT_GameState::instance.playerDataArray[playerID].availableIronGridTiles > 0) { + DAT_GameState::instance.playerDataArray[playerID].algoIronCooldown = 0; + } + if (DAT_GameState::instance.playerDataArray[playerID].availableOilGridTiles > 0) { + DAT_GameState::instance.playerDataArray[playerID].algoOilCooldown = 0; + } + } + +} +} diff --git a/src/OpenSHC/AI/AIVState/resetCountdownInHeatMap.cpp b/src/OpenSHC/AI/AIVState/resetCountdownInHeatMap.cpp new file mode 100644 index 000000000..69f26e8a1 --- /dev/null +++ b/src/OpenSHC/AI/AIVState/resetCountdownInHeatMap.cpp @@ -0,0 +1,14 @@ +#include "../AIVState.func.hpp" + +namespace OpenSHC { +namespace AI { + + // blocks building on the 5x5 heat map cell of the given tile for a while, e.g. after a building got destroyed + // FUNCTION: STRONGHOLDCRUSADER 0x004EDCB0 + void AIVState::resetCountdownInHeatMap(int x, int y) + { + this->heatMaps[x / 5][y / 5].destructionBasedPlacementCooldown = 10; + } + +} +} diff --git a/src/OpenSHC/AI/AIVState/selectBestAIVwithRandomStart.cpp b/src/OpenSHC/AI/AIVState/selectBestAIVwithRandomStart.cpp new file mode 100644 index 000000000..486724816 --- /dev/null +++ b/src/OpenSHC/AI/AIVState/selectBestAIVwithRandomStart.cpp @@ -0,0 +1,62 @@ +#include "../AIVState.func.hpp" + +#include "OpenSHC/IO/FilePackager.func.hpp" +#include "OpenSHC/IO/LowLevelMemory.func.hpp" +#include "OpenSHC/Random/RNG.func.hpp" + +#include "OpenSHC/Globals/DAT_AIVDefinedData.hpp" +#include "OpenSHC/Globals/DAT_LowLevelMemory.hpp" +#include "OpenSHC/Globals/FilePackagerObj.hpp" +#include "OpenSHC/Globals/SEC_RNG.hpp" + +namespace OpenSHC { +namespace AI { + + // starts at a random castle and picks the first one that fits perfectly, otherwise the best fitting one + // FUNCTION: STRONGHOLDCRUSADER 0x004F13F0 + void AIVState::selectBestAIVwithRandomStart(int aivID) + { + int bestFit = 0; + int bestCastleID = -1; + uint castleID = (byte)SEC_RNG::instance.currentNumber2 & 7; + MACRO_CALL_MEMBER(Random::RNG_Func::nextRandomNumber2, SEC_RNG::ptr)(); + MACRO_CALL_MEMBER(IO::LowLevelMemory_Func::fillMemory_ByteValue, DAT_LowLevelMemory::ptr)( + sizeof(this->isHandled), 0, this->isHandled); + for (int tries = 0; tries < 8; ++tries) { + ++castleID; + if ((int)castleID >= 8) { + castleID = 0; + } + int const aiType = this->aivs[aivID].aiType; + if (!this->aivFilePresent[aiType - 2][castleID]) { + continue; + } + if (!MACRO_CALL_MEMBER(IO::FilePackager_Func::unpackAIVFile, FilePackagerObj::ptr)( + DAT_AIVDefinedData::instance.AIVSectionAddressArray, + DAT_AIVDefinedData::instance.AIVFileNames[aiType - 2][castleID])) { + continue; + } + MACRO_CALL_MEMBER(AIVState_Func::rotateAIV, this)(this->aivs[aivID].keepOrientation); + int const fit = MACRO_CALL_MEMBER(AIVState_Func::computeAIVPlacementFit, this)(aivID); + if (fit <= 0) { + continue; + } + if (fit == 999999) { + // the first perfectly fitting castle is taken + this->aivs[aivID].castleID = castleID; + this->aivs[aivID].aivSubType = 2; + return; + } + if (fit > bestFit) { + bestFit = fit; + bestCastleID = castleID; + } + } + if (bestCastleID >= 0) { + this->aivs[aivID].aivSubType = 1; + this->aivs[aivID].castleID = bestCastleID; + } + } + +} +} diff --git a/src/OpenSHC/AI/AIVState/setAIVFilePresenceByFileHashArray.cpp b/src/OpenSHC/AI/AIVState/setAIVFilePresenceByFileHashArray.cpp new file mode 100644 index 000000000..1d9b6899a --- /dev/null +++ b/src/OpenSHC/AI/AIVState/setAIVFilePresenceByFileHashArray.cpp @@ -0,0 +1,47 @@ +#include "../AIVState.func.hpp" + +#include "OpenSHC/Globals/DAT_GameSynchronyState.hpp" + +namespace OpenSHC { +namespace AI { + + // an AIV is only usable in multiplayer if all players have the same file + // FUNCTION: STRONGHOLDCRUSADER 0x004ECDB0 + void AIVState::setAIVFilePresenceByFileHashArray() + { + if (DAT_GameSynchronyState::instance.currentGameMode == Game::GM_SKIRMISH_SINGLE_PLAYER) { + for (int aiIndex = 0; aiIndex < 16; ++aiIndex) { + for (int castleID = 0; castleID < 8; ++castleID) { + this->aivFilePresent[aiIndex][castleID] = 1; + } + } + return; + } + for (int aiIndex = 0; aiIndex < 16; ++aiIndex) { + for (int castleID = 0; castleID < 8; ++castleID) { + int hash = -1; + this->aivFilePresent[aiIndex][castleID] = 1; + for (int player = 1; player < 9; ++player) { + if (DAT_GameSynchronyState::instance.currentPlayerFullIDArray[player] == -1) { + continue; + } + if (hash == -1) { + hash = this->receivedAIVFileHash[player][aiIndex][castleID]; + if (hash == -1) { + this->aivFilePresent[aiIndex][castleID] = 0; + break; + } + } else if (hash != this->receivedAIVFileHash[player][aiIndex][castleID]) { + this->aivFilePresent[aiIndex][castleID] = 0; + break; + } + } + if (hash == -1) { + this->aivFilePresent[aiIndex][castleID] = 0; + } + } + } + } + +} +} diff --git a/src/OpenSHC/AI/AIVState/setAvailableAIV.cpp b/src/OpenSHC/AI/AIVState/setAvailableAIV.cpp new file mode 100644 index 000000000..8efa8cb7b --- /dev/null +++ b/src/OpenSHC/AI/AIVState/setAvailableAIV.cpp @@ -0,0 +1,40 @@ +#include "../AIVState.func.hpp" + +#include "OpenSHC/IO/ResourceManager.func.hpp" + +#include "OpenSHC/Globals/DAT_AIVDefinedData.hpp" +#include "OpenSHC/Globals/DAT_GameCore.hpp" +#include "OpenSHC/Globals/DAT_ResourceManager.hpp" + +namespace OpenSHC { +namespace AI { + + // hashes all AIV files and lists the AIs that have at least one AIV + // FUNCTION: STRONGHOLDCRUSADER 0x004ECAD0 + void AIVState::setAvailableAIV() + { + for (int i = 0; i < 20; ++i) { + this->aivFileAvailabilityPerAIArray[i] = 0; + } + this->numberOfAIsWithAIV = 0; + for (int aiIndex = 0; aiIndex < 16; ++aiIndex) { + int availableAIVs = 0; + for (int castleID = 0; castleID < 8; ++castleID) { + int const hash = MACRO_CALL_MEMBER(IO::ResourceManager_Func::getSimpleFirst1024ByteSumOfFile, + DAT_ResourceManager::ptr)(DAT_AIVDefinedData::instance.AIVFileNames[aiIndex][castleID]); + this->aivFileHash[aiIndex][castleID] = hash; + if (hash != -1) { + ++availableAIVs; + } + } + if (availableAIVs != 0) { + this->aivFileAvailabilityPerAIArray[aiIndex] = availableAIVs; + DAT_GameCore::instance.arrayOfLordIdsWithAIVsUnk[this->numberOfAIsWithAIV] = aiIndex + 1; + ++this->numberOfAIsWithAIV; + } + } + DAT_GameCore::instance.numOfAIsWithCastleUnk = this->numberOfAIsWithAIV; + } + +} +} diff --git a/src/OpenSHC/AI/AIVState/setKeepOffsetAndOrientation.cpp b/src/OpenSHC/AI/AIVState/setKeepOffsetAndOrientation.cpp new file mode 100644 index 000000000..e970fbeff --- /dev/null +++ b/src/OpenSHC/AI/AIVState/setKeepOffsetAndOrientation.cpp @@ -0,0 +1,35 @@ +#include "../AIVState.func.hpp" + +#include "OpenSHC/Map/Navigation/DirectionAlgorithmState.func.hpp" + +#include "OpenSHC/Globals/DAT_DirectionAlgorithmState.hpp" + +namespace OpenSHC { +namespace AI { + + // FUNCTION: STRONGHOLDCRUSADER 0x004ECF70 + void AIVState::setKeepOffsetAndOrientation(int aivID, int keepX, int keepY) + { + // the AIV is 100x100 tiles with the keep at 43/43 + this->aivs[aivID].keepX = keepX; + this->aivs[aivID].keepY = keepY; + this->aivs[aivID].keepXOffset = keepX - 43; + this->aivs[aivID].keepYOffset = keepY - 43; + // orient the castle towards the map center + MACRO_CALL_MEMBER(Map::Navigation::DirectionAlgorithmState_Func::calculatePreferredRelativeOrientation, + DAT_DirectionAlgorithmState::ptr)(keepX, keepY, 200, 200); + // the orientation is one of the 8 compass directions; clearing the low bit snaps a diagonal onto the + // preceding cardinal direction, so only 0, 2, 4 and 6 remain + this->aivs[aivID].keepOrientation = DAT_DirectionAlgorithmState::instance.orientation & 0xfffe; + // east and west are swapped relative to the direction algorithm's numbering + if (this->aivs[aivID].keepOrientation == 6) { + this->aivs[aivID].keepOrientation = 2; + return; + } + if (this->aivs[aivID].keepOrientation == 2) { + this->aivs[aivID].keepOrientation = 6; + } + } + +} +} diff --git a/src/OpenSHC/AI/AIVState/syncAIPlayerNamesAndBuildIntervals.cpp b/src/OpenSHC/AI/AIVState/syncAIPlayerNamesAndBuildIntervals.cpp new file mode 100644 index 000000000..d5eb20c5d --- /dev/null +++ b/src/OpenSHC/AI/AIVState/syncAIPlayerNamesAndBuildIntervals.cpp @@ -0,0 +1,35 @@ +// disable deprecation warnings for strcpy +#pragma warning(disable : 4996) + +#include "../AIVState.func.hpp" + +#include "OpenSHC/AI/AICState.func.hpp" + +#include "OpenSHC/Globals/DAT_AICState.hpp" +#include "OpenSHC/Globals/DAT_GameState.hpp" +#include "OpenSHC/Globals/DAT_GameSynchronyState.hpp" + +namespace OpenSHC { +namespace AI { + + // restores the names of the AI players from the skirmish results and refreshes the AIC build intervals + // FUNCTION: STRONGHOLDCRUSADER 0x004ECE60 + void AIVState::syncAIPlayerNamesAndBuildIntervals() + { + for (int playerID = 1; playerID < 9; ++playerID) { + if (DAT_GameSynchronyState::instance.currentPlayerFullIDArray[playerID] != -1 + || DAT_GameSynchronyState::instance.currentAIArray[playerID] == 0) { + continue; + } + strcpy(DAT_GameSynchronyState::instance.DAT_PlayerNames[playerID], + DAT_GameSynchronyState::instance.finalResults.names[playerID]); + int const aivID = DAT_GameState::instance.playerDataArray[playerID].aivID; + if (aivID != 0) { + this->aivs[aivID].aivPoorLimit_OR_AIC_buildInterval + = MACRO_CALL_MEMBER(AICState_Func::getAIBuildInterval, DAT_AICState::ptr)(playerID); + } + } + } + +} +} diff --git a/src/OpenSHC/AI/AIVState/tryPlaceAIVAndReturnFitPercentage.cpp b/src/OpenSHC/AI/AIVState/tryPlaceAIVAndReturnFitPercentage.cpp new file mode 100644 index 000000000..68b4a4304 --- /dev/null +++ b/src/OpenSHC/AI/AIVState/tryPlaceAIVAndReturnFitPercentage.cpp @@ -0,0 +1,44 @@ +#include "../AIVState.func.hpp" + +#include "OpenSHC/IO/FilePackager.func.hpp" +#include "OpenSHC/IO/LowLevelMemory.func.hpp" + +#include "OpenSHC/Globals/DAT_AIVDefinedData.hpp" +#include "OpenSHC/Globals/DAT_LowLevelMemory.hpp" +#include "OpenSHC/Globals/FilePackagerObj.hpp" + +namespace OpenSHC { +namespace AI { + + // FUNCTION: STRONGHOLDCRUSADER 0x004F14F0 + int AIVState::tryPlaceAIVAndReturnFitPercentage(int aivID, int castleID) + { + MACRO_CALL_MEMBER(IO::LowLevelMemory_Func::fillMemory_ByteValue, DAT_LowLevelMemory::ptr)( + sizeof(this->isHandled), 0, this->isHandled); + if (!MACRO_CALL_MEMBER(IO::FilePackager_Func::unpackAIVFile, FilePackagerObj::ptr)( + DAT_AIVDefinedData::instance.AIVSectionAddressArray, + DAT_AIVDefinedData::instance.AIVFileNames[this->aivs[aivID].aiType - 2][castleID])) { + return -3; + } + MACRO_CALL_MEMBER(AIVState_Func::rotateAIV, this)(this->aivs[aivID].keepOrientation); + int const fit = MACRO_CALL_MEMBER(AIVState_Func::computeAIVPlacementFit, this)(aivID); + if (fit <= 0) { + return -2; + } + this->aivs[aivID].castleID = castleID; + if (fit == 999999) { + // perfect fit + this->aivs[aivID].aivSubType = 2; + return 100; + } + this->aivs[aivID].aivSubType = 1; + // the counters are declared unsigned but used as signed values + int const buildingCounter = this->buildingCounter; + if (buildingCounter == 0) { + return 100; + } + return (buildingCounter - (int)this->invalidTileCounter) * 100 / buildingCounter; + } + +} +} diff --git a/src/OpenSHC/AI/AIVState/wipeAIVsAndHeatMaps.cpp b/src/OpenSHC/AI/AIVState/wipeAIVsAndHeatMaps.cpp new file mode 100644 index 000000000..195d5d825 --- /dev/null +++ b/src/OpenSHC/AI/AIVState/wipeAIVsAndHeatMaps.cpp @@ -0,0 +1,22 @@ +#include "../AIVState.func.hpp" + +#include "OpenSHC/IO/LowLevelMemory.func.hpp" + +#include "OpenSHC/Globals/DAT_LowLevelMemory.hpp" + +namespace OpenSHC { +namespace AI { + + // FUNCTION: STRONGHOLDCRUSADER 0x004EF090 + void AIVState::wipeAIVsAndHeatMaps() + { + this->aivCount = 0; + for (int aivID = 0; aivID < 9; ++aivID) { + MACRO_CALL_MEMBER(IO::LowLevelMemory_Func::fillMemory_ByteValue, DAT_LowLevelMemory::ptr)( + sizeof(this->aivs[aivID]), 0, &this->aivs[aivID]); + } + MACRO_CALL_MEMBER(AIVState_Func::clearTheHeatmaps, this)(); + } + +} +} diff --git a/status/addresses-SHC-3BB0A8C1.txt b/status/addresses-SHC-3BB0A8C1.txt index aa3a5b357..dcc0e5208 100644 --- a/status/addresses-SHC-3BB0A8C1.txt +++ b/status/addresses-SHC-3BB0A8C1.txt @@ -37871,7 +37871,7 @@ SHC_3BB0A8C1_0x004ECA90 | 100.0% | Reimplemented SHC_3BB0A8C1_0x004ECAB0 | 100.0% | Reimplemented -SHC_3BB0A8C1_0x004ECAD0 | 0.0% | Pending +SHC_3BB0A8C1_0x004ECAD0 | 100.0% | Reimplemented SHC_3BB0A8C1_0x004ECBC0 | 0.0% | Pending @@ -37879,19 +37879,19 @@ SHC_3BB0A8C1_0x004ECCA0 | 0.0% | Pending SHC_3BB0A8C1_0x004ECD90 | 0.0% | Pending -SHC_3BB0A8C1_0x004ECDB0 | 0.0% | Pending +SHC_3BB0A8C1_0x004ECDB0 | 100.0% | Reimplemented SHC_3BB0A8C1_0x004ECDE0 | 0.0% | Pending SHC_3BB0A8C1_0x004ECDF0 | 0.0% | Pending -SHC_3BB0A8C1_0x004ECE60 | 0.0% | Pending +SHC_3BB0A8C1_0x004ECE60 | 100.0% | Reimplemented SHC_3BB0A8C1_0x004ECEF0 | 100.0% | Reimplemented -SHC_3BB0A8C1_0x004ECF70 | 0.0% | Pending +SHC_3BB0A8C1_0x004ECF70 | 100.0% | Reimplemented -SHC_3BB0A8C1_0x004ECFE0 | 0.0% | Pending +SHC_3BB0A8C1_0x004ECFE0 | 100.0% | Reimplemented SHC_3BB0A8C1_0x004ED070 | 0.0% | Pending @@ -37927,31 +37927,31 @@ SHC_3BB0A8C1_0x004ED0B0 | 0.0% | Pending SHC_3BB0A8C1_0x004ED240 | 0.0% | Pending -SHC_3BB0A8C1_0x004ED300 | 0.0% | Pending +SHC_3BB0A8C1_0x004ED300 | 100.0% | Reimplemented -SHC_3BB0A8C1_0x004ED3B0 | 0.0% | Pending +SHC_3BB0A8C1_0x004ED3B0 | 100.0% | Reimplemented -SHC_3BB0A8C1_0x004ED410 | 0.0% | Pending +SHC_3BB0A8C1_0x004ED410 | 100.0% | Reimplemented -SHC_3BB0A8C1_0x004EDC90 | 0.0% | Pending +SHC_3BB0A8C1_0x004EDC90 | 100.0% | Reimplemented -SHC_3BB0A8C1_0x004EDCB0 | 0.0% | Pending +SHC_3BB0A8C1_0x004EDCB0 | 100.0% | Reimplemented SHC_3BB0A8C1_0x004EDD00 | 0.0% | Pending -SHC_3BB0A8C1_0x004EDDF0 | 0.0% | Pending +SHC_3BB0A8C1_0x004EDDF0 | 100.0% | Reimplemented -SHC_3BB0A8C1_0x004EE140 | 0.0% | Pending +SHC_3BB0A8C1_0x004EE140 | 100.0% | Reimplemented SHC_3BB0A8C1_0x004EE230 | 0.0% | Pending -SHC_3BB0A8C1_0x004EE430 | 0.0% | Pending +SHC_3BB0A8C1_0x004EE430 | 100.0% | Reimplemented -SHC_3BB0A8C1_0x004EE830 | 0.0% | Pending +SHC_3BB0A8C1_0x004EE830 | 100.0% | Reimplemented SHC_3BB0A8C1_0x004EE920 | 0.0% | Pending -SHC_3BB0A8C1_0x004EEBD0 | 0.0% | Pending +SHC_3BB0A8C1_0x004EEBD0 | 100.0% | Reimplemented SHC_3BB0A8C1_0x004EECC0 | 0.0% | Pending @@ -37961,7 +37961,7 @@ SHC_3BB0A8C1_0x004EEEC0 | 0.0% | Pending SHC_3BB0A8C1_0x004EEF20 | 0.0% | Pending -SHC_3BB0A8C1_0x004EF090 | 0.0% | Pending +SHC_3BB0A8C1_0x004EF090 | 100.0% | Reimplemented SHC_3BB0A8C1_0x004EF0D0 | 0.0% | Pending @@ -37971,25 +37971,25 @@ SHC_3BB0A8C1_0x004EF150 | 0.0% | Pending SHC_3BB0A8C1_0x004EF450 | 0.0% | Pending -SHC_3BB0A8C1_0x004EF8C0 | 0.0% | Pending +SHC_3BB0A8C1_0x004EF8C0 | 100.0% | Reimplemented SHC_3BB0A8C1_0x004EF900 | 0.0% | Pending SHC_3BB0A8C1_0x004EF920 | 0.0% | Pending -SHC_3BB0A8C1_0x004EFA60 | 0.0% | Pending +SHC_3BB0A8C1_0x004EFA60 | 100.0% | Reimplemented SHC_3BB0A8C1_0x004EFAB0 | 0.0% | Pending -SHC_3BB0A8C1_0x004EFB90 | 0.0% | Pending +SHC_3BB0A8C1_0x004EFB90 | 100.0% | Reimplemented -SHC_3BB0A8C1_0x004EFC30 | 0.0% | Pending +SHC_3BB0A8C1_0x004EFC30 | 100.0% | Reimplemented -SHC_3BB0A8C1_0x004EFD90 | 0.0% | Pending +SHC_3BB0A8C1_0x004EFD90 | 100.0% | Reimplemented -SHC_3BB0A8C1_0x004EFE30 | 0.0% | Pending +SHC_3BB0A8C1_0x004EFE30 | 100.0% | Reimplemented -SHC_3BB0A8C1_0x004EFED0 | 0.0% | Pending +SHC_3BB0A8C1_0x004EFED0 | 100.0% | Reimplemented SHC_3BB0A8C1_0x004F0060 | 0.0% | Pending @@ -37999,11 +37999,11 @@ SHC_3BB0A8C1_0x004F0E80 | 0.0% | Pending SHC_3BB0A8C1_0x004F1190 | 0.0% | Pending -SHC_3BB0A8C1_0x004F13E0 | 0.0% | Pending +SHC_3BB0A8C1_0x004F13E0 | 100.0% | Reimplemented -SHC_3BB0A8C1_0x004F13F0 | 0.0% | Pending +SHC_3BB0A8C1_0x004F13F0 | 100.0% | Reimplemented -SHC_3BB0A8C1_0x004F14F0 | 0.0% | Pending +SHC_3BB0A8C1_0x004F14F0 | 100.0% | Reimplemented SHC_3BB0A8C1_0x004F15C0 | 0.0% | Pending @@ -60107,8 +60107,12 @@ SHC_3BB0A8C1_0x0053F07C | 0.0% | Pending SHC_3BB0A8C1_0x0053F084 | 0.0% | Pending +SHC_3BB0A8C1_0x0053F088 | 0.0% | Pending + SHC_3BB0A8C1_0x0053F08F | 0.0% | Pending +SHC_3BB0A8C1_0x0053F09C | 0.0% | Pending + SHC_3BB0A8C1_0x0053F0AA | 0.0% | Pending SHC_3BB0A8C1_0x0053F0BA | 0.0% | Pending @@ -86279,34 +86283,14 @@ SHC_3BB0A8C1_0x00EE2350 | 0.0% | Pending SHC_3BB0A8C1_0x00EE2354 | 0.0% | Pending -SHC_3BB0A8C1_0x00EE2358 | 0.0% | Pending - -SHC_3BB0A8C1_0x00EE235C | 0.0% | Pending - -SHC_3BB0A8C1_0x00EE2360 | 0.0% | Pending - SHC_3BB0A8C1_0x00EE2364 | 0.0% | Pending SHC_3BB0A8C1_0x00EE2368 | 0.0% | Pending -SHC_3BB0A8C1_0x00EE236C | 0.0% | Pending - -SHC_3BB0A8C1_0x00EE2370 | 0.0% | Pending - SHC_3BB0A8C1_0x00EE2374 | 0.0% | Pending -SHC_3BB0A8C1_0x00EE2378 | 0.0% | Pending - -SHC_3BB0A8C1_0x00EE237C | 0.0% | Pending - SHC_3BB0A8C1_0x00EE2380 | 0.0% | Pending -SHC_3BB0A8C1_0x00EE2384 | 0.0% | Pending - -SHC_3BB0A8C1_0x00EE2388 | 0.0% | Pending - -SHC_3BB0A8C1_0x00EE238C | 0.0% | Pending - SHC_3BB0A8C1_0x00EE2390 | 0.0% | Pending SHC_3BB0A8C1_0x00EE2394 | 0.0% | Pending