From 1c6324b589a6bafae8a5ab2d6277bf2b9946c745 Mon Sep 17 00:00:00 2001 From: megboyzz Date: Sun, 9 Aug 2026 18:44:29 +0300 Subject: [PATCH] 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 --- mpcore/src/main/cpp/lan_event_injection.h | 103 +++++++++++++++------- 1 file changed, 72 insertions(+), 31 deletions(-) diff --git a/mpcore/src/main/cpp/lan_event_injection.h b/mpcore/src/main/cpp/lan_event_injection.h index 9998961..0b068c7 100644 --- a/mpcore/src/main/cpp/lan_event_injection.h +++ b/mpcore/src/main/cpp/lan_event_injection.h @@ -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 : ""); + 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); } }