From bdf7ad88b14701077bb0002aa39dce5f0d06a95b Mon Sep 17 00:00:00 2001 From: Dylan Ascencio Date: Wed, 15 May 2024 17:44:21 -0400 Subject: [PATCH 1/5] feat(rdr3/natives): Add extra natives for train tracks --- .../extra-natives-rdr3/src/TrackNatives.cpp | 157 ++++++++++++++++++ ext/native-decls/GetTrackCount.md | 19 +++ ext/native-decls/GetTrackFromIndex.md | 22 +++ ext/native-decls/LoadTracksFromFile.md | 26 +++ 4 files changed, 224 insertions(+) create mode 100644 code/components/extra-natives-rdr3/src/TrackNatives.cpp create mode 100644 ext/native-decls/GetTrackCount.md create mode 100644 ext/native-decls/GetTrackFromIndex.md create mode 100644 ext/native-decls/LoadTracksFromFile.md diff --git a/code/components/extra-natives-rdr3/src/TrackNatives.cpp b/code/components/extra-natives-rdr3/src/TrackNatives.cpp new file mode 100644 index 0000000000..3954e28d61 --- /dev/null +++ b/code/components/extra-natives-rdr3/src/TrackNatives.cpp @@ -0,0 +1,157 @@ + #include "StdInc.h" +#include +#include +#include +#include +#include +#include "GamePrimitives.h" + +#include +#include +#include + +#define MAX_TRACKS 50 +#define DEFAULT_TRACK_FILE "common:/data/levels/rdr3/traintracks.xml" + +struct sTrainTrack +{ + bool bEnabled; + bool bOpen; + bool bHasJunctions; + bool bStopsAtStations; + bool bMPStopsAtStations; + + uint32_t NameHash; + uint32_t BrakingDistance; + + uint32_t TotalNodeCount; + uint32_t LinearNodeCount; + uint32_t CurveNodeCount; + + char Padding[0x1244]; +}; + +// Pointer to the main train tracks array. Fixed-sized array of size 50. +static sTrainTrack* g_trainTracks; +// Pointer to the number of loaded tracks in the main tracks array. +static int* g_trainTrackCount; + +std::mutex g_loaderLock; + +// Internal function that loads the XML file at the given path into the given tracks array. +static hook::cdecl_stub _loadTracks([]() +{ + return hook::get_call(hook::get_pattern("48 8D 15 ? ? ? ? 49 8B C9 E8 ? ? ? ? 48 8D 0D ? ? ? ? E8 ? ? ? ? 48 8D 0D", 10)); +}); + +// Internal function that does some post-processing on the given tracks array; primarily for setting up junctions? +static hook::cdecl_stub _postProcessTracks1([]() +{ + return hook::get_pattern("48 89 4C 24 ? 53 55 56 57 41 54 41 55 41 56 41 57 48 63 99"); +}); + +// Internal function that does some post-processing on the given tracks array; purpose not clear. +static hook::cdecl_stub _postProcessTracks2([]() +{ + return hook::get_pattern("48 89 4C 24 ? 53 55 56 57 41 54 41 55 41 56 41 57 48 83 EC ? 8B 81"); +}); + +// Internal function that clears the main tracks array. +static hook::cdecl_stub _unloadTracks([]() +{ + return hook::get_pattern + ( + "48 89 5C 24 ? 57 48 83 EC ? 48 8D 1D ? ? ? ? BF ? ? ? ? 48 8B CB E8 ? ? ? ? 48 81 C3 ? ? ? ? 48 83 EF ? 75 ? 21 3D " + "? ? ? ? 48 8D 0D ? ? ? ? 48 8B 5C 24 ? 48 83 C4 ? 5F E9 ? ? ? ? CC 48 89 5C 24 ? 57 48 83 EC ? 48 8D 1D ? ? ? ? BF " + "? ? ? ? 48 8B CB E8 ? ? ? ? 48 81 C3 ? ? ? ? 48 83 EF ? 75 ? 21 3D ? ? ? ? 48 8D 0D ? ? ? ? 48 8B 5C 24 ? 48 83 C4 " + "? 5F E9 ? ? ? ? CC 0F 28 05" + ); +}); + +void LoadTracks(const char* path) +{ + if (path != nullptr) + { + _loadTracks(path, g_trainTracks); + } + else + { + _loadTracks(DEFAULT_TRACK_FILE, g_trainTracks); + } + + _postProcessTracks1(g_trainTracks); + _postProcessTracks2(g_trainTracks); +} + +static HookFunction trackNativesFunc([]() +{ + g_trainTracks = hook::get_address(hook::get_pattern("48 8D 15 ? ? ? ? 49 8B C9 E8 ? ? ? ? 48 8D 0D ? ? ? ? E8 ? ? ? ? 48 8D 0D", 3)); + g_trainTrackCount = hook::get_address(hook::get_pattern("8B 05 ? ? ? ? 89 75 ? 89 45", 2)); + + fx::ScriptEngine::RegisterNativeHandler("GET_TRACK_COUNT", [](fx::ScriptContext& scriptContext) + { + scriptContext.SetResult(*g_trainTrackCount); + }); + + fx::ScriptEngine::RegisterNativeHandler("GET_TRACK_FROM_INDEX", [](fx::ScriptContext& scriptContext) + { + int trackCount = *g_trainTrackCount; + int targetIdx = scriptContext.GetArgument(0); + + if (trackCount == 0 || targetIdx >= trackCount) + { + scriptContext.SetResult(0); + } + else + { + scriptContext.SetResult(g_trainTracks[targetIdx].NameHash); + } + }); + + fx::ScriptEngine::RegisterNativeHandler("LOAD_TRACKS_FROM_FILE", [](fx::ScriptContext& scriptContext) + { + fx::ResourceManager* resourceManager = fx::ResourceManager::GetCurrent(); + fwRefContainer resource = resourceManager->GetResource(scriptContext.GetArgument(0)); + + if (!resource.GetRef()) + { + scriptContext.SetResult(false); + return; + } + + std::string filePath = resource->GetPath(); + + // Make sure path separator exists or add it before combining path with file name + char c = filePath[filePath.length() - 1]; + if (c != '/' && c != '\\') + { + filePath += '/'; + } + + filePath += scriptContext.GetArgument(1); + + fwRefContainer stream = vfs::OpenRead(filePath); + if (!stream.GetRef()) + { + trace("unable to find traintracks.xml at %s\n", filePath.c_str()); + scriptContext.SetResult(false); + + return; + } + + std::lock_guard _(g_loaderLock); + + _unloadTracks(); + LoadTracks(filePath.c_str()); + + scriptContext.SetResult(true); + }); + + OnKillNetworkDone.Connect([]() + { + std::lock_guard _(g_loaderLock); + + _unloadTracks(); + LoadTracks(nullptr); + }); +}); diff --git a/ext/native-decls/GetTrackCount.md b/ext/native-decls/GetTrackCount.md new file mode 100644 index 0000000000..6c8fd3c4cf --- /dev/null +++ b/ext/native-decls/GetTrackCount.md @@ -0,0 +1,19 @@ +--- +ns: CFX +apiset: client +game: rdr3 +--- +## GET_TRACK_COUNT + +```c +int GET_TRACK_COUNT(); +``` + +## Examples + +```lua +local trackCount = GetTrackCount() +``` + +## Return value +The number of loaded train tracks. \ No newline at end of file diff --git a/ext/native-decls/GetTrackFromIndex.md b/ext/native-decls/GetTrackFromIndex.md new file mode 100644 index 0000000000..a135385bd9 --- /dev/null +++ b/ext/native-decls/GetTrackFromIndex.md @@ -0,0 +1,22 @@ +--- +ns: CFX +apiset: client +game: rdr3 +--- +## GET_TRACK_FROM_INDEX + +```c +int GET_TRACK_FROM_INDEX(int index); +``` + +## Examples + +```lua +local trackHash = GetTrackFromIndex(idx) +``` + +## Parameters +* **index**: Index of the desired track + +## Return value +The name hash of the train track at the given index, or 0 if the index is invalid. \ No newline at end of file diff --git a/ext/native-decls/LoadTracksFromFile.md b/ext/native-decls/LoadTracksFromFile.md new file mode 100644 index 0000000000..48ef86a9a1 --- /dev/null +++ b/ext/native-decls/LoadTracksFromFile.md @@ -0,0 +1,26 @@ +--- +ns: CFX +apiset: client +game: rdr3 +--- +## LOAD_TRACKS_FROM_FILE + +```c +BOOL LOAD_TRACKS_FROM_FILE(char* resourceName, char* fileName); +``` + +Define the XML file in the resource's fxmanifest under the `file(s)` section, along with any extra track `*.dat` files. +WARNING: Running this native while any trains are spawned WILL crash the client! Make sure the resource using this native is one of the first resources to load. + +## Examples + +```lua +local success = LoadTracksFromFile('my-resource-name', 'traintracks.xml') +``` + +## Parameters +* **resourceName**: The name of the resource containing your modified tracks +* **fileName**: The name of the XML file + +## Return value +True on success. \ No newline at end of file From a4d4ec51fc6896bef33d541acaeeae7a55f6938b Mon Sep 17 00:00:00 2001 From: Dylan Ascencio Date: Wed, 15 May 2024 17:44:21 -0400 Subject: [PATCH 2/5] feat(extra-natives/rdr3): Add extra natives for train tracks --- .../extra-natives-rdr3/src/TrackNatives.cpp | 157 ++++++++++++++++++ ext/native-decls/GetTrackCount.md | 19 +++ ext/native-decls/GetTrackFromIndex.md | 22 +++ ext/native-decls/LoadTracksFromFile.md | 26 +++ 4 files changed, 224 insertions(+) create mode 100644 code/components/extra-natives-rdr3/src/TrackNatives.cpp create mode 100644 ext/native-decls/GetTrackCount.md create mode 100644 ext/native-decls/GetTrackFromIndex.md create mode 100644 ext/native-decls/LoadTracksFromFile.md diff --git a/code/components/extra-natives-rdr3/src/TrackNatives.cpp b/code/components/extra-natives-rdr3/src/TrackNatives.cpp new file mode 100644 index 0000000000..3954e28d61 --- /dev/null +++ b/code/components/extra-natives-rdr3/src/TrackNatives.cpp @@ -0,0 +1,157 @@ + #include "StdInc.h" +#include +#include +#include +#include +#include +#include "GamePrimitives.h" + +#include +#include +#include + +#define MAX_TRACKS 50 +#define DEFAULT_TRACK_FILE "common:/data/levels/rdr3/traintracks.xml" + +struct sTrainTrack +{ + bool bEnabled; + bool bOpen; + bool bHasJunctions; + bool bStopsAtStations; + bool bMPStopsAtStations; + + uint32_t NameHash; + uint32_t BrakingDistance; + + uint32_t TotalNodeCount; + uint32_t LinearNodeCount; + uint32_t CurveNodeCount; + + char Padding[0x1244]; +}; + +// Pointer to the main train tracks array. Fixed-sized array of size 50. +static sTrainTrack* g_trainTracks; +// Pointer to the number of loaded tracks in the main tracks array. +static int* g_trainTrackCount; + +std::mutex g_loaderLock; + +// Internal function that loads the XML file at the given path into the given tracks array. +static hook::cdecl_stub _loadTracks([]() +{ + return hook::get_call(hook::get_pattern("48 8D 15 ? ? ? ? 49 8B C9 E8 ? ? ? ? 48 8D 0D ? ? ? ? E8 ? ? ? ? 48 8D 0D", 10)); +}); + +// Internal function that does some post-processing on the given tracks array; primarily for setting up junctions? +static hook::cdecl_stub _postProcessTracks1([]() +{ + return hook::get_pattern("48 89 4C 24 ? 53 55 56 57 41 54 41 55 41 56 41 57 48 63 99"); +}); + +// Internal function that does some post-processing on the given tracks array; purpose not clear. +static hook::cdecl_stub _postProcessTracks2([]() +{ + return hook::get_pattern("48 89 4C 24 ? 53 55 56 57 41 54 41 55 41 56 41 57 48 83 EC ? 8B 81"); +}); + +// Internal function that clears the main tracks array. +static hook::cdecl_stub _unloadTracks([]() +{ + return hook::get_pattern + ( + "48 89 5C 24 ? 57 48 83 EC ? 48 8D 1D ? ? ? ? BF ? ? ? ? 48 8B CB E8 ? ? ? ? 48 81 C3 ? ? ? ? 48 83 EF ? 75 ? 21 3D " + "? ? ? ? 48 8D 0D ? ? ? ? 48 8B 5C 24 ? 48 83 C4 ? 5F E9 ? ? ? ? CC 48 89 5C 24 ? 57 48 83 EC ? 48 8D 1D ? ? ? ? BF " + "? ? ? ? 48 8B CB E8 ? ? ? ? 48 81 C3 ? ? ? ? 48 83 EF ? 75 ? 21 3D ? ? ? ? 48 8D 0D ? ? ? ? 48 8B 5C 24 ? 48 83 C4 " + "? 5F E9 ? ? ? ? CC 0F 28 05" + ); +}); + +void LoadTracks(const char* path) +{ + if (path != nullptr) + { + _loadTracks(path, g_trainTracks); + } + else + { + _loadTracks(DEFAULT_TRACK_FILE, g_trainTracks); + } + + _postProcessTracks1(g_trainTracks); + _postProcessTracks2(g_trainTracks); +} + +static HookFunction trackNativesFunc([]() +{ + g_trainTracks = hook::get_address(hook::get_pattern("48 8D 15 ? ? ? ? 49 8B C9 E8 ? ? ? ? 48 8D 0D ? ? ? ? E8 ? ? ? ? 48 8D 0D", 3)); + g_trainTrackCount = hook::get_address(hook::get_pattern("8B 05 ? ? ? ? 89 75 ? 89 45", 2)); + + fx::ScriptEngine::RegisterNativeHandler("GET_TRACK_COUNT", [](fx::ScriptContext& scriptContext) + { + scriptContext.SetResult(*g_trainTrackCount); + }); + + fx::ScriptEngine::RegisterNativeHandler("GET_TRACK_FROM_INDEX", [](fx::ScriptContext& scriptContext) + { + int trackCount = *g_trainTrackCount; + int targetIdx = scriptContext.GetArgument(0); + + if (trackCount == 0 || targetIdx >= trackCount) + { + scriptContext.SetResult(0); + } + else + { + scriptContext.SetResult(g_trainTracks[targetIdx].NameHash); + } + }); + + fx::ScriptEngine::RegisterNativeHandler("LOAD_TRACKS_FROM_FILE", [](fx::ScriptContext& scriptContext) + { + fx::ResourceManager* resourceManager = fx::ResourceManager::GetCurrent(); + fwRefContainer resource = resourceManager->GetResource(scriptContext.GetArgument(0)); + + if (!resource.GetRef()) + { + scriptContext.SetResult(false); + return; + } + + std::string filePath = resource->GetPath(); + + // Make sure path separator exists or add it before combining path with file name + char c = filePath[filePath.length() - 1]; + if (c != '/' && c != '\\') + { + filePath += '/'; + } + + filePath += scriptContext.GetArgument(1); + + fwRefContainer stream = vfs::OpenRead(filePath); + if (!stream.GetRef()) + { + trace("unable to find traintracks.xml at %s\n", filePath.c_str()); + scriptContext.SetResult(false); + + return; + } + + std::lock_guard _(g_loaderLock); + + _unloadTracks(); + LoadTracks(filePath.c_str()); + + scriptContext.SetResult(true); + }); + + OnKillNetworkDone.Connect([]() + { + std::lock_guard _(g_loaderLock); + + _unloadTracks(); + LoadTracks(nullptr); + }); +}); diff --git a/ext/native-decls/GetTrackCount.md b/ext/native-decls/GetTrackCount.md new file mode 100644 index 0000000000..6c8fd3c4cf --- /dev/null +++ b/ext/native-decls/GetTrackCount.md @@ -0,0 +1,19 @@ +--- +ns: CFX +apiset: client +game: rdr3 +--- +## GET_TRACK_COUNT + +```c +int GET_TRACK_COUNT(); +``` + +## Examples + +```lua +local trackCount = GetTrackCount() +``` + +## Return value +The number of loaded train tracks. \ No newline at end of file diff --git a/ext/native-decls/GetTrackFromIndex.md b/ext/native-decls/GetTrackFromIndex.md new file mode 100644 index 0000000000..a135385bd9 --- /dev/null +++ b/ext/native-decls/GetTrackFromIndex.md @@ -0,0 +1,22 @@ +--- +ns: CFX +apiset: client +game: rdr3 +--- +## GET_TRACK_FROM_INDEX + +```c +int GET_TRACK_FROM_INDEX(int index); +``` + +## Examples + +```lua +local trackHash = GetTrackFromIndex(idx) +``` + +## Parameters +* **index**: Index of the desired track + +## Return value +The name hash of the train track at the given index, or 0 if the index is invalid. \ No newline at end of file diff --git a/ext/native-decls/LoadTracksFromFile.md b/ext/native-decls/LoadTracksFromFile.md new file mode 100644 index 0000000000..48ef86a9a1 --- /dev/null +++ b/ext/native-decls/LoadTracksFromFile.md @@ -0,0 +1,26 @@ +--- +ns: CFX +apiset: client +game: rdr3 +--- +## LOAD_TRACKS_FROM_FILE + +```c +BOOL LOAD_TRACKS_FROM_FILE(char* resourceName, char* fileName); +``` + +Define the XML file in the resource's fxmanifest under the `file(s)` section, along with any extra track `*.dat` files. +WARNING: Running this native while any trains are spawned WILL crash the client! Make sure the resource using this native is one of the first resources to load. + +## Examples + +```lua +local success = LoadTracksFromFile('my-resource-name', 'traintracks.xml') +``` + +## Parameters +* **resourceName**: The name of the resource containing your modified tracks +* **fileName**: The name of the XML file + +## Return value +True on success. \ No newline at end of file From 422aaacf051847b193f79a3b06760cc33904b8cc Mon Sep 17 00:00:00 2001 From: Dylan Ascencio Date: Sun, 2 Jun 2024 20:04:04 -0400 Subject: [PATCH 3/5] Implement a fix for the primary crash seen so far with custom tracks --- .../extra-natives-rdr3/src/TrackNatives.cpp | 2 +- .../src/PatchTrackJunctionCrash.cpp | 131 ++++++++++++++++++ 2 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 code/components/gta-core-rdr3/src/PatchTrackJunctionCrash.cpp diff --git a/code/components/extra-natives-rdr3/src/TrackNatives.cpp b/code/components/extra-natives-rdr3/src/TrackNatives.cpp index 3954e28d61..c3641503c4 100644 --- a/code/components/extra-natives-rdr3/src/TrackNatives.cpp +++ b/code/components/extra-natives-rdr3/src/TrackNatives.cpp @@ -1,4 +1,4 @@ - #include "StdInc.h" +#include "StdInc.h" #include #include #include diff --git a/code/components/gta-core-rdr3/src/PatchTrackJunctionCrash.cpp b/code/components/gta-core-rdr3/src/PatchTrackJunctionCrash.cpp new file mode 100644 index 0000000000..84dbaa53f5 --- /dev/null +++ b/code/components/gta-core-rdr3/src/PatchTrackJunctionCrash.cpp @@ -0,0 +1,131 @@ +#include + +#include "Hooking.h" +#include "Hooking.Stubs.h" + +// +// When the game loads junctions for train tracks, it will construct the junction with default values. Only later, +// when all the tracks have been loaded, does it set up the actual connections between the junctions that +// allow trains to utilize them. If a junction doesn't refer to an existing track, the post-processing function rightly +// ignores it and leaves it with its default values. However, custom train tracks have revealed a flaw with this: +// if a train attempts to interact with a junction that was ignored during post-processing, a crash occurs as the train +// attempts to use its invalid data to determine what track it should switch to. +// +// The solution presented below intercepts the original PostProcessJunctions() function. It checks the junctions that +// were loaded from the track files, and removes those that do not refer to a valid track. The game is then allowed +// to proceed into the original version of the function to continue setting up the junction data. +// + +constexpr uint8_t NODE_FLAG_JUNCTION = 0x08; +constexpr uint8_t NODE_FLAG_0x10 = 0x10; + +struct sTrainTrackJunction +{ + char Padding[0x08]; + uint32_t NodeIndex; + char Padding2[0x0C]; + uint32_t Index; + char Padding3[0x14]; + uint32_t TrackNameHash; + char Padding4[0x1C]; +}; + +struct sTrainTrackJunctionPool +{ + sTrainTrackJunction Junctions[20]; + uint32_t Count; +}; + +struct sTrainTrackNode +{ + uint64_t VTable; + uint8_t Flags; + uint8_t Padding[0x27]; +}; + +struct sTrainTrack +{ + bool bEnabled; + bool bOpen; + bool bHasJunctions; + bool bStopsAtStations; + bool bMPStopsAtStations; + + uint32_t NameHash; + uint32_t BrakingDistance; + + uint32_t TotalNodeCount; + uint32_t LinearNodeCount; + uint32_t CurveNodeCount; + + uint32_t m001C; + + sTrainTrackNode** NodePtrs; + + char Padding[0x290]; + sTrainTrackJunctionPool Junctions; + char Padding2[0x960]; +}; + +struct sTrainTrackPool +{ + sTrainTrack Tracks[50]; + uint32_t Count; +}; + +// Pointer to the train tracks pool. +static sTrainTrackPool* g_trainTrackPool; +// Pointer to the original function that sets up track junctions. +static void (*g_origPostProcessJunctions)(sTrainTrackPool*); + +// Internal function for getting a track's index from its name hash. Returns -1 if the hash was not in the tracks pool. +static hook::cdecl_stub _getTrackIndexFromHash([]() +{ + return hook::get_pattern("44 8B 0D ? ? ? ? 45 32 C0"); +}); + +static void PostProcessJunctions(sTrainTrackPool* tracks) +{ + for (uint32_t trk = 0; trk < tracks->Count; trk++) + { + sTrainTrack& curTrack = tracks->Tracks[trk]; + for (uint32_t jct = 0; jct < curTrack.Junctions.Count; jct++) + { + sTrainTrackJunction& curJunct = curTrack.Junctions.Junctions[jct]; + if (_getTrackIndexFromHash(curJunct.TrackNameHash) == -1) + { + trace("Removing junction %i from track %i (Unknown track name %X)\n", jct, trk, curJunct.TrackNameHash); + + // Clear the flags marking this node as a junction; not doing this causes the game to hang! + curTrack.NodePtrs[curJunct.NodeIndex]->Flags &= ~(NODE_FLAG_0x10 | NODE_FLAG_JUNCTION); + + // Move all the junctions down one slot in the array (and adjust their internal indices). + for (uint32_t rmv = jct; rmv < curTrack.Junctions.Count - 1; rmv++) + { + memcpy(&curTrack.Junctions.Junctions[rmv], &curTrack.Junctions.Junctions[rmv + 1], sizeof(sTrainTrackJunction)); + curTrack.Junctions.Junctions[rmv].Index--; + } + + // Clear the now-unreferenced junction at the end of the array, just to be tidy. + memset(&curTrack.Junctions.Junctions[curTrack.Junctions.Count - 1], 0, sizeof(sTrainTrackJunction)); + curTrack.Junctions.Count--; + jct--; + + // Make sure the track knows it has no junctions if the only one it had was removed. + if (curTrack.Junctions.Count == 0) + { + curTrack.bHasJunctions = false; + } + } + } + } + + // Continue on to the original game's processing function. + g_origPostProcessJunctions(tracks); +} + +static HookFunction hookFunction([]() +{ + auto location = hook::get_call(hook::get_pattern("E8 ? ? ? ? 48 8B CD E8 ? ? ? ? 4C 8D 9C 24 ? ? ? ? 49 8B 5B ? 49 8B 6B ? 49 8B 73 ? 49 8B 7B ? 49 8B E3 41 5F")); + g_origPostProcessJunctions = hook::trampoline(location, PostProcessJunctions); +}); From ed73eebcb8a37ded9084cd31bc5b2b1e938576c0 Mon Sep 17 00:00:00 2001 From: Dylan Ascencio Date: Mon, 3 Jun 2024 03:06:59 -0400 Subject: [PATCH 4/5] Add more crash fixes for train tracks --- ...onCrash.cpp => PatchTrainTrackCrashes.cpp} | 97 ++++++++++++++++++- 1 file changed, 93 insertions(+), 4 deletions(-) rename code/components/gta-core-rdr3/src/{PatchTrackJunctionCrash.cpp => PatchTrainTrackCrashes.cpp} (57%) diff --git a/code/components/gta-core-rdr3/src/PatchTrackJunctionCrash.cpp b/code/components/gta-core-rdr3/src/PatchTrainTrackCrashes.cpp similarity index 57% rename from code/components/gta-core-rdr3/src/PatchTrackJunctionCrash.cpp rename to code/components/gta-core-rdr3/src/PatchTrainTrackCrashes.cpp index 84dbaa53f5..5dbd6bffbb 100644 --- a/code/components/gta-core-rdr3/src/PatchTrackJunctionCrash.cpp +++ b/code/components/gta-core-rdr3/src/PatchTrainTrackCrashes.cpp @@ -18,6 +18,8 @@ constexpr uint8_t NODE_FLAG_JUNCTION = 0x08; constexpr uint8_t NODE_FLAG_0x10 = 0x10; +constexpr uint8_t JUNCTION_MAX = 20; +constexpr uint8_t TRAIN_TRACK_MAX = 50; struct sTrainTrackJunction { @@ -32,7 +34,7 @@ struct sTrainTrackJunction struct sTrainTrackJunctionPool { - sTrainTrackJunction Junctions[20]; + sTrainTrackJunction Junctions[JUNCTION_MAX]; uint32_t Count; }; @@ -69,14 +71,17 @@ struct sTrainTrack struct sTrainTrackPool { - sTrainTrack Tracks[50]; + sTrainTrack Tracks[TRAIN_TRACK_MAX]; uint32_t Count; }; // Pointer to the train tracks pool. static sTrainTrackPool* g_trainTrackPool; + // Pointer to the original function that sets up track junctions. static void (*g_origPostProcessJunctions)(sTrainTrackPool*); +// Internal function for spawning a train as a mission vehicle. +static int64_t (*g_origCreateMissionTrain)(uint32_t, float*, bool, bool, bool, bool); // Internal function for getting a track's index from its name hash. Returns -1 if the hash was not in the tracks pool. static hook::cdecl_stub _getTrackIndexFromHash([]() @@ -124,8 +129,92 @@ static void PostProcessJunctions(sTrainTrackPool* tracks) g_origPostProcessJunctions(tracks); } +static int64_t CreateMissionTrain(uint32_t config, float* position, bool direction, bool passengers, bool p4, bool conductor) +{ + if (g_trainTrackPool->Count == 0) + { + trace("CreateMissionTrain() failed: Track pool is empty!\n"); + return 0; + } + + return g_origCreateMissionTrain(config, position, direction, passengers, p4, conductor); +} + +static bool IsTrackPoolFull() +{ + if (g_trainTrackPool->Count >= TRAIN_TRACK_MAX) + { + trace("Track pool is full, no more tracks can be loaded. (Count: %i)\n", g_trainTrackPool->Count); + return false; + } + + return true; +} + static HookFunction hookFunction([]() { - auto location = hook::get_call(hook::get_pattern("E8 ? ? ? ? 48 8B CD E8 ? ? ? ? 4C 8D 9C 24 ? ? ? ? 49 8B 5B ? 49 8B 6B ? 49 8B 73 ? 49 8B 7B ? 49 8B E3 41 5F")); - g_origPostProcessJunctions = hook::trampoline(location, PostProcessJunctions); + g_trainTrackPool = hook::get_address(hook::get_pattern("48 8D 15 ? ? ? ? 49 8B C9 E8 ? ? ? ? 48 8D 0D ? ? ? ? E8 ? ? ? ? 48 8D 0D", 3)); + + // Fixes crash with junctions that point to invalid tracks + { + auto location = hook::get_call(hook::get_pattern("E8 ? ? ? ? 48 8B CD E8 ? ? ? ? 4C 8D 9C 24 ? ? ? ? 49 8B 5B ? 49 8B 6B ? 49 8B 73 ? 49 8B 7B ? 49 8B E3 41 5F")); + g_origPostProcessJunctions = hook::trampoline(location, PostProcessJunctions); + } + + // Prevents trains from being spawned when no tracks exist + { + auto location = hook::get_pattern("48 8B C4 48 89 58 ? 48 89 68 ? 48 89 70 ? 48 89 78 ? 41 56 48 81 EC ? ? ? ? 33 DB 41 8A E9"); + g_origCreateMissionTrain = hook::trampoline(location, CreateMissionTrain); + } + + static struct : jitasm::Frontend + { + intptr_t retSuccess = 0; + intptr_t retFail = 0; + + void Init(intptr_t success, intptr_t fail) + { + this->retSuccess = success; + this->retFail = fail; + } + + virtual void InternalMain() override + { + // Original check for a valid XML element. + test(rbx, rbx); + jz("fail"); + + // New check for track count < 50. + mov(rax, reinterpret_cast(IsTrackPoolFull)); + call(rax); + test(al, al); + jz("fail"); + + // We have a valid XML child and the track pool isn't full, load the next track! + mov(rax, retSuccess); + jmp(rax); + + // Can't load another track, exit the loading loop. + L("fail"); + mov(rax, retFail); + jmp(rax); + } + } patchStub; + + // Prevents the game from loading more than 50 tracks + { + auto location = hook::get_pattern("48 85 DB 0F 85 ? ? ? ? 49 8B CF"); + + // Grab the offset of the body loop from jnz. + int32_t loopBodyPos = *(int32_t*)(location + 5); + // loopBodyPos is relative to the end of jnz, which is test + jnz (3 + 6 bytes). + const auto successPtr = reinterpret_cast(location) + 9 + loopBodyPos; + + // Skip the original test + jnz instructions. + const auto failPtr = reinterpret_cast(location) + 9; + + patchStub.Init(successPtr, failPtr); + hook::nop(location, 9); + hook::jump(location, patchStub.GetCode()); + } }); From f874c874bfab4e8dbbc47eda80d66f4866c508a8 Mon Sep 17 00:00:00 2001 From: Dylan Ascencio Date: Mon, 3 Jun 2024 15:40:28 -0400 Subject: [PATCH 5/5] Clean up track natives and crashes files a bit --- .../extra-natives-rdr3/src/TrackNatives.cpp | 44 +++++++------- .../src/PatchTrainTrackCrashes.cpp | 57 +++++++++---------- 2 files changed, 51 insertions(+), 50 deletions(-) diff --git a/code/components/extra-natives-rdr3/src/TrackNatives.cpp b/code/components/extra-natives-rdr3/src/TrackNatives.cpp index c3641503c4..2a0deab257 100644 --- a/code/components/extra-natives-rdr3/src/TrackNatives.cpp +++ b/code/components/extra-natives-rdr3/src/TrackNatives.cpp @@ -10,10 +10,10 @@ #include #include -#define MAX_TRACKS 50 -#define DEFAULT_TRACK_FILE "common:/data/levels/rdr3/traintracks.xml" +constexpr uint8_t TRAIN_TRACK_MAX = 50; +constexpr char DEFAULT_TRACKS_XML[] = "common:/data/levels/rdr3/traintracks.xml"; -struct sTrainTrack +struct CTrainTrack { bool bEnabled; bool bOpen; @@ -31,27 +31,29 @@ struct sTrainTrack char Padding[0x1244]; }; -// Pointer to the main train tracks array. Fixed-sized array of size 50. -static sTrainTrack* g_trainTracks; -// Pointer to the number of loaded tracks in the main tracks array. -static int* g_trainTrackCount; +struct CTrainTrackPool +{ + CTrainTrack Tracks[TRAIN_TRACK_MAX]; + uint32_t Count; +}; +static CTrainTrackPool* g_trainTracksPool; std::mutex g_loaderLock; // Internal function that loads the XML file at the given path into the given tracks array. -static hook::cdecl_stub _loadTracks([]() +static hook::cdecl_stub _loadTracks([]() { return hook::get_call(hook::get_pattern("48 8D 15 ? ? ? ? 49 8B C9 E8 ? ? ? ? 48 8D 0D ? ? ? ? E8 ? ? ? ? 48 8D 0D", 10)); }); // Internal function that does some post-processing on the given tracks array; primarily for setting up junctions? -static hook::cdecl_stub _postProcessTracks1([]() +static hook::cdecl_stub _postProcessTracks1([]() { return hook::get_pattern("48 89 4C 24 ? 53 55 56 57 41 54 41 55 41 56 41 57 48 63 99"); }); // Internal function that does some post-processing on the given tracks array; purpose not clear. -static hook::cdecl_stub _postProcessTracks2([]() +static hook::cdecl_stub _postProcessTracks2([]() { return hook::get_pattern("48 89 4C 24 ? 53 55 56 57 41 54 41 55 41 56 41 57 48 83 EC ? 8B 81"); }); @@ -68,34 +70,36 @@ static hook::cdecl_stub _unloadTracks([]() ); }); +// Loads tracks from the given file path. If the path is nullptr, the game's default tracks are loaded instead. void LoadTracks(const char* path) { if (path != nullptr) { - _loadTracks(path, g_trainTracks); + _loadTracks(path, g_trainTracksPool); } else { - _loadTracks(DEFAULT_TRACK_FILE, g_trainTracks); + _loadTracks(DEFAULT_TRACKS_XML, g_trainTracksPool); } - _postProcessTracks1(g_trainTracks); - _postProcessTracks2(g_trainTracks); + _postProcessTracks1(g_trainTracksPool); + _postProcessTracks2(g_trainTracksPool); } static HookFunction trackNativesFunc([]() { - g_trainTracks = hook::get_address(hook::get_pattern("48 8D 15 ? ? ? ? 49 8B C9 E8 ? ? ? ? 48 8D 0D ? ? ? ? E8 ? ? ? ? 48 8D 0D", 3)); - g_trainTrackCount = hook::get_address(hook::get_pattern("8B 05 ? ? ? ? 89 75 ? 89 45", 2)); + g_trainTracksPool = hook::get_address(hook::get_pattern("48 8D 15 ? ? ? ? 49 8B C9 E8 ? ? ? ? 48 8D 0D ? ? ? ? E8 ? ? ? ? 48 8D 0D", 3)); + // Returns the number of loaded tracks. fx::ScriptEngine::RegisterNativeHandler("GET_TRACK_COUNT", [](fx::ScriptContext& scriptContext) { - scriptContext.SetResult(*g_trainTrackCount); + scriptContext.SetResult(g_trainTracksPool->Count); }); + // Returns the name hash of the track with the given index, or 0 if the index is outside the bounds of the track pool. fx::ScriptEngine::RegisterNativeHandler("GET_TRACK_FROM_INDEX", [](fx::ScriptContext& scriptContext) { - int trackCount = *g_trainTrackCount; + int trackCount = g_trainTracksPool->Count; int targetIdx = scriptContext.GetArgument(0); if (trackCount == 0 || targetIdx >= trackCount) @@ -104,10 +108,11 @@ static HookFunction trackNativesFunc([]() } else { - scriptContext.SetResult(g_trainTracks[targetIdx].NameHash); + scriptContext.SetResult(g_trainTracksPool->Tracks[targetIdx].NameHash); } }); + // Unloads the currently loaded tracks and replaces them with tracks loaded from a specified resource file. fx::ScriptEngine::RegisterNativeHandler("LOAD_TRACKS_FROM_FILE", [](fx::ScriptContext& scriptContext) { fx::ResourceManager* resourceManager = fx::ResourceManager::GetCurrent(); @@ -147,6 +152,7 @@ static HookFunction trackNativesFunc([]() scriptContext.SetResult(true); }); + // Reset tracks to default state when the client has disconnected from a server. OnKillNetworkDone.Connect([]() { std::lock_guard _(g_loaderLock); diff --git a/code/components/gta-core-rdr3/src/PatchTrainTrackCrashes.cpp b/code/components/gta-core-rdr3/src/PatchTrainTrackCrashes.cpp index 5dbd6bffbb..032d9b6129 100644 --- a/code/components/gta-core-rdr3/src/PatchTrainTrackCrashes.cpp +++ b/code/components/gta-core-rdr3/src/PatchTrainTrackCrashes.cpp @@ -4,16 +4,11 @@ #include "Hooking.Stubs.h" // -// When the game loads junctions for train tracks, it will construct the junction with default values. Only later, -// when all the tracks have been loaded, does it set up the actual connections between the junctions that -// allow trains to utilize them. If a junction doesn't refer to an existing track, the post-processing function rightly -// ignores it and leaves it with its default values. However, custom train tracks have revealed a flaw with this: -// if a train attempts to interact with a junction that was ignored during post-processing, a crash occurs as the train -// attempts to use its invalid data to determine what track it should switch to. +// This file patches a few crashes that can occur when using the LOAD_TRACKS_FROM_FILE custom native: // -// The solution presented below intercepts the original PostProcessJunctions() function. It checks the junctions that -// were loaded from the track files, and removes those that do not refer to a valid track. The game is then allowed -// to proceed into the original version of the function to continue setting up the junction data. +// * Track junctions pointing to invalid tracks will now be deleted after the tracks XML is loaded. +// * The base-game native CREATE_MISSION_TRAIN will now fail to create a train if the client it is executed on has no tracks loaded. +// * The base game only has dedicated space for 50 total track objects; the track loading loop will now exit once the pool reaches that maximum. // constexpr uint8_t NODE_FLAG_JUNCTION = 0x08; @@ -21,7 +16,7 @@ constexpr uint8_t NODE_FLAG_0x10 = 0x10; constexpr uint8_t JUNCTION_MAX = 20; constexpr uint8_t TRAIN_TRACK_MAX = 50; -struct sTrainTrackJunction +struct CTrainTrackJunction { char Padding[0x08]; uint32_t NodeIndex; @@ -32,20 +27,20 @@ struct sTrainTrackJunction char Padding4[0x1C]; }; -struct sTrainTrackJunctionPool +struct CTrainTrackJunctionPool { - sTrainTrackJunction Junctions[JUNCTION_MAX]; + CTrainTrackJunction Junctions[JUNCTION_MAX]; uint32_t Count; }; -struct sTrainTrackNode +struct CTrainTrackNode { uint64_t VTable; uint8_t Flags; uint8_t Padding[0x27]; }; -struct sTrainTrack +struct CTrainTrack { bool bEnabled; bool bOpen; @@ -62,24 +57,23 @@ struct sTrainTrack uint32_t m001C; - sTrainTrackNode** NodePtrs; + CTrainTrackNode** NodePtrs; char Padding[0x290]; - sTrainTrackJunctionPool Junctions; + CTrainTrackJunctionPool Junctions; char Padding2[0x960]; }; -struct sTrainTrackPool +struct CTrainTrackPool { - sTrainTrack Tracks[TRAIN_TRACK_MAX]; + CTrainTrack Tracks[TRAIN_TRACK_MAX]; uint32_t Count; }; -// Pointer to the train tracks pool. -static sTrainTrackPool* g_trainTrackPool; +static CTrainTrackPool* g_trainTrackPool; // Pointer to the original function that sets up track junctions. -static void (*g_origPostProcessJunctions)(sTrainTrackPool*); +static void (*g_origPostProcessJunctions)(CTrainTrackPool*); // Internal function for spawning a train as a mission vehicle. static int64_t (*g_origCreateMissionTrain)(uint32_t, float*, bool, bool, bool, bool); @@ -89,17 +83,17 @@ static hook::cdecl_stub _getTrackIndexFromHash([]() return hook::get_pattern("44 8B 0D ? ? ? ? 45 32 C0"); }); -static void PostProcessJunctions(sTrainTrackPool* tracks) +static void PostProcessJunctions(CTrainTrackPool* tracks) { for (uint32_t trk = 0; trk < tracks->Count; trk++) { - sTrainTrack& curTrack = tracks->Tracks[trk]; + CTrainTrack& curTrack = tracks->Tracks[trk]; for (uint32_t jct = 0; jct < curTrack.Junctions.Count; jct++) { - sTrainTrackJunction& curJunct = curTrack.Junctions.Junctions[jct]; + CTrainTrackJunction& curJunct = curTrack.Junctions.Junctions[jct]; if (_getTrackIndexFromHash(curJunct.TrackNameHash) == -1) { - trace("Removing junction %i from track %i (Unknown track name %X)\n", jct, trk, curJunct.TrackNameHash); + trace("Removing junction %i from track %i - unknown track name 0x%X.\n", jct, trk, curJunct.TrackNameHash); // Clear the flags marking this node as a junction; not doing this causes the game to hang! curTrack.NodePtrs[curJunct.NodeIndex]->Flags &= ~(NODE_FLAG_0x10 | NODE_FLAG_JUNCTION); @@ -107,12 +101,13 @@ static void PostProcessJunctions(sTrainTrackPool* tracks) // Move all the junctions down one slot in the array (and adjust their internal indices). for (uint32_t rmv = jct; rmv < curTrack.Junctions.Count - 1; rmv++) { - memcpy(&curTrack.Junctions.Junctions[rmv], &curTrack.Junctions.Junctions[rmv + 1], sizeof(sTrainTrackJunction)); + memcpy(&curTrack.Junctions.Junctions[rmv], &curTrack.Junctions.Junctions[rmv + 1], sizeof(CTrainTrackJunction)); curTrack.Junctions.Junctions[rmv].Index--; } // Clear the now-unreferenced junction at the end of the array, just to be tidy. - memset(&curTrack.Junctions.Junctions[curTrack.Junctions.Count - 1], 0, sizeof(sTrainTrackJunction)); + memset(&curTrack.Junctions.Junctions[curTrack.Junctions.Count - 1], 0, sizeof(CTrainTrackJunction)); + curTrack.Junctions.Count--; jct--; @@ -133,7 +128,7 @@ static int64_t CreateMissionTrain(uint32_t config, float* position, bool directi { if (g_trainTrackPool->Count == 0) { - trace("CreateMissionTrain() failed: Track pool is empty!\n"); + trace("CreateMissionTrain() failed - track pool is empty!\n"); return 0; } @@ -144,7 +139,7 @@ static bool IsTrackPoolFull() { if (g_trainTrackPool->Count >= TRAIN_TRACK_MAX) { - trace("Track pool is full, no more tracks can be loaded. (Count: %i)\n", g_trainTrackPool->Count); + trace("Track pool is full - no more tracks can be loaded.\n"); return false; } @@ -153,7 +148,7 @@ static bool IsTrackPoolFull() static HookFunction hookFunction([]() { - g_trainTrackPool = hook::get_address(hook::get_pattern("48 8D 15 ? ? ? ? 49 8B C9 E8 ? ? ? ? 48 8D 0D ? ? ? ? E8 ? ? ? ? 48 8D 0D", 3)); + g_trainTrackPool = hook::get_address(hook::get_pattern("48 8D 15 ? ? ? ? 49 8B C9 E8 ? ? ? ? 48 8D 0D ? ? ? ? E8 ? ? ? ? 48 8D 0D", 3)); // Fixes crash with junctions that point to invalid tracks { @@ -207,7 +202,7 @@ static HookFunction hookFunction([]() // Grab the offset of the body loop from jnz. int32_t loopBodyPos = *(int32_t*)(location + 5); - // loopBodyPos is relative to the end of jnz, which is test + jnz (3 + 6 bytes). + // loopBodyPos is relative to the end of jnz, which is at location + test + jnz (location + 3 + 6 bytes). const auto successPtr = reinterpret_cast(location) + 9 + loopBodyPos; // Skip the original test + jnz instructions.