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:
@@ -54,6 +54,21 @@ extern void* libapp_base;
|
|||||||
#define MAPTRACK_EVENTVEC_BEGIN_OFFSET 0x240
|
#define MAPTRACK_EVENTVEC_BEGIN_OFFSET 0x240
|
||||||
#define MAPTRACK_EVENTVEC_END_OFFSET 0x244
|
#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,
|
// ---- Function pointer types, matching each function's real (unmangled,
|
||||||
// register-convention) signature from its Hex-Rays decompile ----
|
// register-convention) signature from its Hex-Rays decompile ----
|
||||||
typedef void* (*RaceEventCtorFn)();
|
typedef void* (*RaceEventCtorFn)();
|
||||||
@@ -131,19 +146,30 @@ static AddEventFn AddEvent = nullptr;
|
|||||||
|
|
||||||
static HandleEventFn orig_MapTrackHandleEvent = nullptr;
|
static HandleEventFn orig_MapTrackHandleEvent = nullptr;
|
||||||
|
|
||||||
// Set to true to actually fabricate and inject a synthetic event once, into
|
// Set to true to fabricate and inject a synthetic event into exactly one
|
||||||
// the first MapTrack pin observed with at least one real event already
|
// chosen street (matched via MAPTRACK_GROUPNAMEVEC_BEGIN_OFFSET - see comment
|
||||||
// attached (a pin with real events is a lower-risk test target than an
|
// above - against kTargetGroupName below), instead of every pin. Set to false
|
||||||
// all-locked one - see the plan's verification notes). Set to false to run in
|
// to run in observe-only mode: every pin's group-name and (if available)
|
||||||
// observe-only mode: every pin's own TrackName gets logged (via its first real
|
// TrackName get logged without touching anything - use this first to read the
|
||||||
// event, since that's already-known-safe to read) without touching anything -
|
// real group-name values off a live device and pick kTargetGroupName from
|
||||||
// useful for confirming which street is which before narrowing the target.
|
// them, the same "observation-only first step" the original plan called for.
|
||||||
static constexpr bool kInjectSyntheticEvent = true;
|
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 const char* kSyntheticEventName = "LAN: Test Lobby";
|
||||||
|
|
||||||
static bool g_syntheticEventInjected = false;
|
static void InjectSyntheticEvent(void* mapTrackThis, const char* eventName) {
|
||||||
|
|
||||||
static void InjectSyntheticEvent(void* mapTrackThis) {
|
|
||||||
void* ev = RaceEventCtor();
|
void* ev = RaceEventCtor();
|
||||||
if (!ev) {
|
if (!ev) {
|
||||||
Log("LAN injection: RaceEventCtor failed");
|
Log("LAN injection: RaceEventCtor failed");
|
||||||
@@ -151,7 +177,7 @@ static void InjectSyntheticEvent(void* mapTrackThis) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void* internedName = nullptr;
|
void* internedName = nullptr;
|
||||||
InternString(&internedName, kSyntheticEventName);
|
InternString(&internedName, eventName);
|
||||||
*(void**)((uint8_t*)ev + RACEEVENT_EVENTNAME_OFFSET) = internedName;
|
*(void**)((uint8_t*)ev + RACEEVENT_EVENTNAME_OFFSET) = internedName;
|
||||||
|
|
||||||
void* reward = CashRewardCtor();
|
void* reward = CashRewardCtor();
|
||||||
@@ -188,7 +214,7 @@ static void InjectSyntheticEvent(void* mapTrackThis) {
|
|||||||
AddEvent(mapTrackThis, &key);
|
AddEvent(mapTrackThis, &key);
|
||||||
|
|
||||||
Log("LAN injection: added synthetic event '%s' to MapTrack %p (key=0x%08x)",
|
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) {
|
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);
|
int evType = *(int*)((uint8_t*)event + 4);
|
||||||
if (evType == EVENT_TYPE_FLOW_SET_LAYOUT_SCREEN) {
|
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* vecBegin = *(uint32_t**)((uint8_t*)mapTrackThis + MAPTRACK_EVENTVEC_BEGIN_OFFSET);
|
||||||
uint32_t* vecEnd = *(uint32_t**)((uint8_t*)mapTrackThis + MAPTRACK_EVENTVEC_END_OFFSET);
|
uint32_t* vecEnd = *(uint32_t**)((uint8_t*)mapTrackThis + MAPTRACK_EVENTVEC_END_OFFSET);
|
||||||
// Sanity-check before trusting these enough to dereference vecBegin[0]
|
// 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);
|
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
|
// Targeted injection: only the one street whose group-name matches
|
||||||
// this street's FIRST evtype==1025 firing usually finds its own
|
// kTargetGroupName gets the synthetic card - not gated on count>0,
|
||||||
// RaceEvent-hash registry still empty (a real race between the map
|
// since AddEvent push_backs onto the event vector regardless of its
|
||||||
// screen's layout pass and background registry population - masked
|
// current size, and this street's FIRST evtype==1025 firing can find
|
||||||
// during breakpoint-based debugging, since pausing the process gives
|
// its own RaceEvent-hash registry still empty (a real race between
|
||||||
// the background loading thread extra wall-clock time to finish).
|
// the map screen's layout pass and background registry population,
|
||||||
// AddEvent push_backs onto the vector regardless of its current size,
|
// live-tested 2026-08-07 - AddEvent still succeeds either way).
|
||||||
// so injecting unconditionally on the first pin encountered is just as
|
|
||||||
// valid a test and doesn't depend on winning that race.
|
|
||||||
//
|
//
|
||||||
// Temporarily injecting into every pin this dispatches to (not just
|
// Known limitation (2026-08-08): the group names reachable through
|
||||||
// the first) for visual confirmation: it's not yet known which of the
|
// this hook are all region{1,2,4,5}_{foothills,desert,chicago,
|
||||||
// ~12 MapTrack instances that fire through this hook correspond to
|
// newyork}_track{1,2,3} career-progression placeholders - none of
|
||||||
// the handful of on-screen streets the player actually sees - the 3
|
// them correspond to the currently-visible/playable on-screen
|
||||||
// visible streets each showed a real event already, so their own
|
// streets. A live AddEvent-level trace confirmed those visible
|
||||||
// FlowSetLayoutScreenEvent may be dispatched through a different path
|
// streets never call AddEvent during normal play at all (their
|
||||||
// this hook doesn't intercept. Broadening to "every pin" for this
|
// events are most likely already baked into the save data rather
|
||||||
// pass answers that empirically instead of guessing further.
|
// than resolved through this runtime prefab-cache path), so
|
||||||
if (kInjectSyntheticEvent) {
|
// kTargetGroupName can only currently target one of the
|
||||||
InjectSyntheticEvent(mapTrackThis);
|
// 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user