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_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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user