#pragma once // Subtask 2.1 — substitute real lobby players' car+color into AI opponent // slots. See ANALYSIS.md §6hh and ARCHITECTURE.md §3b for the full RE // writeup this is built on. // // cont.70 CONFIRMED LIVE: OpponentCollection::PopulateFromProperties // (sub_2B649C) builds a vector of Opponent* (8-byte elements) at // this+12/+16/+20. StreetRaceStartingGrid's own placement method // (sub_2B88BC) separately iterates a DIFFERENT vector (12-byte elements, // different heap addresses entirely, confirmed by comparing live pointer // dumps from both hooks) - so the two are NOT the same objects, there's an // intermediate spawn/resolve step between them. Despite that, overwriting // Opponent.CarDescriptionName/ColourIndex (+20/+24 string, +36 int) at // Populate time (map-load) WAS confirmed live to reach the actual spawned, // rendered car at the starting grid - screenshotted a Ford Focus RS500 // forced into an opponent slot that was a completely different car/color // before the hook, on a real (non-synthetic) "Перед вами FAIRHAVEN" replay. // Whatever the intermediate step is, it reads CarDescriptionName/ColourIndex // off these same Opponent objects (or a value-copy taken after Populate // already ran), not off some earlier, already-fixed snapshot. Mechanism is // proven; the intermediate step itself was not traced (not needed for the // question this was testing). #include #include #include #include "util/util.h" #include "util/hook_install.h" extern void* libapp_base; #define OPPONENT_COLLECTION_POPULATE_OFFSET 0x2B649C #define STREET_RACE_GRID_PLACE_OFFSET 0x2B88BC typedef int (*OpponentCollectionPopulateFn)(uint32_t* thisPtr, int a2, uint32_t* a3); static OpponentCollectionPopulateFn orig_OpponentCollectionPopulate = nullptr; static volatile int g_opponentPopulateLogBudget = 2000; // Blanket test substitution (every opponent slot of EVERY collection, any // race) that produced the cont.70 live confirmation above. Kept here, but // gated OFF by default (g_enableBlanketOpponentSubstitutionTest) - it has no // way to target a specific race or a specific real lobby player yet (no // lobby system exists to source that data from), so leaving it on would // silently affect every race in normal play. Re-enable deliberately only for // another isolated live test; the real, production substitution hook // (targeted, sourced from actual lobby player data) is a separate follow-up // once the lobby UI/data model exists to drive it. // // cont.72: extended from a single fixed test car (cont.70) to a small roster // of distinct cars/colors cycling by slot index, to emulate what a real // multi-player lobby would look like (each opponent slot = "a different // player's own car"), per direct request. struct TestSubstituteCar { const char* carId; int colourIndex; }; // cont.76: user-requested specific demo roster (car IDs sourced from // unpacked game data files, not binary strings, per user's own correction). static const TestSubstituteCar kTestSubstituteRoster[] = { {"marussia_b2_2011_desc", 0}, {"lamborghini_gallardo_lp570_4_superleggera_2011_desc", 0}, {"srt_viper_2013_desc", 0}, {"lamborghini_aventador_lp700_4_2011_desc", 0}, }; static const int kTestSubstituteRosterLength = (int)(sizeof(kTestSubstituteRoster) / sizeof(kTestSubstituteRoster[0])); static volatile bool g_enableBlanketOpponentSubstitutionTest = true; extern "C" int Hook_OpponentCollectionPopulate(uint32_t* thisPtr, int a2, uint32_t* a3) { int result = orig_OpponentCollectionPopulate(thisPtr, a2, a3); uint32_t* begin = (uint32_t*)thisPtr[3]; uint32_t* end = (uint32_t*)thisPtr[4]; int count = begin ? (int)(end - begin) / 2 : 0; if (count > 0 && g_enableBlanketOpponentSubstitutionTest) { for (int i = 0; i < count; i++) { uint32_t* opp = (uint32_t*)begin[i * 2]; if (!opp) continue; const TestSubstituteCar& car = kTestSubstituteRoster[i % kTestSubstituteRosterLength]; size_t len = strlen(car.carId); *(const char**)((uint8_t*)opp + 20) = car.carId; *(const char**)((uint8_t*)opp + 24) = car.carId + len; *(int*)((uint8_t*)opp + 36) = car.colourIndex; } } if (g_opponentPopulateLogBudget > 0) { g_opponentPopulateLogBudget--; Log("DIAG OpponentCollection::Populate: this=%p vec=[%p..%p) count=%d", thisPtr, begin, end, count); for (int i = 0; i < count && i < 8; i++) { uint32_t* opp = (uint32_t*)begin[i * 2]; if (!opp) { Log(" [%d] Opponent*=NULL", i); continue; } const char* nameBegin = *(const char**)((uint8_t*)opp + 20); const char* nameEnd = *(const char**)((uint8_t*)opp + 24); int nameLen = (nameEnd && nameBegin && nameEnd > nameBegin) ? (int)(nameEnd - nameBegin) : 0; char buf[96]; if (nameLen > 0 && nameLen < (int)sizeof(buf)) { memcpy(buf, nameBegin, nameLen); buf[nameLen] = 0; } else { buf[0] = 0; } int colourIndex = *(int*)((uint8_t*)opp + 36); Log(" [%d] Opponent*=%p CarDescriptionName=\"%s\" ColourIndex=%d (AFTER any substitution)", i, opp, buf, colourIndex); } } return result; } static bool InstallOpponentCollectionPopulateHook() { orig_OpponentCollectionPopulate = (OpponentCollectionPopulateFn)InstallArmTrampolineHook( libapp_base, OPPONENT_COLLECTION_POPULATE_OFFSET, (void*)&Hook_OpponentCollectionPopulate, "OpponentCollection::Populate diag hook"); return orig_OpponentCollectionPopulate != nullptr; } typedef int (*StreetRaceGridPlaceFn)(int a1, int* a2, int a3, uint32_t* a4); static StreetRaceGridPlaceFn orig_StreetRaceGridPlace = nullptr; static volatile int g_gridPlaceLogBudget = 20; // cont.72: random player grid position. sub_2B88BC's own algorithm (see // ANALYSIS.md §6hh Q2) is why the player is always last - it loops over // every opponent first (accumulating a randomized inter-car distance each // time via sub_291BA4/PlaceOpponent), THEN places the player once, at // whatever distance the loop finished on. There's no field to flip for // this - the "always last" behavior is baked into the call ORDER, so // randomizing it means reimplementing that order, not just tweaking data // beforehand/afterward like every other hook in this project. This calls // the same two real placement primitives orig uses (sub_291BA4 for an // opponent - keeps its own lateral zigzag math untouched; sub_2914DC/ // PlaceCar directly for the player, lateral=0, matching orig's own player // call) in a randomly reordered sequence: count+1 total slots (N opponents // + 1 player), one is picked at random for the player, the rest go to // opponents in their original order. Distance accumulation matches orig's // own shape (place, then advance by a random offset in // [MinDistanceBetweenRacers, MaxDistanceBetweenRacers]) but uses plain // rand() instead of replicating sub_75680/sub_61C9F8's own RNG chain // (which is seeded from the just-placed car's own return value in a way // not fully understood - not worth the risk of misusing it for a test // hook). Falls back to the real, untouched orig_StreetRaceGridPlace // whenever this test is disabled - zero behavior change for normal play. typedef int (*PlaceOpponentFn)(int a1, int a2, int a3, float a4, float a5, int a6); typedef int (*PlaceCarFn)(int a1, uint32_t* a2, int a3, int a4, float a5); static PlaceOpponentFn PlaceOpponent = nullptr; static PlaceCarFn PlaceCar = nullptr; static volatile bool g_enableRandomPlayerGridPositionTest = true; extern "C" int Hook_StreetRaceGridPlace(int a1, int* a2, int a3, uint32_t* a4) { if (g_gridPlaceLogBudget > 0) { g_gridPlaceLogBudget--; uint32_t begin = a4[0]; uint32_t end = a4[1]; int count = (end > begin) ? (int)(end - begin) / 12 : 0; Log("DIAG StreetRaceGrid::Place: a1(grid)=%p a2(ctx)=%p a3(playerHandle)=0x%x vec=[0x%x..0x%x) count=%d", (void*)(uintptr_t)a1, (void*)a2, a3, begin, end, count); for (int i = 0; i < count && i < 8; i++) { uint32_t* elem = (uint32_t*)(uintptr_t)(begin + i * 12); Log(" [%d] word0=0x%x word1=0x%x word2=0x%x", i, elem[0], elem[1], elem[2]); } } if (!g_enableRandomPlayerGridPositionTest || !PlaceOpponent || !PlaceCar) { return orig_StreetRaceGridPlace(a1, a2, a3, a4); } uint32_t begin = a4[0]; uint32_t end = a4[1]; int count = (end > begin) ? (int)(end - begin) / 12 : 0; if (count <= 0) { return orig_StreetRaceGridPlace(a1, a2, a3, a4); } float minDist = *(float*)((uint8_t*)(uintptr_t)a1 + 12); float maxDist = *(float*)((uint8_t*)(uintptr_t)a1 + 16); float trackWidthFraction = *(float*)((uint8_t*)(uintptr_t)a1 + 20); float playerSpeed = *(float*)((uint8_t*)(uintptr_t)a1 + 24); float opponentSpeed = *(float*)((uint8_t*)(uintptr_t)a1 + 28); int playerSlot = rand() % (count + 1); // 0..count inclusive - count+1 total physical slots int ctxDeref = *a2; float distance = 0.0f; int opponentIndex = 0; Log("RANDOM GRID TEST: %d opponents, player placed at slot %d/%d", count, playerSlot, count); // Opponent lane index a6 cycles ((a6+1)%3), and a6%3==0 lands exactly on // world-lateral 0 (center) - the SAME literal offset PlaceCar always uses // for the player. Vanilla is safe because the player is placed once, at // the very end, past the entire accumulated distance - never adjacent to // any specific opponent. Here the player can land next to an opponent // sharing that center lane, and a plain minDist..maxDist gap (sized for // adjacent DIFFERENT lanes) isn't enough separation for two cars in the // SAME lane - reproduced live as a spawn-inside-another-car launch bug. // Fix: widen the distance gap on both sides of the player's slot so // longitudinal separation alone guarantees no overlap, regardless of lane. for (int slot = 0; slot <= count; slot++) { if (slot == playerSlot) { PlaceCar(ctxDeref, (uint32_t*)(uintptr_t)a3, (int)distance, 0, playerSpeed); } else { uint32_t* elem = (uint32_t*)(uintptr_t)(begin + opponentIndex * 12); PlaceOpponent(ctxDeref, (int)(uintptr_t)elem, (int)distance, trackWidthFraction, opponentSpeed, opponentIndex); opponentIndex++; } float randomFraction = (float)rand() / (float)RAND_MAX; bool adjacentToPlayer = (slot == playerSlot) || (slot + 1 == playerSlot); float gapMin = adjacentToPlayer ? (minDist + maxDist) : minDist; float gapMax = adjacentToPlayer ? (minDist + maxDist) * 2.0f : maxDist; distance += gapMin + randomFraction * (gapMax - gapMin); } return 1; } static bool InstallStreetRaceGridPlaceHook() { orig_StreetRaceGridPlace = (StreetRaceGridPlaceFn)InstallArmTrampolineHook( libapp_base, STREET_RACE_GRID_PLACE_OFFSET, (void*)&Hook_StreetRaceGridPlace, "StreetRaceGrid::Place diag hook"); PlaceOpponent = (PlaceOpponentFn)((uintptr_t)libapp_base + 0x291BA4); PlaceCar = (PlaceCarFn)((uintptr_t)libapp_base + 0x2914DC); return orig_StreetRaceGridPlace != nullptr; }