SD: record the two weapon lookups that gate the loop-carried draw
The loop-carried cl_Chance(0.2f) at 0x006ae575 has measured zero on all six composer calls of all four runs, and a word count cannot say which of its two gates did it: a null from the UNRESTRICTED weapon lookup skips the whole point-defence block, a null from the RESTRICTED one short-circuits every iteration without drawing. Both print zero -- method rule 20 one level below the level the sub-bracket was built for. So the chooser 0x006ad2a0 is detoured and its two return values recorded per composer call, keyed by the return address that distinguishes the call sites (0x006ae3c6 restricted, 0x006ae3e1 default), and printed as w_alt / w_def. NOT YET RUN. Every measurement in findings/subsystems/ship-design-composer.md was taken with the previous build; this hook is the instrument for the next lane, not for the numbers already published. Header regenerated from ghidra/addresses.json plus the fragments, never hand-resolved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARBgSooAfokKUy6wKUKEyZ
This commit is contained in:
parent
dd105da759
commit
414bcc743e
2 changed files with 38 additions and 3 deletions
|
|
@ -1,5 +1,5 @@
|
|||
// GENERATED — do not edit. Facts about Sword of the Stars.exe (GOG 1.8.1).
|
||||
// Source: sots-re ghidra/addresses.json @ 1bae6f1, generated 2026-09-08 by tools/gen_addresses.py
|
||||
// Source: sots-re ghidra/addresses.json @ 67cd78b, generated 2026-09-08 by tools/gen_addresses.py
|
||||
// Runtime address = (uintptr_t)GetModuleHandle(NULL) + RVA (the exe is ASLR-relocated).
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
|
@ -2147,6 +2147,8 @@ constexpr uint32_t AIShipSectionContext_stride = 0x00000124;
|
|||
constexpr uint32_t ShipSectionDef_off_Banks = 0x0000031c;
|
||||
// offset Mars::String -- the section's file name on a Game::ShipSectionDef; capacity word at +0x1c8, so the payload is the inline buffer when capacity < 0x10 and *(char**)(def+0x1b4) otherwise. The composer compares it case-insensitively against "DEPointDefence" and "CRPointDefence" at 0x006ae4e6/0x006ae4fd: a section whose name matches gets a point-defence weapon in EVERY qualifying bank instead of every D'-th one [verified]
|
||||
constexpr uint32_t ShipSectionDef_off_Name = 0x000001b4;
|
||||
// cdecl int (StrategyAIAgent* agent, int hullClass, std::vector<int>* restrictTo, int bankClass, int bankSize, void* ctx, int flag) /* seven stack arguments, caller cleans 0x38 for the two calls at 0x006ae3c1/0x006ae3dc. Returns a weapon id, or 0 when nothing fits. The composer calls it in the weapon-assignment loop and then TWICE more just before the point-defence pass: once with a one-element list holding the literal 0x25 (read as the point-defence weapon family -- INFERRED, from the DEPointDefence/CRPointDefence section names the same pass matches and the 0.2 probability it rolls) and once unrestricted. A null from the unrestricted call skips the whole point-defence block; a null from the restricted one makes every iteration short-circuit without drawing. Both make the loop-carried draw cost zero, which is why lane SD's probe records the two return values separately */ [verified]
|
||||
constexpr uint32_t AIChooseWeaponForBank = 0x002ad2a0;
|
||||
// thiscall int (Game::SVScriptObject* this, int evt, void* arg) // the script-object event bus. Calls this->vft[0x10](evt, arg) -- the GENERIC handler every object sees -- then `cmp evt,0x20; ja done; jmp dword [evt*4 + SVScriptObject_EventSlotJumpTable]`, which dispatches to ONE event-specific vtable slot with the argument shape that event carries. Every hand-written `vft[0x10](id,0); vft[slot]()` pair in the two turn drivers is this same two-step done on the root object [verified]
|
||||
constexpr uint32_t SVScriptObject_DispatchEvent = 0x003a60d0;
|
||||
// data void* [33] // evt (0..0x20) -> the vtable slot SVScriptObject_DispatchEvent calls. Slot byte offsets in evt order: 0x14 0x18 0x1c 0x20 0x24 0x28 0x2c 0x30 0x34 0x38 0x3c 0x40 0x44 0x48 0x4c 0x50 0x54 0x58 0x5c 0x60 0x64 0x6c 0x70 0x74 0x68 0x7c 0x80 0x84 0x78 0x88 0x8c 0x90 0x94. Note 0x15->+0x6c, 0x16->+0x70, 0x17->+0x74, 0x18->+0x68 and 0x1c->+0x78 are NOT in slot order [verified]
|
||||
|
|
|
|||
|
|
@ -386,6 +386,30 @@ void NoteCaller(const void* ret, std::uint8_t which, int result) {
|
|||
++g_ncallers;
|
||||
}
|
||||
|
||||
// ---- lane SD: the two weapon lookups that gate the loop-carried draw -----------------------------
|
||||
//
|
||||
// `H_obs = 0` has two causes the sub-bracket cannot tell apart from outside: the DEFAULT weapon
|
||||
// lookup returning null skips the whole point-defence block, and the RESTRICTED (point-defence)
|
||||
// lookup returning null short-circuits every iteration without drawing. Both print zero. That is
|
||||
// method rule 20 one level below the level the probe was built for, so the two return values are
|
||||
// captured here, keyed by the return address that distinguishes the two call sites
|
||||
// (0x006ae3c6 = restricted, 0x006ae3e1 = default).
|
||||
std::uint32_t g_wAlt = 0xffffffffu; // 0xffffffff = "not called in this composer call"
|
||||
std::uint32_t g_wDef = 0xffffffffu;
|
||||
using ChooseWeaponFn = int(SHIM_CDECL*)(int, int, int, int, int, int, int);
|
||||
void* g_trChooseWeapon = nullptr;
|
||||
|
||||
int SHIM_CDECL DetourChooseWeapon(int a1, int a2, int a3, int a4, int a5, int a6, int a7) {
|
||||
const std::uintptr_t ra = reinterpret_cast<std::uintptr_t>(__builtin_return_address(0));
|
||||
const int r = reinterpret_cast<ChooseWeaponFn>(g_trChooseWeapon)(a1, a2, a3, a4, a5, a6, a7);
|
||||
if (g_subActive) {
|
||||
const std::uint32_t va = static_cast<std::uint32_t>(ra - g_exeBase) + A::IMAGE_BASE;
|
||||
if (va == 0x006ae3c6u) g_wAlt = static_cast<std::uint32_t>(r);
|
||||
else if (va == 0x006ae3e1u) g_wDef = static_cast<std::uint32_t>(r);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
using ClChanceFn = bool(SHIM_CDECL*)(float);
|
||||
using ClRandRangeFn = int(SHIM_CDECL*)(int, int);
|
||||
void* g_trClChance = nullptr;
|
||||
|
|
@ -506,6 +530,8 @@ int sd_composer_body(void* ecx, void* edx, DesignReq* req, unsigned dry) {
|
|||
const std::int32_t leftIn = LeftOf(g_active);
|
||||
|
||||
g_subActive = true;
|
||||
g_wAlt = 0xffffffffu;
|
||||
g_wDef = 0xffffffffu;
|
||||
g_subWords = 0;
|
||||
g_subRandFloatCalls = 0;
|
||||
g_nsub = 0;
|
||||
|
|
@ -598,8 +624,9 @@ int sd_composer_body(void* ecx, void* edx, DesignReq* req, unsigned dry) {
|
|||
isPD[1] ? 1 : 0, isPD[2] ? 1 : 0, static_cast<double>(f), M, Dp, Hpred, cH,
|
||||
Hpred == cH ? "HOLDS" : (cH == 0 ? "GATED-OR-WRONG" : "WRONG"));
|
||||
LogF("aidesignsite seq=%u dseq=%u A_randfloat=%u B_hull=%u C_cmd=%u D_chance=%u(t%u) "
|
||||
"E_pick=%u F_chance=%u(t%u) G_chance=%u(t%u) H_loop=%u(t%u) sub_overflow=%u",
|
||||
g_seq, seq, cA, cB, cC, cD, tD, cE, cF, tF, cG, tG, cH, tH, g_subOverflow);
|
||||
"E_pick=%u F_chance=%u(t%u) G_chance=%u(t%u) H_loop=%u(t%u) sub_overflow=%u "
|
||||
"w_alt=0x%08x w_def=0x%08x",
|
||||
g_seq, seq, cA, cB, cC, cD, tD, cE, cF, tF, cG, tG, cH, tH, g_subOverflow, g_wAlt, g_wDef);
|
||||
for (int p = 0; p < 3; ++p)
|
||||
LogF("aidesignpart seq=%u dseq=%u part=%d sec=0x%08x mounts=%u qual=%u pd=%d name=%s",
|
||||
g_seq, seq, p, sectionId[p], U32(parts + static_cast<std::uint32_t>(p) * kPartStride +
|
||||
|
|
@ -803,6 +830,12 @@ void install_ai_rng(std::uintptr_t exeBase, const char* gameDir, void (*log)(con
|
|||
MH_StatusToString(a), MH_StatusToString(b));
|
||||
}
|
||||
if (g_design) {
|
||||
void* wt = reinterpret_cast<void*>(exeBase + A::AIChooseWeaponForBank);
|
||||
MH_STATUS wa = MH_CreateHook(wt, reinterpret_cast<void*>(&DetourChooseWeapon),
|
||||
&g_trChooseWeapon);
|
||||
MH_STATUS wb = wa == MH_OK ? MH_EnableHook(wt) : wa;
|
||||
LogF("aidesign: weapon chooser 0x006ad2a0 rva=0x%08x va=%p create=%s enable=%s",
|
||||
A::AIChooseWeaponForBank, wt, MH_StatusToString(wa), MH_StatusToString(wb));
|
||||
void* ct = reinterpret_cast<void*>(exeBase + A::AIComposeShipBlueprint);
|
||||
MH_STATUS a = MH_CreateHook(ct, reinterpret_cast<void*>(&sd_composer_thunk), &g_trComposer);
|
||||
MH_STATUS b = a == MH_OK ? MH_EnableHook(ct) : a;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue