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..2a0deab257 --- /dev/null +++ b/code/components/extra-natives-rdr3/src/TrackNatives.cpp @@ -0,0 +1,163 @@ +#include "StdInc.h" +#include +#include +#include +#include +#include +#include "GamePrimitives.h" + +#include +#include +#include + +constexpr uint8_t TRAIN_TRACK_MAX = 50; +constexpr char DEFAULT_TRACKS_XML[] = "common:/data/levels/rdr3/traintracks.xml"; + +struct CTrainTrack +{ + 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]; +}; + +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([]() +{ + 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" + ); +}); + +// 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_trainTracksPool); + } + else + { + _loadTracks(DEFAULT_TRACKS_XML, g_trainTracksPool); + } + + _postProcessTracks1(g_trainTracksPool); + _postProcessTracks2(g_trainTracksPool); +} + +static HookFunction trackNativesFunc([]() +{ + 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_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_trainTracksPool->Count; + int targetIdx = scriptContext.GetArgument(0); + + if (trackCount == 0 || targetIdx >= trackCount) + { + scriptContext.SetResult(0); + } + else + { + 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(); + 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); + }); + + // Reset tracks to default state when the client has disconnected from a server. + OnKillNetworkDone.Connect([]() + { + std::lock_guard _(g_loaderLock); + + _unloadTracks(); + LoadTracks(nullptr); + }); +}); diff --git a/code/components/gta-core-rdr3/src/PatchTrainTrackCrashes.cpp b/code/components/gta-core-rdr3/src/PatchTrainTrackCrashes.cpp new file mode 100644 index 0000000000..032d9b6129 --- /dev/null +++ b/code/components/gta-core-rdr3/src/PatchTrainTrackCrashes.cpp @@ -0,0 +1,215 @@ +#include + +#include "Hooking.h" +#include "Hooking.Stubs.h" + +// +// This file patches a few crashes that can occur when using the LOAD_TRACKS_FROM_FILE custom native: +// +// * 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; +constexpr uint8_t NODE_FLAG_0x10 = 0x10; +constexpr uint8_t JUNCTION_MAX = 20; +constexpr uint8_t TRAIN_TRACK_MAX = 50; + +struct CTrainTrackJunction +{ + char Padding[0x08]; + uint32_t NodeIndex; + char Padding2[0x0C]; + uint32_t Index; + char Padding3[0x14]; + uint32_t TrackNameHash; + char Padding4[0x1C]; +}; + +struct CTrainTrackJunctionPool +{ + CTrainTrackJunction Junctions[JUNCTION_MAX]; + uint32_t Count; +}; + +struct CTrainTrackNode +{ + uint64_t VTable; + uint8_t Flags; + uint8_t Padding[0x27]; +}; + +struct CTrainTrack +{ + 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; + + CTrainTrackNode** NodePtrs; + + char Padding[0x290]; + CTrainTrackJunctionPool Junctions; + char Padding2[0x960]; +}; + +struct CTrainTrackPool +{ + CTrainTrack Tracks[TRAIN_TRACK_MAX]; + uint32_t Count; +}; + +static CTrainTrackPool* g_trainTrackPool; + +// Pointer to the original function that sets up track junctions. +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); + +// 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(CTrainTrackPool* tracks) +{ + for (uint32_t trk = 0; trk < tracks->Count; trk++) + { + CTrainTrack& curTrack = tracks->Tracks[trk]; + for (uint32_t jct = 0; jct < curTrack.Junctions.Count; jct++) + { + CTrainTrackJunction& curJunct = curTrack.Junctions.Junctions[jct]; + if (_getTrackIndexFromHash(curJunct.TrackNameHash) == -1) + { + 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); + + // 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(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(CTrainTrackJunction)); + + 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 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.\n"); + return false; + } + + return true; +} + +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)); + + // 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 at location + test + jnz (location + 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()); + } +}); 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