Add per-street targeted LAN event injection by group-name

Replaces the "inject into every pin" visual-confirmation mode with
matching against MapTrack's own event-group-name vector (offsets
0xE0/0xE4), which is available as soon as HandleEvent(evtype=1025)
fires regardless of whether any RaceEvent has resolved yet. Injection
now targets exactly one street (kTargetGroupName) and the card's
displayed name is an explicit parameter instead of a fixed constant.

Live-tested: single-target injection hits exactly the intended widget
with zero effect on the other 11 reachable MapTrack instances. The 12
reachable group names are all region/career-progression placeholders
(region{1,2,4,5}_{foothills,desert,chicago,newyork}_track{1,2,3}) -
none correspond to the currently visible/playable on-screen streets,
which a live AddEvent-level trace confirmed never call AddEvent during
normal play at all (see PROGRESS.md 2026-08-08 cont. 9/10 for the full
investigation, including the now-reverted diagnostic hooks used to
establish this).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-09 18:44:29 +03:00
co-authored by Claude Sonnet 5
parent ca55b2ea97
commit 1c6324b589
+72 -31
View File
@@ -54,6 +54,21 @@ extern void* libapp_base;
#define MAPTRACK_EVENTVEC_BEGIN_OFFSET 0x240
#define MAPTRACK_EVENTVEC_END_OFFSET 0x244
// {char* begin, char* end} vector of event-*group-name* C-strings (each a
// plain null-terminated string, confirmed by RefreshEvents' own strlen-style
// scan over it - sub_369040, static offsets 0xE0/0xE4). Populated from the
// map screen's layout/prefab data at widget-setup time, independent of
// whether any RaceEvent has actually resolved into the runtime hash-table
// cache yet - unlike RACEEVENT_TRACKNAME_OFFSET (which needs a resolved
// event, so reads as empty for a still-locked/0-event street), this vector
// is available the moment HandleEvent(evtype=1025) first fires. RefreshEvents
// uses each entry as the lookup key into the per-group handle cache
// (sub_242778) that ultimately feeds MapTrack::AddEvent - i.e. this is the
// same stable per-street identifier the real engine itself keys off of, not
// a guessed substitute.
#define MAPTRACK_GROUPNAMEVEC_BEGIN_OFFSET 0xE0
#define MAPTRACK_GROUPNAMEVEC_END_OFFSET 0xE4
// ---- Function pointer types, matching each function's real (unmangled,
// register-convention) signature from its Hex-Rays decompile ----
typedef void* (*RaceEventCtorFn)();
@@ -131,19 +146,30 @@ static AddEventFn AddEvent = nullptr;
static HandleEventFn orig_MapTrackHandleEvent = nullptr;
// Set to true to actually fabricate and inject a synthetic event once, into
// the first MapTrack pin observed with at least one real event already
// attached (a pin with real events is a lower-risk test target than an
// all-locked one - see the plan's verification notes). Set to false to run in
// observe-only mode: every pin's own TrackName gets logged (via its first real
// event, since that's already-known-safe to read) without touching anything -
// useful for confirming which street is which before narrowing the target.
// Set to true to fabricate and inject a synthetic event into exactly one
// chosen street (matched via MAPTRACK_GROUPNAMEVEC_BEGIN_OFFSET - see comment
// above - against kTargetGroupName below), instead of every pin. Set to false
// to run in observe-only mode: every pin's group-name and (if available)
// TrackName get logged without touching anything - use this first to read the
// real group-name values off a live device and pick kTargetGroupName from
// them, the same "observation-only first step" the original plan called for.
static constexpr bool kInjectSyntheticEvent = true;
// The event-group name to match against MAPTRACK_GROUPNAMEVEC_BEGIN_OFFSET's
// first entry - i.e. which street gets the synthetic card. These are internal
// engine identifiers (region+area+slot), not the localized on-screen street
// names - live-observed 2026-08-07 on this device/save: exactly 12 group
// names exist, all "region{1,2,4,5}_{foothills,desert,chicago,newyork}_track{1,2,3}".
// Picked track1 of region1_foothills as the target for this pass - which
// on-screen street (if any) that corresponds to is exactly what the
// visual-confirmation test below is for.
static const char* kTargetGroupName = "region1_foothills_track1";
// Display name shown on the injected card - the "arbitrary name" half of the
// request; change freely, no offset/layout implications.
static const char* kSyntheticEventName = "LAN: Test Lobby";
static bool g_syntheticEventInjected = false;
static void InjectSyntheticEvent(void* mapTrackThis) {
static void InjectSyntheticEvent(void* mapTrackThis, const char* eventName) {
void* ev = RaceEventCtor();
if (!ev) {
Log("LAN injection: RaceEventCtor failed");
@@ -151,7 +177,7 @@ static void InjectSyntheticEvent(void* mapTrackThis) {
}
void* internedName = nullptr;
InternString(&internedName, kSyntheticEventName);
InternString(&internedName, eventName);
*(void**)((uint8_t*)ev + RACEEVENT_EVENTNAME_OFFSET) = internedName;
void* reward = CashRewardCtor();
@@ -188,7 +214,7 @@ static void InjectSyntheticEvent(void* mapTrackThis) {
AddEvent(mapTrackThis, &key);
Log("LAN injection: added synthetic event '%s' to MapTrack %p (key=0x%08x)",
kSyntheticEventName, mapTrackThis, key);
eventName, mapTrackThis, key);
}
extern "C" int Hook_MapTrackHandleEvent(void* mapTrackThis, void* event) {
@@ -196,6 +222,20 @@ extern "C" int Hook_MapTrackHandleEvent(void* mapTrackThis, void* event) {
int evType = *(int*)((uint8_t*)event + 4);
if (evType == EVENT_TYPE_FLOW_SET_LAYOUT_SCREEN) {
// Read the street's stable group-name identifier (see
// MAPTRACK_GROUPNAMEVEC_BEGIN_OFFSET comment) - available regardless
// of whether any RaceEvent has resolved yet, unlike the
// TrackName-from-first-event logging below.
char** groupNameBegin = *(char***)((uint8_t*)mapTrackThis + MAPTRACK_GROUPNAMEVEC_BEGIN_OFFSET);
char** groupNameEnd = *(char***)((uint8_t*)mapTrackThis + MAPTRACK_GROUPNAMEVEC_END_OFFSET);
bool groupVecLooksValid = (uintptr_t)groupNameBegin > 0x10000 && groupNameEnd >= groupNameBegin
&& (groupNameEnd - groupNameBegin) < 64;
const char* groupName = nullptr;
if (groupVecLooksValid && groupNameEnd != groupNameBegin) {
groupName = groupNameBegin[0];
}
Log("MapTrack %p: groupName='%s'", mapTrackThis, groupName ? groupName : "<none>");
uint32_t* vecBegin = *(uint32_t**)((uint8_t*)mapTrackThis + MAPTRACK_EVENTVEC_BEGIN_OFFSET);
uint32_t* vecEnd = *(uint32_t**)((uint8_t*)mapTrackThis + MAPTRACK_EVENTVEC_END_OFFSET);
// Sanity-check before trusting these enough to dereference vecBegin[0]
@@ -241,26 +281,27 @@ extern "C" int Hook_MapTrackHandleEvent(void* mapTrackThis, void* event) {
Log("MapTrack %p: 0 events (locked/no events for this street)", mapTrackThis);
}
// Injection no longer gated on count>0: live testing (2026-08-07) showed
// this street's FIRST evtype==1025 firing usually finds its own
// RaceEvent-hash registry still empty (a real race between the map
// screen's layout pass and background registry population - masked
// during breakpoint-based debugging, since pausing the process gives
// the background loading thread extra wall-clock time to finish).
// AddEvent push_backs onto the vector regardless of its current size,
// so injecting unconditionally on the first pin encountered is just as
// valid a test and doesn't depend on winning that race.
// Targeted injection: only the one street whose group-name matches
// kTargetGroupName gets the synthetic card - not gated on count>0,
// since AddEvent push_backs onto the event vector regardless of its
// current size, and this street's FIRST evtype==1025 firing can find
// its own RaceEvent-hash registry still empty (a real race between
// the map screen's layout pass and background registry population,
// live-tested 2026-08-07 - AddEvent still succeeds either way).
//
// Temporarily injecting into every pin this dispatches to (not just
// the first) for visual confirmation: it's not yet known which of the
// ~12 MapTrack instances that fire through this hook correspond to
// the handful of on-screen streets the player actually sees - the 3
// visible streets each showed a real event already, so their own
// FlowSetLayoutScreenEvent may be dispatched through a different path
// this hook doesn't intercept. Broadening to "every pin" for this
// pass answers that empirically instead of guessing further.
if (kInjectSyntheticEvent) {
InjectSyntheticEvent(mapTrackThis);
// Known limitation (2026-08-08): the group names reachable through
// this hook are all region{1,2,4,5}_{foothills,desert,chicago,
// newyork}_track{1,2,3} career-progression placeholders - none of
// them correspond to the currently-visible/playable on-screen
// streets. A live AddEvent-level trace confirmed those visible
// streets never call AddEvent during normal play at all (their
// events are most likely already baked into the save data rather
// than resolved through this runtime prefab-cache path), so
// kTargetGroupName can only currently target one of the
// career-progression slots, not an arbitrary on-screen street name.
// See PROGRESS.md 2026-08-08 (cont. 9) for the full investigation.
if (kInjectSyntheticEvent && groupName && strcmp(groupName, kTargetGroupName) == 0) {
InjectSyntheticEvent(mapTrackThis, kSyntheticEventName);
}
}