Log mappin slot RVAs
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user