Add live-tunable GPS solver highway weight

This commit is contained in:
2026-06-26 23:33:20 -05:00
parent 3fa723c3cc
commit 92287bd8e1
12 changed files with 363 additions and 43 deletions
+46 -11
View File
@@ -2,6 +2,41 @@
Current date/time context: 2026-06-26, local timezone America/Chicago. Current date/time context: 2026-06-26, local timezone America/Chicago.
## 2026-06-26 Confirmed Solver-Cost Patch
- We have unequivocally found the player GPS route-cost lever.
- The crashing RED4ext detour on `0x40bb40` has been replaced by an inline
patch at the road-tail block `0x40bb98`. The patch preserves vanilla
`node+0x93` behavior, checks `node+0x88 & 0x4000` for `Highway`, and then
multiplies highway lane cost by a tunable float.
- This inline patch avoids the register-clobber crash from the C++ detour.
Static disassembly showed the optimized caller reusing volatile state after
calling the tiny vanilla multiplier helper.
- The proof build used highway multiplier `0.35` and visibly changed player
map routes. It also caused over-aggressive behavior, including a bad custom
pin hairpin around the city-center roundabout, proving the hook is real but
that `0.35` is too low.
- The vanilla A/B build disabled only the inline multiplier patch and restored
obviously different route choices. User confirmed the custom pin returned to
the straight city-center roundabout route, while the `0.35` build took the
wrong hairpin.
- Current installed build enables the inline patch with default highway
multiplier `0.80` and polls
`red4ext/plugins/EdgeWeightGPS/solver_weights.bin` every 500 ms. The file is
exactly one little-endian float32 highway multiplier.
- The inline patch is deliberately broad across the vanilla road-tail branch:
reaching `0x40bb98` already means vanilla selected road-style modes `0/2/4`,
so we do not hard-gate on `solver+0xc4 == 2`. This avoids creating a manual
drive/autodrive disparity. Pedestrian mode `1` uses a different vanilla
branch and is not changed by this road-tail patch.
- Preset writer:
`tools/write_solver_weights.py`. Preset binaries live in
`presets/solver_weights/`: `vanilla`, `mild`, `default`, `strong`,
`proof-highway-cheap`, and `highway-expensive`.
- Evidence logs:
`logs/EdgeWeightGPS_inline_highway035_changed_routes_1449.log` and
`logs/EdgeWeightGPS_vanilla_ab_2120.log`.
## 2026-06-26 Static Update ## 2026-06-26 Static Update
- Installed RED4ext state is safe: the game plugin directory contains - Installed RED4ext state is safe: the game plugin directory contains
@@ -110,19 +145,19 @@ Important caveat:
- Older DLLs are preserved disabled as: - Older DLLs are preserved disabled as:
`Cyberpunk 2077/red4ext/plugins/EdgeWeightGPS/EdgeWeightGPS.dll.disabled` `Cyberpunk 2077/red4ext/plugins/EdgeWeightGPS/EdgeWeightGPS.dll.disabled`
and `EdgeWeightGPS.dd1423b.dll.disabled`. and `EdgeWeightGPS.dd1423b.dll.disabled`.
- Source defaults now enable the full async solver multiplier hook: - Source defaults enable the inline road-tail patch:
`kEnableGpsMultiplierHooks = true`. `kEnableGpsNodeMultiplierInlinePatch = true`.
- The current behavior patch is deliberately narrow and diagnostic: - Source defaults do not enable the old full-function detour:
`DetourGpsNodeMultiplier` hooks native `0x40bb40`, applies only when `kEnableGpsMultiplierHooks = false`.
`job+0xc4 == 2` (driving route mode), reads lane/node flags from - Current behavior patch is the inline `0x40bb98` code-cave patch. It preserves
`node+0x88`, and multiplies vanilla node edge cost by highway `0.35`, vanilla road cost except for highway lanes, whose cost is multiplied by the
GPSOnly `1.10`, pavement `1.25`, and all other flags `1.0`. current `solver_weights.bin` float. Installed default is `0.80`.
- `0x40bb00` auxiliary/reverse multiplier is hooked only for signature logging; - `solver_weights.bin` lives beside the DLL and is hot-polled every 500 ms.
it is not patched yet.
- The plugin log was truncated immediately before installing this build. - The plugin log was truncated immediately before installing this build.
- Latest archived test log: - Latest archived test log:
`logs/EdgeWeightGPS_spatial_inversion_no_route_change_1845.log`. `logs/EdgeWeightGPS_vanilla_ab_2120.log`.
- Game is cold as of the latest user message. - A cold restart is required after installing a new DLL; changing
`solver_weights.bin` after that is hot-reloaded.
## 2026-06-26 Full Async Solver Findings ## 2026-06-26 Full Async Solver Findings
+16 -6
View File
@@ -66,12 +66,22 @@ Current behavior:
Active build note: Active build note:
- The currently installed build is disabled on disk: - The currently installed build is active as
`EdgeWeightGPS.dll.disabled`. There is no active `EdgeWeightGPS.dll` in the `red4ext/plugins/EdgeWeightGPS/EdgeWeightGPS.dll`.
game plugin directory. - Source defaults enable the stable inline solver-cost patch at `0x40bb98`
- Source defaults are still read-only for route behavior. They disable provider (`kEnableGpsNodeMultiplierInlinePatch = true`) and keep the old full-function
class multiplier overrides and the spatial edge-cost patch, while keeping GPS `0x40bb40` C++ detour disabled (`kEnableGpsMultiplierHooks = false`).
query lifecycle tracing available for probe builds. - The inline patch preserves vanilla road cost except for highway lanes in the
road-tail branch. Highway lanes are multiplied by the live value in
`solver_weights.bin`, defaulting to `0.80`.
- The patch is intentionally not hard-gated on `solver+0xc4 == 2`; the vanilla
branch already limits it to road-style modes `0/2/4`, which keeps manual
drive and autodrive-like callers from diverging while leaving pedestrian mode
`1` alone.
- `solver_weights.bin` lives beside the DLL and is exactly one little-endian
float32. The plugin polls it every 500 ms from the Running game-state update.
`tools/write_solver_weights.py` writes the installed file or generates preset
binaries under `presets/solver_weights/`.
- The async route-job hooks are split behind `kEnableGpsRouteJobLifecycleHooks` - The async route-job hooks are split behind `kEnableGpsRouteJobLifecycleHooks`
and are disabled in the active probe; this keeps the log focused on and are disabled in the active probe; this keeps the log focused on
`0x70a42c` submissions, `0x7094b8` result records, and local `0x70a42c` submissions, `0x7094b8` result records, and local
+44 -14
View File
@@ -980,32 +980,62 @@ the original symptom: the solver is weighted enough to avoid obviously invalid
surfaces, but it has no strong reason to stay on highway corridors for long surfaces, but it has no strong reason to stay on highway corridors for long
vehicle trips. vehicle trips.
The current RED4ext prototype therefore hooks `0x40bb40` and only changes The first RED4ext prototype tried to hook `0x40bb40` as a normal C++ detour.
driving-mode calls (`solver/job + 0xc4 == 2`). It reads flags from That found the correct function but crashed during load-time GPS warmup because
`node+0x88`, leaves vanilla results intact for ordinary roads and unknown the optimized caller expects the tiny vanilla helper not to disturb volatile
surfaces, and applies this diagnostic multiplier set: register state used immediately afterward.
The stable prototype now patches inline at the road-tail block `0x40bb98`
instead. Reaching that block already means the vanilla function selected one of
the road-style modes `0/2/4`; pedestrian mode `1` uses a different branch. The
patch preserves vanilla `node+0x93` behavior, checks `node+0x88 & 0x4000`, and
applies an extra multiplier only for highway lanes.
The proof multiplier was:
```text ```text
Highway 0.35 Highway 0.35
GPSOnly 1.10
Pavement 1.25
Other 1.00
``` ```
`0x40bb00` is currently hooked only for signature logging. If the node Live A/B testing confirmed this is the decisive world-map route-cost lever.
multiplier test moves routes but produces weird reverse/turnaround behavior, With `0.35`, routes visibly changed and a custom pin around the city-center
the next step is to tune or gate the auxiliary multiplier as well. roundabout took an obviously bad highway/ramp hairpin. With the inline patch
disabled, the same route returned to the straight vanilla path through the
roundabout.
The current installed build uses a default highway multiplier of `0.80` and
hot-reloads `solver_weights.bin` from the plugin directory. The file is exactly
one little-endian float32. `tools/write_solver_weights.py` writes the installed
file and can generate named presets:
```text
vanilla 1.00
mild 0.90
default 0.80
strong 0.70
proof-highway-cheap 0.35
highway-expensive 3.00
```
The patch intentionally does not hard-gate on `solver+0xc4 == 2`, because
autodrive-like local vehicle navigation may use another road-style mode. Since
the patch lives in the vanilla road-tail branch, it applies to road-style modes
`0/2/4` and still leaves pedestrian mode `1` alone.
`0x40bb00` is not patched yet. If tuned highway weights still produce
reverse/turnaround artifacts, inspect or patch that auxiliary multiplier so
reverse/special progress branches receive a compatible policy.
## Future Improvements ## Future Improvements
Better versions of this mod could: Better versions of this mod could:
- tune the `0x40bb40` solver multiplier after live A/B testing confirms visible - tune the `0x40bb98` inline highway multiplier from the current `0.80` default
route changes using live `solver_weights.bin` presets
- inspect and, if needed, patch `0x40bb00` so reverse/special progress branches - inspect and, if needed, patch `0x40bb00` so reverse/special progress branches
receive the same road-class policy as forward expansion receive the same road-class policy as forward expansion
- add a tiny live-tuning file for the solver multipliers, similar to the earlier - extend `solver_weights.bin` beyond one highway float only if GPSOnly/pavement
`spatial_weights.bin`, once the hook is confirmed stable tuning becomes necessary
- replace the crashing `0x8d46cc` detour with a narrower inline patch or a - replace the crashing `0x8d46cc` detour with a narrower inline patch or a
safer caller-side hook safer caller-side hook
- decode enough of the `VAND` graph to join navigation points to traffic lane - decode enough of the `VAND` graph to join navigation points to traffic lane
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
ヘフL?
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
fff?
@@ -0,0 +1 @@
33>
+1
View File
@@ -0,0 +1 @@
333?
Binary file not shown.
+142 -12
View File
@@ -102,7 +102,7 @@ constexpr bool kEnableVerboseMappinRouteState = false;
constexpr bool kEnableGpsMultiplierHooks = false; constexpr bool kEnableGpsMultiplierHooks = false;
constexpr bool kEnableGpsAuxMultiplierHook = false; constexpr bool kEnableGpsAuxMultiplierHook = false;
constexpr bool kEnableGpsMultiplierSignatureTrace = false; constexpr bool kEnableGpsMultiplierSignatureTrace = false;
constexpr bool kEnableGpsNodeMultiplierInlinePatch = false; constexpr bool kEnableGpsNodeMultiplierInlinePatch = true;
constexpr bool kEnableGpsCostTablePatch = false; constexpr bool kEnableGpsCostTablePatch = false;
constexpr bool kEnableGpsProviderClassPatch = false; constexpr bool kEnableGpsProviderClassPatch = false;
constexpr bool kEnableGpsProviderClassPatchTrace = false; constexpr bool kEnableGpsProviderClassPatchTrace = false;
@@ -213,7 +213,7 @@ constexpr uint16_t kTrafficLaneFlagNoAiDriving = 0x2000;
constexpr uint16_t kTrafficLaneFlagHighway = 0x4000; constexpr uint16_t kTrafficLaneFlagHighway = 0x4000;
constexpr uint16_t kTrafficLaneFlagNoAutodrive = 0x8000; constexpr uint16_t kTrafficLaneFlagNoAutodrive = 0x8000;
constexpr uintptr_t kMinimumReadableAddress = 0x10000; constexpr uintptr_t kMinimumReadableAddress = 0x10000;
constexpr float kGpsSolverHighwayMultiplier = 0.35f; constexpr float kGpsSolverHighwayMultiplierDefault = 0.80f;
constexpr float kGpsSolverGpsOnlyMultiplier = 1.10f; constexpr float kGpsSolverGpsOnlyMultiplier = 1.10f;
constexpr float kGpsSolverPavementMultiplier = 1.25f; constexpr float kGpsSolverPavementMultiplier = 1.25f;
constexpr size_t kGpsNodeMultiplierRoadTailPatchSize = 14; constexpr size_t kGpsNodeMultiplierRoadTailPatchSize = 14;
@@ -226,6 +226,7 @@ constexpr float kGpsSpatialRoadMultiplier = 1.0f;
constexpr float kGpsSpatialGpsOnlyMultiplier = 1.20f; constexpr float kGpsSpatialGpsOnlyMultiplier = 1.20f;
constexpr float kGpsSpatialPavementMultiplier = 1.35f; constexpr float kGpsSpatialPavementMultiplier = 1.35f;
constexpr float kGpsSpatialUnknownMultiplier = 1.05f; constexpr float kGpsSpatialUnknownMultiplier = 1.05f;
constexpr uint64_t kGpsSolverWeightsReloadIntervalMs = 500;
constexpr uint64_t kGpsSpatialWeightsReloadIntervalMs = 500; constexpr uint64_t kGpsSpatialWeightsReloadIntervalMs = 500;
struct GpsProviderClassOverride struct GpsProviderClassOverride
@@ -327,10 +328,12 @@ uint32_t gGpsResolveTraceWindowSerial = 0;
std::array<uint32_t, 64> gGpsEdgeCostClassLogCounts{}; std::array<uint32_t, 64> gGpsEdgeCostClassLogCounts{};
std::array<uint8_t, kGpsNodeMultiplierRoadTailPatchSize> gGpsNodeMultiplierRoadTailOriginal{}; std::array<uint8_t, kGpsNodeMultiplierRoadTailPatchSize> gGpsNodeMultiplierRoadTailOriginal{};
void* gGpsNodeMultiplierRoadTailCave = nullptr; void* gGpsNodeMultiplierRoadTailCave = nullptr;
uint8_t* gGpsNodeMultiplierRoadTailHighwayConstant = nullptr;
bool gGpsNodeMultiplierRoadTailPatchApplied = false; bool gGpsNodeMultiplierRoadTailPatchApplied = false;
std::mutex gGpsCostLogMutex; std::mutex gGpsCostLogMutex;
std::mutex gGpsAsyncLogMutex; std::mutex gGpsAsyncLogMutex;
std::mutex gGpsCostTablePatchMutex; std::mutex gGpsCostTablePatchMutex;
std::mutex gGpsSolverWeightsMutex;
uint64_t gLogStartTick = 0; uint64_t gLogStartTick = 0;
uint64_t gLogFrequency = 0; uint64_t gLogFrequency = 0;
void* gMappinSetTrackedTarget = nullptr; void* gMappinSetTrackedTarget = nullptr;
@@ -569,9 +572,13 @@ std::array<std::atomic<float>, 5> gGpsSpatialMultipliers = {
kGpsSpatialPavementMultiplier, kGpsSpatialPavementMultiplier,
kGpsSpatialUnknownMultiplier, kGpsSpatialUnknownMultiplier,
}; };
std::atomic<float> gGpsSolverHighwayMultiplier{kGpsSolverHighwayMultiplierDefault};
std::mutex gGpsSpatialWeightsMutex; std::mutex gGpsSpatialWeightsMutex;
std::atomic<uint64_t> gGpsSolverWeightsNextReloadCheckMs = 0;
std::atomic<uint64_t> gGpsSpatialWeightsNextReloadCheckMs = 0; std::atomic<uint64_t> gGpsSpatialWeightsNextReloadCheckMs = 0;
std::filesystem::file_time_type gGpsSolverWeightsLastWriteTime{};
std::filesystem::file_time_type gGpsSpatialWeightsLastWriteTime{}; std::filesystem::file_time_type gGpsSpatialWeightsLastWriteTime{};
bool gGpsSolverWeightsFileKnown = false;
bool gGpsSpatialWeightsFileKnown = false; bool gGpsSpatialWeightsFileKnown = false;
uintptr_t GetImageBase(); uintptr_t GetImageBase();
@@ -598,6 +605,11 @@ std::filesystem::path GetSpatialWeightsPath()
return GetPluginDirectory() / L"spatial_weights.bin"; return GetPluginDirectory() / L"spatial_weights.bin";
} }
std::filesystem::path GetSolverWeightsPath()
{
return GetPluginDirectory() / L"solver_weights.bin";
}
uint64_t GetElapsedMs() uint64_t GetElapsedMs()
{ {
if (!gLogStartTick || !gLogFrequency) if (!gLogStartTick || !gLogFrequency)
@@ -652,6 +664,103 @@ void WriteFloat(uint8_t* aTarget, float aValue)
std::memcpy(aTarget, &aValue, sizeof(aValue)); std::memcpy(aTarget, &aValue, sizeof(aValue));
} }
bool IsUsableSolverMultiplier(float aValue)
{
return std::isfinite(aValue) && aValue > 0.0f && aValue < 20.0f;
}
void StoreGpsSolverHighwayMultiplier(float aValue)
{
gGpsSolverHighwayMultiplier.store(aValue, std::memory_order_relaxed);
if (gGpsNodeMultiplierRoadTailHighwayConstant)
{
WriteFloat(gGpsNodeMultiplierRoadTailHighwayConstant, aValue);
FlushInstructionCache(GetCurrentProcess(), gGpsNodeMultiplierRoadTailHighwayConstant, sizeof(aValue));
}
}
void LogGpsSolverHighwayMultiplier(std::string_view aPrefix, float aValue)
{
std::ostringstream line;
line << aPrefix << " highway=" << aValue;
LogRed4ext(line.str());
}
void MaybeReloadGpsSolverWeights(bool aForce = false)
{
if (!kEnableGpsNodeMultiplierInlinePatch)
{
return;
}
const auto nowMs = GetElapsedMs();
if (!aForce && nowMs < gGpsSolverWeightsNextReloadCheckMs.load(std::memory_order_relaxed))
{
return;
}
std::lock_guard lock(gGpsSolverWeightsMutex);
if (!aForce && nowMs < gGpsSolverWeightsNextReloadCheckMs.load(std::memory_order_relaxed))
{
return;
}
gGpsSolverWeightsNextReloadCheckMs.store(nowMs + kGpsSolverWeightsReloadIntervalMs, std::memory_order_relaxed);
const auto path = GetSolverWeightsPath();
std::error_code error;
const auto writeTime = std::filesystem::last_write_time(path, error);
if (error)
{
if (aForce || gGpsSolverWeightsFileKnown)
{
gGpsSolverWeightsFileKnown = false;
std::ostringstream line;
line << "solver weights file unavailable path=" << path.string()
<< " using highway=" << gGpsSolverHighwayMultiplier.load(std::memory_order_relaxed);
LogRed4ext(line.str());
}
return;
}
if (!aForce && gGpsSolverWeightsFileKnown && writeTime == gGpsSolverWeightsLastWriteTime)
{
return;
}
float highwayMultiplier = 0.0f;
std::ifstream input(path, std::ios::binary);
input.read(reinterpret_cast<char*>(&highwayMultiplier), static_cast<std::streamsize>(sizeof(highwayMultiplier)));
if (input.gcount() != static_cast<std::streamsize>(sizeof(highwayMultiplier)))
{
std::ostringstream line;
line << "solver weights reload skipped path=" << path.string() << " bytes=" << input.gcount()
<< " expected=" << sizeof(highwayMultiplier);
LogRed4ext(line.str());
gGpsSolverWeightsFileKnown = true;
gGpsSolverWeightsLastWriteTime = writeTime;
return;
}
if (!IsUsableSolverMultiplier(highwayMultiplier))
{
std::ostringstream line;
line << "solver weights reload skipped path=" << path.string() << " invalidHighway=" << highwayMultiplier;
LogRed4ext(line.str());
gGpsSolverWeightsFileKnown = true;
gGpsSolverWeightsLastWriteTime = writeTime;
return;
}
StoreGpsSolverHighwayMultiplier(highwayMultiplier);
gGpsSolverWeightsFileKnown = true;
gGpsSolverWeightsLastWriteTime = writeTime;
std::ostringstream line;
line << "solver weights reloaded path=" << path.string() << " highway=" << highwayMultiplier;
LogRed4ext(line.str());
}
bool InstallGpsNodeMultiplierInlinePatch() bool InstallGpsNodeMultiplierInlinePatch()
{ {
if (!kEnableGpsNodeMultiplierInlinePatch || gGpsNodeMultiplierRoadTailPatchApplied) if (!kEnableGpsNodeMultiplierInlinePatch || gGpsNodeMultiplierRoadTailPatchApplied)
@@ -659,7 +768,7 @@ bool InstallGpsNodeMultiplierInlinePatch()
return true; return true;
} }
constexpr size_t kCaveSize = 64; constexpr size_t kCaveSize = 80;
auto* cave = static_cast<uint8_t*>( auto* cave = static_cast<uint8_t*>(
VirtualAlloc(nullptr, kCaveSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)); VirtualAlloc(nullptr, kCaveSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE));
if (!cave) if (!cave)
@@ -668,8 +777,9 @@ bool InstallGpsNodeMultiplierInlinePatch()
return false; return false;
} }
// Replaces the road-tail at 0x40bb98. This preserves the vanilla node93 // Replaces the road-tail at 0x40bb98. Reaching this block already means
// multiplier, then applies the highway multiplier before returning. // vanilla selected one of the road-style solver modes (0/2/4), so keep
// the change broad enough for manual GPS and autodrive-like callers.
size_t offset = 0; size_t offset = 0;
const auto emit = [&](std::initializer_list<uint8_t> aBytes) { const auto emit = [&](std::initializer_list<uint8_t> aBytes) {
for (auto byte : aBytes) for (auto byte : aBytes)
@@ -691,7 +801,7 @@ bool InstallGpsNodeMultiplierInlinePatch()
emit({0xF3, 0x0F, 0x59, 0x05, 0x00, 0x00, 0x00, 0x00}); // mulss xmm0, [rip+disp32] emit({0xF3, 0x0F, 0x59, 0x05, 0x00, 0x00, 0x00, 0x00}); // mulss xmm0, [rip+disp32]
emit({0xC3}); // ret emit({0xC3}); // ret
while (offset < 40) while ((offset % alignof(float)) != 0)
{ {
cave[offset++] = 0x90; cave[offset++] = 0x90;
} }
@@ -700,7 +810,8 @@ bool InstallGpsNodeMultiplierInlinePatch()
WriteFloat(cave + offset, 1.1f); WriteFloat(cave + offset, 1.1f);
offset += sizeof(float); offset += sizeof(float);
const auto highwayConstantOffset = offset; const auto highwayConstantOffset = offset;
WriteFloat(cave + offset, kGpsSolverHighwayMultiplier); const auto highwayMultiplier = gGpsSolverHighwayMultiplier.load(std::memory_order_relaxed);
WriteFloat(cave + offset, highwayMultiplier);
offset += sizeof(float); offset += sizeof(float);
const auto caveAddress = reinterpret_cast<uintptr_t>(cave); const auto caveAddress = reinterpret_cast<uintptr_t>(cave);
@@ -743,12 +854,13 @@ bool InstallGpsNodeMultiplierInlinePatch()
VirtualProtect(target, sizeof(patch), oldProtect, &ignoredProtect); VirtualProtect(target, sizeof(patch), oldProtect, &ignoredProtect);
gGpsNodeMultiplierRoadTailCave = cave; gGpsNodeMultiplierRoadTailCave = cave;
gGpsNodeMultiplierRoadTailHighwayConstant = cave + highwayConstantOffset;
gGpsNodeMultiplierRoadTailPatchApplied = true; gGpsNodeMultiplierRoadTailPatchApplied = true;
std::ostringstream line; std::ostringstream line;
line << "gps-node-multiplier-inline-patch applied target_rva=0x" << std::hex line << "gps-node-multiplier-inline-patch applied target_rva=0x" << std::hex
<< kGpsNodeMultiplierRoadTailRva << " cave=" << static_cast<void*>(cave) << kGpsNodeMultiplierRoadTailRva << " cave=" << static_cast<void*>(cave)
<< " highwayMultiplier=" << std::dec << kGpsSolverHighwayMultiplier; << " highwayMultiplier=" << std::dec << highwayMultiplier << " modes=road-tail";
LogRed4ext(line.str()); LogRed4ext(line.str());
return true; return true;
} }
@@ -782,6 +894,7 @@ void RemoveGpsNodeMultiplierInlinePatch()
gGpsNodeMultiplierRoadTailCave = nullptr; gGpsNodeMultiplierRoadTailCave = nullptr;
} }
gGpsNodeMultiplierRoadTailHighwayConstant = nullptr;
gGpsNodeMultiplierRoadTailPatchApplied = false; gGpsNodeMultiplierRoadTailPatchApplied = false;
} }
@@ -2338,16 +2451,21 @@ bool TryReadUint32FieldValue(void* aObject, uintptr_t aOffset, uint32_t& aValue)
aValue); aValue);
} }
bool IsGpsRoadStyleMode(uint32_t aMode)
{
return aMode == 0 || aMode == 2 || aMode == 4;
}
bool IsDrivingRouteJob(void* aRouteSystem, void* aActiveEntry) bool IsDrivingRouteJob(void* aRouteSystem, void* aActiveEntry)
{ {
uint32_t mode = 0; uint32_t mode = 0;
if (TryReadUint32FieldValue(aActiveEntry, 0x80, mode) && mode == 2) if (TryReadUint32FieldValue(aActiveEntry, 0x80, mode) && IsGpsRoadStyleMode(mode))
{ {
return true; return true;
} }
auto* job = ReadPointerField(aRouteSystem, 0x90D0); auto* job = ReadPointerField(aRouteSystem, 0x90D0);
return TryReadUint32FieldValue(job, 0xC4, mode) && mode == 2; return TryReadUint32FieldValue(job, 0xC4, mode) && IsGpsRoadStyleMode(mode);
} }
void AppendFloatArray(std::ostringstream& aLine, const char* aLabel, const float* aValues, size_t aCount) void AppendFloatArray(std::ostringstream& aLine, const char* aLabel, const float* aValues, size_t aCount)
@@ -4339,7 +4457,7 @@ float GpsSolverNodeWeightMultiplier(uint16_t aFlags)
{ {
if ((aFlags & kTrafficLaneFlagHighway) != 0) if ((aFlags & kTrafficLaneFlagHighway) != 0)
{ {
return kGpsSolverHighwayMultiplier; return gGpsSolverHighwayMultiplier.load(std::memory_order_relaxed);
} }
if ((aFlags & kTrafficLaneFlagGpsOnly) != 0) if ((aFlags & kTrafficLaneFlagGpsOnly) != 0)
{ {
@@ -5084,7 +5202,7 @@ float DetourGpsNodeMultiplier(void* aJob, void* aNode)
result = ReadGpsSolverNonRoadMultiplier(retry); result = ReadGpsSolverNonRoadMultiplier(retry);
} }
if (mode == 2 && std::isfinite(result)) if (IsGpsRoadStyleMode(mode) && std::isfinite(result))
{ {
result *= GpsSolverNodeWeightMultiplier(nodeFlags); result *= GpsSolverNodeWeightMultiplier(nodeFlags);
} }
@@ -5352,6 +5470,8 @@ bool OnRunningUpdate(void* aApp)
++gUpdateLogCount; ++gUpdateLogCount;
} }
MaybeReloadGpsSolverWeights();
return false; return false;
} }
@@ -5420,6 +5540,16 @@ RED4EXT_C_EXPORT bool RED4EXT_CALL Main(RED4ext::v1::PluginHandle aHandle, RED4e
LogGpsSpatialMultipliers("spatial weights defaults", values); LogGpsSpatialMultipliers("spatial weights defaults", values);
} }
if (kEnableGpsNodeMultiplierInlinePatch)
{
std::ostringstream line;
line << "solver weights path=" << GetSolverWeightsPath().string();
LogRed4ext(line.str());
MaybeReloadGpsSolverWeights(true);
LogGpsSolverHighwayMultiplier("solver weights active",
gGpsSolverHighwayMultiplier.load(std::memory_order_relaxed));
}
InstallGpsNodeMultiplierInlinePatch(); InstallGpsNodeMultiplierInlinePatch();
if (kEnableGpsQueryLifecycleHooks) if (kEnableGpsQueryLifecycleHooks)
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env python3
"""Write EdgeWeightGPS solver highway presets as one raw float32 value."""
from __future__ import annotations
import argparse
import struct
from pathlib import Path
DEFAULT_INSTALL_PATH = Path(
"/var/home/salt/.var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/common/"
"Cyberpunk 2077/red4ext/plugins/EdgeWeightGPS/solver_weights.bin"
)
PRESETS: dict[str, float] = {
"vanilla": 1.00,
"default": 0.80,
"mild": 0.90,
"strong": 0.70,
"proof-highway-cheap": 0.35,
"highway-expensive": 3.00,
}
def parse_multiplier(text: str) -> float:
value = float(text)
if not 0.0 < value < 20.0:
raise argparse.ArgumentTypeError("multiplier must be greater than 0 and less than 20")
return value
def write_weight(path: Path, value: float) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(struct.pack("<f", value))
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("preset", nargs="?", choices=sorted(PRESETS), default="default")
parser.add_argument("--value", type=parse_multiplier, help="explicit highway multiplier")
parser.add_argument("--output", type=Path, default=DEFAULT_INSTALL_PATH)
parser.add_argument("--preset-dir", type=Path, help="write every named preset into this directory")
args = parser.parse_args()
if args.preset_dir:
for name, value in PRESETS.items():
path = args.preset_dir / f"{name}.bin"
write_weight(path, value)
print(f"{path}: {value}")
return 0
value = args.value if args.value is not None else PRESETS[args.preset]
write_weight(args.output, value)
print(f"{args.output}: {value}")
return 0
if __name__ == "__main__":
raise SystemExit(main())