Add live spatial weight tuning
This commit is contained in:
@@ -71,6 +71,15 @@ Current date/time context: 2026-06-26, local timezone America/Chicago.
|
||||
`toolbox run -c 2077 tools/build_red4ext_shim.sh`.
|
||||
- The resulting DLL remains uninstalled. Game install still has only
|
||||
`EdgeWeightGPS.dll.disabled`, not an active plugin DLL.
|
||||
- Follow-up live-tuning support adds
|
||||
`red4ext/plugins/EdgeWeightGPS/spatial_weights.bin`: five little-endian
|
||||
float32 values in highway, road, GPSOnly, pavement, unknown order. The plugin
|
||||
polls this file on route-producer entry, so changing it and replotting a route
|
||||
applies new weights without rebuild/relaunch.
|
||||
- `tools/write_spatial_weights.py` can write the active file or generate
|
||||
presets. Generated presets live in `work/spatial-weights/`:
|
||||
`vanilla.bin`, `current.bin`, `highway-free.bin`,
|
||||
`highway-expensive.bin`, and `surface-extreme.bin`.
|
||||
|
||||
Important caveat:
|
||||
|
||||
|
||||
@@ -81,6 +81,15 @@ Active build note:
|
||||
`GPSEdgeCost` (`0x44f838`), samples a generated traffic-lane spatial grid
|
||||
using the search-state coordinates, and applies highway/road/pavement
|
||||
multipliers to returned edge costs.
|
||||
- When the spatial patch is enabled, the plugin hot-reloads
|
||||
`spatial_weights.bin` from the same directory as the DLL. The file is exactly
|
||||
five little-endian float32 values in this order:
|
||||
`highway`, `road`, `GPSOnly`, `pavement`, `unknown`.
|
||||
- `tools/write_spatial_weights.py` writes that file directly, or can generate
|
||||
named preset files under `work/spatial-weights/` for live copy-over testing.
|
||||
The prepared presets are:
|
||||
`vanilla`, `current`, `highway-free`, `highway-expensive`, and
|
||||
`surface-extreme`.
|
||||
- The old verbose local-search probe is still available behind
|
||||
`kEnableGpsLocalSearchHooks`. It hooks `GPSSearch` (`0x44f054`),
|
||||
`GPSResolveHandle` (`0x44e1a8`), `GPSEdgeCost` (`0x44f838`), and the two
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
@@ -211,6 +212,7 @@ constexpr float kGpsSpatialRoadMultiplier = 1.0f;
|
||||
constexpr float kGpsSpatialGpsOnlyMultiplier = 1.20f;
|
||||
constexpr float kGpsSpatialPavementMultiplier = 1.35f;
|
||||
constexpr float kGpsSpatialUnknownMultiplier = 1.05f;
|
||||
constexpr uint64_t kGpsSpatialWeightsReloadIntervalMs = 500;
|
||||
|
||||
struct GpsProviderClassOverride
|
||||
{
|
||||
@@ -530,17 +532,38 @@ GpsProviderFilterFunc gOriginalGpsFilteredProviderFilter = nullptr;
|
||||
GpsProviderFilterFunc gOriginalGpsBaseProviderFilter = nullptr;
|
||||
GpsMultiplierFunc gOriginalGpsAuxMultiplier = nullptr;
|
||||
GpsMultiplierFunc gOriginalGpsNodeMultiplier = nullptr;
|
||||
std::array<std::atomic<float>, 5> gGpsSpatialMultipliers = {
|
||||
kGpsSpatialHighwayMultiplier,
|
||||
kGpsSpatialRoadMultiplier,
|
||||
kGpsSpatialGpsOnlyMultiplier,
|
||||
kGpsSpatialPavementMultiplier,
|
||||
kGpsSpatialUnknownMultiplier,
|
||||
};
|
||||
std::mutex gGpsSpatialWeightsMutex;
|
||||
std::atomic<uint64_t> gGpsSpatialWeightsNextReloadCheckMs = 0;
|
||||
std::filesystem::file_time_type gGpsSpatialWeightsLastWriteTime{};
|
||||
bool gGpsSpatialWeightsFileKnown = false;
|
||||
|
||||
std::filesystem::path GetLogPath()
|
||||
std::filesystem::path GetPluginDirectory()
|
||||
{
|
||||
wchar_t modulePath[MAX_PATH]{};
|
||||
if (gModule && GetModuleFileNameW(gModule, modulePath, MAX_PATH) > 0)
|
||||
{
|
||||
auto path = std::filesystem::path(modulePath);
|
||||
return path.parent_path() / L"EdgeWeightGPS.log";
|
||||
return path.parent_path();
|
||||
}
|
||||
|
||||
return std::filesystem::path(L"red4ext") / L"plugins" / L"EdgeWeightGPS" / L"EdgeWeightGPS.log";
|
||||
return std::filesystem::path(L"red4ext") / L"plugins" / L"EdgeWeightGPS";
|
||||
}
|
||||
|
||||
std::filesystem::path GetLogPath()
|
||||
{
|
||||
return GetPluginDirectory() / L"EdgeWeightGPS.log";
|
||||
}
|
||||
|
||||
std::filesystem::path GetSpatialWeightsPath()
|
||||
{
|
||||
return GetPluginDirectory() / L"spatial_weights.bin";
|
||||
}
|
||||
|
||||
uint64_t GetElapsedMs()
|
||||
@@ -587,6 +610,100 @@ void LogRed4ext(std::string_view aMessage)
|
||||
}
|
||||
}
|
||||
|
||||
bool IsUsableSpatialMultiplier(float aValue)
|
||||
{
|
||||
return std::isfinite(aValue) && aValue > 0.0f && aValue < 20.0f;
|
||||
}
|
||||
|
||||
void StoreGpsSpatialMultipliers(const std::array<float, 5>& aValues)
|
||||
{
|
||||
for (size_t index = 0; index < aValues.size(); ++index)
|
||||
{
|
||||
gGpsSpatialMultipliers[index].store(aValues[index], std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
void LogGpsSpatialMultipliers(std::string_view aPrefix, const std::array<float, 5>& aValues)
|
||||
{
|
||||
std::ostringstream line;
|
||||
line << aPrefix << " highway=" << aValues[0] << " road=" << aValues[1] << " gpsonly=" << aValues[2]
|
||||
<< " pavement=" << aValues[3] << " unknown=" << aValues[4];
|
||||
LogRed4ext(line.str());
|
||||
}
|
||||
|
||||
void MaybeReloadGpsSpatialWeights()
|
||||
{
|
||||
if (!kEnableGpsSpatialEdgeWeightPatch)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const auto nowMs = GetElapsedMs();
|
||||
if (nowMs < gGpsSpatialWeightsNextReloadCheckMs.load(std::memory_order_relaxed))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard lock(gGpsSpatialWeightsMutex);
|
||||
if (nowMs < gGpsSpatialWeightsNextReloadCheckMs.load(std::memory_order_relaxed))
|
||||
{
|
||||
return;
|
||||
}
|
||||
gGpsSpatialWeightsNextReloadCheckMs.store(nowMs + kGpsSpatialWeightsReloadIntervalMs, std::memory_order_relaxed);
|
||||
|
||||
const auto path = GetSpatialWeightsPath();
|
||||
std::error_code error;
|
||||
const auto writeTime = std::filesystem::last_write_time(path, error);
|
||||
if (error)
|
||||
{
|
||||
if (gGpsSpatialWeightsFileKnown)
|
||||
{
|
||||
gGpsSpatialWeightsFileKnown = false;
|
||||
std::ostringstream line;
|
||||
line << "spatial weights file unavailable path=" << path.string() << " using current/default values";
|
||||
LogRed4ext(line.str());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (gGpsSpatialWeightsFileKnown && writeTime == gGpsSpatialWeightsLastWriteTime)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
std::array<float, 5> values{};
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
input.read(reinterpret_cast<char*>(values.data()), static_cast<std::streamsize>(values.size() * sizeof(float)));
|
||||
if (input.gcount() != static_cast<std::streamsize>(values.size() * sizeof(float)))
|
||||
{
|
||||
std::ostringstream line;
|
||||
line << "spatial weights reload skipped path=" << path.string() << " bytes=" << input.gcount()
|
||||
<< " expected=" << values.size() * sizeof(float);
|
||||
LogRed4ext(line.str());
|
||||
gGpsSpatialWeightsFileKnown = true;
|
||||
gGpsSpatialWeightsLastWriteTime = writeTime;
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto value : values)
|
||||
{
|
||||
if (!IsUsableSpatialMultiplier(value))
|
||||
{
|
||||
std::ostringstream line;
|
||||
line << "spatial weights reload skipped path=" << path.string() << " invalidValue=" << value;
|
||||
LogRed4ext(line.str());
|
||||
gGpsSpatialWeightsFileKnown = true;
|
||||
gGpsSpatialWeightsLastWriteTime = writeTime;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
StoreGpsSpatialMultipliers(values);
|
||||
gGpsSpatialWeightsFileKnown = true;
|
||||
gGpsSpatialWeightsLastWriteTime = writeTime;
|
||||
LogGpsSpatialMultipliers("spatial weights reloaded", values);
|
||||
}
|
||||
|
||||
uintptr_t GetImageBase()
|
||||
{
|
||||
return reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr));
|
||||
@@ -1035,15 +1152,15 @@ float GpsSpatialRoadClassMultiplier(char aCode)
|
||||
switch (aCode)
|
||||
{
|
||||
case 'H':
|
||||
return kGpsSpatialHighwayMultiplier;
|
||||
return gGpsSpatialMultipliers[0].load(std::memory_order_relaxed);
|
||||
case 'R':
|
||||
return kGpsSpatialRoadMultiplier;
|
||||
return gGpsSpatialMultipliers[1].load(std::memory_order_relaxed);
|
||||
case 'G':
|
||||
return kGpsSpatialGpsOnlyMultiplier;
|
||||
return gGpsSpatialMultipliers[2].load(std::memory_order_relaxed);
|
||||
case 'P':
|
||||
return kGpsSpatialPavementMultiplier;
|
||||
return gGpsSpatialMultipliers[3].load(std::memory_order_relaxed);
|
||||
default:
|
||||
return kGpsSpatialUnknownMultiplier;
|
||||
return gGpsSpatialMultipliers[4].load(std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4213,6 +4330,8 @@ int32_t DetourGpsRouteProducer(void* aGraph, void* aQuery, void* aOutResult)
|
||||
LogRed4ext(line.str());
|
||||
}
|
||||
|
||||
MaybeReloadGpsSpatialWeights();
|
||||
|
||||
GpsEdgeCostStats stats{};
|
||||
auto* previousStats = gActiveGpsEdgeCostStats;
|
||||
const auto previousRouteProducerCall = gActiveGpsRouteProducerCall;
|
||||
@@ -4905,6 +5024,21 @@ RED4EXT_C_EXPORT bool RED4EXT_CALL Main(RED4ext::v1::PluginHandle aHandle, RED4e
|
||||
const auto added = aSdk->gameStates->Add(aHandle, kGameStateRunning, &gRunningState);
|
||||
LogRed4ext(added ? "registered Running game-state callbacks" : "failed to register Running game-state callbacks");
|
||||
|
||||
if (kEnableGpsSpatialEdgeWeightPatch)
|
||||
{
|
||||
std::array<float, 5> values = {
|
||||
gGpsSpatialMultipliers[0].load(std::memory_order_relaxed),
|
||||
gGpsSpatialMultipliers[1].load(std::memory_order_relaxed),
|
||||
gGpsSpatialMultipliers[2].load(std::memory_order_relaxed),
|
||||
gGpsSpatialMultipliers[3].load(std::memory_order_relaxed),
|
||||
gGpsSpatialMultipliers[4].load(std::memory_order_relaxed),
|
||||
};
|
||||
std::ostringstream line;
|
||||
line << "spatial weights path=" << GetSpatialWeightsPath().string();
|
||||
LogRed4ext(line.str());
|
||||
LogGpsSpatialMultipliers("spatial weights defaults", values);
|
||||
}
|
||||
|
||||
if (kEnableGpsQueryLifecycleHooks)
|
||||
{
|
||||
AttachProbeHook("RunGPSQuery body 0x29bd128", kRunGpsQueryBodyRva,
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Write EdgeWeightGPS spatial weight presets as five raw float32 values."""
|
||||
|
||||
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/spatial_weights.bin"
|
||||
)
|
||||
|
||||
PRESETS: dict[str, tuple[float, float, float, float, float]] = {
|
||||
"vanilla": (1.0, 1.0, 1.0, 1.0, 1.0),
|
||||
"current": (0.62, 1.0, 1.20, 1.35, 1.05),
|
||||
"highway-free": (0.25, 1.10, 1.35, 1.60, 1.15),
|
||||
"highway-expensive": (3.0, 1.0, 1.0, 1.0, 1.0),
|
||||
"surface-extreme": (0.45, 1.20, 1.80, 2.50, 1.30),
|
||||
}
|
||||
|
||||
|
||||
def parse_weights(text: str) -> tuple[float, float, float, float, float]:
|
||||
values = tuple(float(part) for part in text.split(","))
|
||||
if len(values) != 5:
|
||||
raise argparse.ArgumentTypeError("expected five comma-separated floats")
|
||||
return values # type: ignore[return-value]
|
||||
|
||||
|
||||
def write_weights(path: Path, values: tuple[float, float, float, float, float]) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(struct.pack("<5f", *values))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("preset", nargs="?", choices=sorted(PRESETS), default="current")
|
||||
parser.add_argument("--weights", type=parse_weights, help="five comma-separated floats: H,R,G,P,U")
|
||||
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, values in PRESETS.items():
|
||||
write_weights(args.preset_dir / f"{name}.bin", values)
|
||||
print(f"{args.preset_dir / f'{name}.bin'}: {values}")
|
||||
return 0
|
||||
|
||||
values = args.weights if args.weights is not None else PRESETS[args.preset]
|
||||
write_weights(args.output, values)
|
||||
print(f"{args.output}: {values}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user