Log mappin slot RVAs

This commit is contained in:
2026-06-19 23:15:11 -05:00
parent 306167c6a5
commit 463b3a70e6
2 changed files with 169 additions and 58 deletions
+84 -36
View File
@@ -17,11 +17,14 @@ and lane connection probabilities. Live tests have not supported that:
- probing the obvious `TrafficSystem_Pathfinding` and - probing the obvious `TrafficSystem_Pathfinding` and
`StartPathfinding`/`StopPathfinding` string-xref wrappers did not fire during `StartPathfinding`/`StopPathfinding` string-xref wrappers did not fire during
deliberate world-map route plotting deliberate world-map route plotting
- probing the obvious `RunGPSQuery` and `UpdateGPSQuery` helpers also did not
fire during deliberate world-map route plotting
The stronger current theory is that player GPS route selection goes through a The stronger current theory is that player GPS route selection goes through a
native GPS/mappin query path, with traffic data used by traffic simulation and native world-map mappin tracking path, which then updates native GPS state
possibly as an input to cooked GPS data, but not as a live route-cost graph that downstream. Traffic lane data is still likely used somewhere in the final route
can be steered by simple resource weighting. line, but the tested `maxSpeed` and lane-exit probability fields are traffic
simulation inputs, not the live player-GPS edge-cost knobs.
## Static GPS Query Candidates ## Static GPS Query Candidates
@@ -50,8 +53,8 @@ the wrapper.
`UpdateGPSQueryHelper` appears related but takes a request type and target. Its `UpdateGPSQueryHelper` appears related but takes a request type and target. Its
return value is also used as a success flag. return value is also used as a success flag.
The next RED4ext probe should hook these helpers with return-value-preserving These helpers were hooked with return-value-preserving signatures, not the
signatures, not the registration thunks: registration thunks:
```cpp ```cpp
bool RunGPSQueryHelper(void* this_, void* from, void* to, void* debugText, bool RunGPSQueryHelper(void* this_, void* from, void* to, void* debugText,
@@ -61,9 +64,50 @@ bool UpdateGPSQueryHelper(void* this_, uint32_t requestType, void* target,
``` ```
These signatures are inferred from the wrapper call sites and Windows x64 These signatures are inferred from the wrapper call sites and Windows x64
calling convention. If either helper fires exactly when a map destination is calling convention.
selected, it is a much better path into the actual route computation than the
traffic-pathfinding probes. Live result: neither helper fired when plotting multiple world-map routes. They
remain useful static landmarks for GPS-like script APIs, but they are not the
world-map destination tracking path being exercised by normal player route
selection.
## Mappin Tracking Handoff
The current native lead is the script/native mappin tracking path:
```text
TrackMappin string rva: 0x2e82c30
TrackMappin registration xref: 0x1843eb8
TrackMappin wrapper rva: 0x27c4ab4
TrackMappin core rva: 0x27c11d0
Mappin system resolver rva: 0x02bb840
SetMappinTrackingAlternative string rva: 0x2c672e0
SetMappinTrackingAlternative registration xref: 0x121c194
SetMappinTrackingAlternative wrapper rva: 0x1246794
```
`TrackMappinCore` resolves a native mappin system through `0x02bb840`. The
resolver loads an object pointer from `this + 0xa8`, reads the mappin-system
type/singleton global at `0x342f138`, and calls vtable slot `0x08` on the
object it loaded.
Once the native mappin system is resolved, `TrackMappinCore` branches on whether
the supplied mappin handle is null:
- non-null tracked mappin: call resolved system vtable slot `0x220`
- null tracked mappin: call resolved system vtable slot `0x228`
That looks exactly like "set tracked mappin" versus "clear tracked mappin". The
installed RED4ext probe now resolves this system at runtime, logs the concrete
vtable addresses for slots `0x220`, `0x228`, `0x250`, `0x2e0`, `0x368`, and
`0x3a8`, and attaches temporary hooks to slots `0x220` and `0x228`. The next
in-game route-plotting run should therefore give directly disassemblable RVAs
for the concrete mappin-system methods that receive the tracked destination.
`SetMappinTrackingAlternative` parses two script arguments and calls vtable slot
`0x418` on its receiver. It is probably related to alternate tracking behavior,
but static disassembly has not yet shown it to be the main route planner.
## The High-Level Shape ## The High-Level Shape
@@ -78,9 +122,10 @@ navigation tools. They are not the car GPS. The car GPS system type exists as
`gamegpsGPSSystem`, but its public RTTI surface does not expose planner methods `gamegpsGPSSystem`, but its public RTTI surface does not expose planner methods
that a REDscript mod can override. that a REDscript mod can override.
That means the practical mod surface is data, not algorithm replacement. The That means the practical mod surface is not yet settled. The data-only surface
native `gamegpsGPSSystem` likely builds or queries a graph from traffic lane has been tested and appears insufficient. The next credible surface is a
resources, then renders a GPS line from the selected lane path. RED4ext hook in the native mappin/GPS handoff, after the concrete target
function has been identified.
## Relevant Game Types ## Relevant Game Types
@@ -228,8 +273,8 @@ pavement lanes:
The values are not real-world mph/kph. They are compact game-scale lane speeds. The values are not real-world mph/kph. They are compact game-scale lane speeds.
Because highways are already mostly `17` and normal roads are mostly `15`, a Because highways are already mostly `17` and normal roads are mostly `15`, a
large value like `90` would be reckless. The mod currently uses conservative large value like `90` was expected to be reckless. The first conservative
defaults: archive used:
- highway speed target: `25` - highway speed target: `25`
- road speed floor: `15` - road speed floor: `15`
@@ -241,15 +286,16 @@ That changes `2,519` lanes:
It leaves pavement/crosswalk/blocked/disabled lanes alone. It leaves pavement/crosswalk/blocked/disabled lanes alone.
## What The Mod Actually Does ## What The Data Archive Experiment Did
The mod does not replace the pathfinding algorithm. The data archive experiment did not replace the pathfinding algorithm.
It edits the traffic graph's lane metadata so the native GPS planner has better It edited the traffic graph's lane metadata so the native GPS planner might have
cost inputs. If the planner factors `maxSpeed` into traversal cost in the usual better cost inputs. The original hypothesis was that if the planner factored
way, then a segment with the same physical length but higher `maxSpeed` becomes `maxSpeed` into traversal cost in the usual way, then a segment with the same
cheaper. That nudges the native planner toward highways and high-capacity roads physical length but higher `maxSpeed` would become cheaper. That should have
without having to hook the planner itself. nudged the native planner toward highways and high-capacity roads without
hooking the planner itself.
Conceptually: Conceptually:
@@ -258,9 +304,9 @@ old cost ~= lane.length / lane.maxSpeed
new cost ~= lane.length / patched_lane.maxSpeed new cost ~= lane.length / patched_lane.maxSpeed
``` ```
The actual game formula is native and not exposed in RTTI, so this is an Live testing disproved this as a player-GPS lever. The patched value clearly
inference. The inference is supported by the presence of `maxSpeed`, `length`, reached the game because traffic speed changed, but player GPS route choice did
connectivity data, and GPS-specific lane graph metadata in the same resource. not.
## Why Not Just Write A New A*? ## Why Not Just Write A New A*?
@@ -330,11 +376,11 @@ per lane. Each row has `inLanes` and `outlanes`. Each outgoing connection
contains `laneIndex`, `exitProbabilityCompressed`, `isSharpAngle`, contains `laneIndex`, `exitProbabilityCompressed`, `isSharpAngle`,
`nextLaneEntryPosition`, and `thisLaneExitPosition`. `nextLaneEntryPosition`, and `thisLaneExitPosition`.
There is still no explicit edge cost field. The current best data-only test is There is still no explicit edge cost field. A follow-up data-only test patched
to patch `exitProbabilityCompressed`: strongly favor road/GPSOnly/highway `exitProbabilityCompressed`, strongly favoring road/GPSOnly/highway connections
connections that enter or remain on highways, and lower competing non-highway that enter or remain on highways and lowering competing non-highway exits. That
exits. A prepared experimental archive changes 145 outgoing connection archive changed 145 outgoing connection probabilities across 136 lanes. It also
probabilities across 136 lanes. loaded and ran, but it did not change the tested player GPS routes.
The Oodle library was not available inside the toolbox, so WolvenKit packed with The Oodle library was not available inside the toolbox, so WolvenKit packed with
its Kraken fallback. A vanilla full-resource control archive loaded in game and its Kraken fallback. A vanilla full-resource control archive loaded in game and
@@ -349,15 +395,17 @@ instead.
Better versions of this mod could: Better versions of this mod could:
- build district-specific presets - hook the native destination-tracking handoff after the concrete mappin-system
- patch `all.lane_connections` probabilities around ramps and highway exits method is identified
- inspect `GPSOnly` connector lanes separately - trace the downstream GPS update that consumes the tracked mappin
- compare route output with probability-only archives enabled
- locate any native or baked edge-cost data not exposed in RTTI - locate any native or baked edge-cost data not exposed in RTTI
- inspect `GPSOnly` connector lanes if a later native trace proves they are
consumed by the player GPS
- build district-specific presets if a real data-cost surface is found
- generate a diff report of every changed lane with flags, length, speed, and - generate a diff report of every changed lane with flags, length, speed, and
graph component graph component
- ship multiple archives: conservative, strong, and experimental - ship multiple archives only if a data patch is proven to affect routing
The next practical step is in-game route testing between known bad waypoint The next practical step is one more in-game route-plotting run with the current
pairs, especially cases where vanilla GPS exits highways too early or cuts RED4ext mappin-system slot probe installed. If it logs concrete slot RVAs, the
through surface streets. next static-disassembly target is no longer a guess.
+85 -22
View File
@@ -180,17 +180,70 @@ void LogRed4ext(std::string_view aMessage)
} }
} }
uintptr_t GetImageBase()
{
return reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr));
}
uintptr_t GetImageEnd()
{
const auto imageBase = GetImageBase();
if (!imageBase)
{
return 0;
}
const auto* dos = reinterpret_cast<const IMAGE_DOS_HEADER*>(imageBase);
if (dos->e_magic != IMAGE_DOS_SIGNATURE)
{
return 0;
}
const auto* nt = reinterpret_cast<const IMAGE_NT_HEADERS*>(imageBase + dos->e_lfanew);
if (nt->Signature != IMAGE_NT_SIGNATURE)
{
return 0;
}
return imageBase + nt->OptionalHeader.SizeOfImage;
}
bool IsInImage(const void* aPointer)
{
const auto imageBase = GetImageBase();
const auto imageEnd = GetImageEnd();
const auto address = reinterpret_cast<uintptr_t>(aPointer);
return imageBase && imageEnd && address >= imageBase && address < imageEnd;
}
void AppendPointerWithRva(std::ostringstream& aLine, const char* aLabel, const void* aPointer)
{
aLine << " " << aLabel << "=" << aPointer;
if (IsInImage(aPointer))
{
const auto imageBase = GetImageBase();
const auto address = reinterpret_cast<uintptr_t>(aPointer);
aLine << " " << aLabel << "_rva=0x" << std::hex << (address - imageBase) << std::dec;
}
}
void AppendReturnRva(std::ostringstream& aLine, const void* aReturnAddress)
{
if (IsInImage(aReturnAddress))
{
const auto imageBase = GetImageBase();
const auto address = reinterpret_cast<uintptr_t>(aReturnAddress);
aLine << " ret_rva=0x" << std::hex << (address - imageBase) << std::dec;
}
}
void LogHookCall(const char* aName, uint32_t aCount, void* aThis, void* a2, void* a3, void* a4) void LogHookCall(const char* aName, uint32_t aCount, void* aThis, void* a2, void* a3, void* a4)
{ {
const auto imageBase = reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr));
const auto ret = reinterpret_cast<uintptr_t>(__builtin_return_address(0));
std::ostringstream line; std::ostringstream line;
line << "hook " << aName << " call=" << aCount << " this=" << aThis << " a2=" << a2 << " a3=" << a3 line << "hook " << aName << " call=" << aCount << " this=" << aThis << " a2=" << a2 << " a3=" << a3
<< " a4=" << a4; << " a4=" << a4;
if (ret && imageBase && ret >= imageBase) AppendReturnRva(line, __builtin_return_address(0));
{
line << " ret_rva=0x" << std::hex << (ret - imageBase) << std::dec;
}
LogRed4ext(line.str()); LogRed4ext(line.str());
} }
@@ -222,10 +275,15 @@ void LogMappinSystemSlots(uint32_t aCount, void* aSystem, void* aVtable)
}; };
std::ostringstream line; std::ostringstream line;
line << "mappin-system slots call=" << aCount << " system=" << aSystem << " vtable=" << aVtable line << "mappin-system slots call=" << aCount;
<< " slot220=" << readSlot(0x220) << " slot228=" << readSlot(0x228) AppendPointerWithRva(line, "system", aSystem);
<< " slot250=" << readSlot(0x250) << " slot2e0=" << readSlot(0x2E0) AppendPointerWithRva(line, "vtable", aVtable);
<< " slot368=" << readSlot(0x368) << " slot3a8=" << readSlot(0x3A8); AppendPointerWithRva(line, "slot220", readSlot(0x220));
AppendPointerWithRva(line, "slot228", readSlot(0x228));
AppendPointerWithRva(line, "slot250", readSlot(0x250));
AppendPointerWithRva(line, "slot2e0", readSlot(0x2E0));
AppendPointerWithRva(line, "slot368", readSlot(0x368));
AppendPointerWithRva(line, "slot3a8", readSlot(0x3A8));
LogRed4ext(line.str()); LogRed4ext(line.str());
} }
@@ -453,8 +511,10 @@ void DetourMappinSetTracked(void* aSystem, void* aMappinId)
if (gMappinSetTrackedLogCount < 64) if (gMappinSetTrackedLogCount < 64)
{ {
std::ostringstream line; std::ostringstream line;
line << "hook MappinSystem::SetTracked slot220 call=" << gMappinSetTrackedLogCount line << "hook MappinSystem::SetTracked slot220 call=" << gMappinSetTrackedLogCount;
<< " system=" << aSystem << " mappinId=" << aMappinId; AppendPointerWithRva(line, "system", aSystem);
AppendPointerWithRva(line, "mappinId", aMappinId);
AppendReturnRva(line, __builtin_return_address(0));
LogRed4ext(line.str()); LogRed4ext(line.str());
++gMappinSetTrackedLogCount; ++gMappinSetTrackedLogCount;
} }
@@ -470,8 +530,9 @@ void DetourMappinClearTracked(void* aSystem)
if (gMappinClearTrackedLogCount < 64) if (gMappinClearTrackedLogCount < 64)
{ {
std::ostringstream line; std::ostringstream line;
line << "hook MappinSystem::ClearTracked slot228 call=" << gMappinClearTrackedLogCount line << "hook MappinSystem::ClearTracked slot228 call=" << gMappinClearTrackedLogCount;
<< " system=" << aSystem; AppendPointerWithRva(line, "system", aSystem);
AppendReturnRva(line, __builtin_return_address(0));
LogRed4ext(line.str()); LogRed4ext(line.str());
++gMappinClearTrackedLogCount; ++gMappinClearTrackedLogCount;
} }
@@ -490,7 +551,7 @@ void AttachProbeHook(const char* aName, uintptr_t aRva, void* aDetour, void** aO
return; return;
} }
auto* target = reinterpret_cast<void*>(reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr)) + aRva); auto* target = reinterpret_cast<void*>(GetImageBase() + aRva);
const auto attached = gSdk->hooking->Attach(gHandle, target, aDetour, aOriginal); const auto attached = gSdk->hooking->Attach(gHandle, target, aDetour, aOriginal);
std::ostringstream line; std::ostringstream line;
@@ -518,8 +579,10 @@ void AttachAbsoluteHook(const char* aName, void* aTarget, void* aDetour, void**
const auto attached = gSdk->hooking->Attach(gHandle, aTarget, aDetour, aOriginal); const auto attached = gSdk->hooking->Attach(gHandle, aTarget, aDetour, aOriginal);
std::ostringstream line; std::ostringstream line;
line << "absolute hook attach " << aName << " target=" << aTarget line << "absolute hook attach " << aName;
<< " result=" << (attached ? "ok" : "failed") << " original=" << (aOriginal ? *aOriginal : nullptr); AppendPointerWithRva(line, "target", aTarget);
line << " result=" << (attached ? "ok" : "failed");
AppendPointerWithRva(line, "original", aOriginal ? *aOriginal : nullptr);
LogRed4ext(line.str()); LogRed4ext(line.str());
} }
@@ -530,7 +593,7 @@ void DetachProbeHook(const char* aName, uintptr_t aRva)
return; return;
} }
auto* target = reinterpret_cast<void*>(reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr)) + aRva); auto* target = reinterpret_cast<void*>(GetImageBase() + aRva);
const auto detached = gSdk->hooking->Detach(gHandle, target); const auto detached = gSdk->hooking->Detach(gHandle, target);
std::ostringstream line; std::ostringstream line;
@@ -548,15 +611,15 @@ void DetachAbsoluteHook(const char* aName, void* aTarget)
const auto detached = gSdk->hooking->Detach(gHandle, aTarget); const auto detached = gSdk->hooking->Detach(gHandle, aTarget);
std::ostringstream line; std::ostringstream line;
line << "absolute hook detach " << aName << " target=" << aTarget line << "absolute hook detach " << aName;
<< " result=" << (detached ? "ok" : "failed"); AppendPointerWithRva(line, "target", aTarget);
line << " result=" << (detached ? "ok" : "failed");
LogRed4ext(line.str()); LogRed4ext(line.str());
} }
void InspectAndHookMappinSystem(void* aThis) void InspectAndHookMappinSystem(void* aThis)
{ {
auto* resolver = reinterpret_cast<MappinSystemResolverFunc>( auto* resolver = reinterpret_cast<MappinSystemResolverFunc>(GetImageBase() + kMappinSystemResolverRva);
reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr)) + kMappinSystemResolverRva);
void* system = resolver ? resolver(aThis) : nullptr; void* system = resolver ? resolver(aThis) : nullptr;
auto* vtable = system ? *reinterpret_cast<void***>(system) : nullptr; auto* vtable = system ? *reinterpret_cast<void***>(system) : nullptr;