diff --git a/docs/traffic-system-debrief.md b/docs/traffic-system-debrief.md index 9208963..3027d37 100644 --- a/docs/traffic-system-debrief.md +++ b/docs/traffic-system-debrief.md @@ -17,11 +17,14 @@ and lane connection probabilities. Live tests have not supported that: - probing the obvious `TrafficSystem_Pathfinding` and `StartPathfinding`/`StopPathfinding` string-xref wrappers did not fire during 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 -native GPS/mappin query path, with traffic data used by traffic simulation and -possibly as an input to cooked GPS data, but not as a live route-cost graph that -can be steered by simple resource weighting. +native world-map mappin tracking path, which then updates native GPS state +downstream. Traffic lane data is still likely used somewhere in the final route +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 @@ -50,8 +53,8 @@ the wrapper. `UpdateGPSQueryHelper` appears related but takes a request type and target. Its return value is also used as a success flag. -The next RED4ext probe should hook these helpers with return-value-preserving -signatures, not the registration thunks: +These helpers were hooked with return-value-preserving signatures, not the +registration thunks: ```cpp 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 -calling convention. If either helper fires exactly when a map destination is -selected, it is a much better path into the actual route computation than the -traffic-pathfinding probes. +calling convention. + +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 @@ -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 that a REDscript mod can override. -That means the practical mod surface is data, not algorithm replacement. The -native `gamegpsGPSSystem` likely builds or queries a graph from traffic lane -resources, then renders a GPS line from the selected lane path. +That means the practical mod surface is not yet settled. The data-only surface +has been tested and appears insufficient. The next credible surface is a +RED4ext hook in the native mappin/GPS handoff, after the concrete target +function has been identified. ## Relevant Game Types @@ -228,8 +273,8 @@ pavement lanes: 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 -large value like `90` would be reckless. The mod currently uses conservative -defaults: +large value like `90` was expected to be reckless. The first conservative +archive used: - highway speed target: `25` - road speed floor: `15` @@ -241,15 +286,16 @@ That changes `2,519` lanes: 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 -cost inputs. If the planner factors `maxSpeed` into traversal cost in the usual -way, then a segment with the same physical length but higher `maxSpeed` becomes -cheaper. That nudges the native planner toward highways and high-capacity roads -without having to hook the planner itself. +It edited the traffic graph's lane metadata so the native GPS planner might have +better cost inputs. The original hypothesis was that if the planner factored +`maxSpeed` into traversal cost in the usual way, then a segment with the same +physical length but higher `maxSpeed` would become cheaper. That should have +nudged the native planner toward highways and high-capacity roads without +hooking the planner itself. Conceptually: @@ -258,9 +304,9 @@ old cost ~= lane.length / lane.maxSpeed new cost ~= lane.length / patched_lane.maxSpeed ``` -The actual game formula is native and not exposed in RTTI, so this is an -inference. The inference is supported by the presence of `maxSpeed`, `length`, -connectivity data, and GPS-specific lane graph metadata in the same resource. +Live testing disproved this as a player-GPS lever. The patched value clearly +reached the game because traffic speed changed, but player GPS route choice did +not. ## 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`, `nextLaneEntryPosition`, and `thisLaneExitPosition`. -There is still no explicit edge cost field. The current best data-only test is -to patch `exitProbabilityCompressed`: strongly favor road/GPSOnly/highway -connections that enter or remain on highways, and lower competing non-highway -exits. A prepared experimental archive changes 145 outgoing connection -probabilities across 136 lanes. +There is still no explicit edge cost field. A follow-up data-only test patched +`exitProbabilityCompressed`, strongly favoring road/GPSOnly/highway connections +that enter or remain on highways and lowering competing non-highway exits. That +archive changed 145 outgoing connection probabilities across 136 lanes. It also +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 its Kraken fallback. A vanilla full-resource control archive loaded in game and @@ -349,15 +395,17 @@ instead. Better versions of this mod could: -- build district-specific presets -- patch `all.lane_connections` probabilities around ramps and highway exits -- inspect `GPSOnly` connector lanes separately -- compare route output with probability-only archives enabled +- hook the native destination-tracking handoff after the concrete mappin-system + method is identified +- trace the downstream GPS update that consumes the tracked mappin - 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 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 -pairs, especially cases where vanilla GPS exits highways too early or cuts -through surface streets. +The next practical step is one more in-game route-plotting run with the current +RED4ext mappin-system slot probe installed. If it logs concrete slot RVAs, the +next static-disassembly target is no longer a guess. diff --git a/red4ext/EdgeWeightGPS/src/Main.cpp b/red4ext/EdgeWeightGPS/src/Main.cpp index 539956f..1ab8085 100644 --- a/red4ext/EdgeWeightGPS/src/Main.cpp +++ b/red4ext/EdgeWeightGPS/src/Main.cpp @@ -180,17 +180,70 @@ void LogRed4ext(std::string_view aMessage) } } +uintptr_t GetImageBase() +{ + return reinterpret_cast(GetModuleHandleW(nullptr)); +} + +uintptr_t GetImageEnd() +{ + const auto imageBase = GetImageBase(); + if (!imageBase) + { + return 0; + } + + const auto* dos = reinterpret_cast(imageBase); + if (dos->e_magic != IMAGE_DOS_SIGNATURE) + { + return 0; + } + + const auto* nt = reinterpret_cast(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(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(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(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) { - const auto imageBase = reinterpret_cast(GetModuleHandleW(nullptr)); - const auto ret = reinterpret_cast(__builtin_return_address(0)); std::ostringstream line; line << "hook " << aName << " call=" << aCount << " this=" << aThis << " a2=" << a2 << " a3=" << a3 << " a4=" << a4; - if (ret && imageBase && ret >= imageBase) - { - line << " ret_rva=0x" << std::hex << (ret - imageBase) << std::dec; - } + AppendReturnRva(line, __builtin_return_address(0)); LogRed4ext(line.str()); } @@ -222,10 +275,15 @@ void LogMappinSystemSlots(uint32_t aCount, void* aSystem, void* aVtable) }; std::ostringstream line; - line << "mappin-system slots call=" << aCount << " system=" << aSystem << " vtable=" << aVtable - << " slot220=" << readSlot(0x220) << " slot228=" << readSlot(0x228) - << " slot250=" << readSlot(0x250) << " slot2e0=" << readSlot(0x2E0) - << " slot368=" << readSlot(0x368) << " slot3a8=" << readSlot(0x3A8); + line << "mappin-system slots call=" << aCount; + AppendPointerWithRva(line, "system", aSystem); + AppendPointerWithRva(line, "vtable", aVtable); + 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()); } @@ -453,8 +511,10 @@ void DetourMappinSetTracked(void* aSystem, void* aMappinId) if (gMappinSetTrackedLogCount < 64) { std::ostringstream line; - line << "hook MappinSystem::SetTracked slot220 call=" << gMappinSetTrackedLogCount - << " system=" << aSystem << " mappinId=" << aMappinId; + line << "hook MappinSystem::SetTracked slot220 call=" << gMappinSetTrackedLogCount; + AppendPointerWithRva(line, "system", aSystem); + AppendPointerWithRva(line, "mappinId", aMappinId); + AppendReturnRva(line, __builtin_return_address(0)); LogRed4ext(line.str()); ++gMappinSetTrackedLogCount; } @@ -470,8 +530,9 @@ void DetourMappinClearTracked(void* aSystem) if (gMappinClearTrackedLogCount < 64) { std::ostringstream line; - line << "hook MappinSystem::ClearTracked slot228 call=" << gMappinClearTrackedLogCount - << " system=" << aSystem; + line << "hook MappinSystem::ClearTracked slot228 call=" << gMappinClearTrackedLogCount; + AppendPointerWithRva(line, "system", aSystem); + AppendReturnRva(line, __builtin_return_address(0)); LogRed4ext(line.str()); ++gMappinClearTrackedLogCount; } @@ -490,7 +551,7 @@ void AttachProbeHook(const char* aName, uintptr_t aRva, void* aDetour, void** aO return; } - auto* target = reinterpret_cast(reinterpret_cast(GetModuleHandleW(nullptr)) + aRva); + auto* target = reinterpret_cast(GetImageBase() + aRva); const auto attached = gSdk->hooking->Attach(gHandle, target, aDetour, aOriginal); 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); std::ostringstream line; - line << "absolute hook attach " << aName << " target=" << aTarget - << " result=" << (attached ? "ok" : "failed") << " original=" << (aOriginal ? *aOriginal : nullptr); + line << "absolute hook attach " << aName; + AppendPointerWithRva(line, "target", aTarget); + line << " result=" << (attached ? "ok" : "failed"); + AppendPointerWithRva(line, "original", aOriginal ? *aOriginal : nullptr); LogRed4ext(line.str()); } @@ -530,7 +593,7 @@ void DetachProbeHook(const char* aName, uintptr_t aRva) return; } - auto* target = reinterpret_cast(reinterpret_cast(GetModuleHandleW(nullptr)) + aRva); + auto* target = reinterpret_cast(GetImageBase() + aRva); const auto detached = gSdk->hooking->Detach(gHandle, target); std::ostringstream line; @@ -548,15 +611,15 @@ void DetachAbsoluteHook(const char* aName, void* aTarget) const auto detached = gSdk->hooking->Detach(gHandle, aTarget); std::ostringstream line; - line << "absolute hook detach " << aName << " target=" << aTarget - << " result=" << (detached ? "ok" : "failed"); + line << "absolute hook detach " << aName; + AppendPointerWithRva(line, "target", aTarget); + line << " result=" << (detached ? "ok" : "failed"); LogRed4ext(line.str()); } void InspectAndHookMappinSystem(void* aThis) { - auto* resolver = reinterpret_cast( - reinterpret_cast(GetModuleHandleW(nullptr)) + kMappinSystemResolverRva); + auto* resolver = reinterpret_cast(GetImageBase() + kMappinSystemResolverRva); void* system = resolver ? resolver(aThis) : nullptr; auto* vtable = system ? *reinterpret_cast(system) : nullptr;