From 0496a31bb000fff7236f3bb3cdc59385e191f4af Mon Sep 17 00:00:00 2001 From: Dylan Ascencio Date: Tue, 2 Jul 2024 17:31:03 -0400 Subject: [PATCH] feat(rdr3): Manifest statement for custom train tracks --- .../src/ResourcesTest.cpp | 13 ++ .../src/PatchTrainTrackCrashes.cpp | 215 ++++++++++++++++++ .../src/PatchTrainTrackFileOverride.cpp | 63 +++++ 3 files changed, 291 insertions(+) create mode 100644 code/components/gta-core-rdr3/src/PatchTrainTrackCrashes.cpp create mode 100644 code/components/gta-core-rdr3/src/PatchTrainTrackFileOverride.cpp diff --git a/code/components/citizen-resources-gta/src/ResourcesTest.cpp b/code/components/citizen-resources-gta/src/ResourcesTest.cpp index 54862be1b8..348b31715f 100644 --- a/code/components/citizen-resources-gta/src/ResourcesTest.cpp +++ b/code/components/citizen-resources-gta/src/ResourcesTest.cpp @@ -57,6 +57,11 @@ namespace streaming void RemoveDataFileFromLoadList(const std::string& type, const std::string& path); void SetNextLevelPath(const std::string& path); + +#if defined(IS_RDR3) + // RDR3 only: Set the path for a train track XML file to load instead of the default one. + void SetTrainTrackOverridePath(const std::string& path); +#endif } #endif @@ -188,6 +193,14 @@ static InitFunction initFunction([] () streaming::SetNextLevelPath(resourceRoot + meta.second); } +#if defined(IS_RDR3) + // RDR3 only: allow a creator to replace the default train tracks with custom ones. + for (auto& meta : metaData->GetEntries("replace_traintrack_file")) + { + streaming::SetTrainTrackOverridePath(resourceRoot + meta.second); + } +#endif + if (!RangeLengthMatches(metaData->GetEntries("data_file"), metaData->GetEntries("data_file_extra"))) { GlobalError("data_file entry count mismatch in resource %s", resource->GetName()); 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..fb3abf6688 --- /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 overriding the default train tracks with user-generated ones: +// +// * 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/code/components/gta-core-rdr3/src/PatchTrainTrackFileOverride.cpp b/code/components/gta-core-rdr3/src/PatchTrainTrackFileOverride.cpp new file mode 100644 index 0000000000..5b383cbde1 --- /dev/null +++ b/code/components/gta-core-rdr3/src/PatchTrainTrackFileOverride.cpp @@ -0,0 +1,63 @@ +#include +#include +#include +#include +#include + +// +// Trains, trolleys, and minecarts all follow "tracks" made of splines. These tracks are defined by several *.dat files, +// which are in turn grouped together via the metadata at common:/data/levels/rdr3/traintracks.xml. +// +// This patch adds a global override path for traintracks.xml, which can be set by using the new `replace_traintrack_file` +// manifest statement in the fxmanifest file. This allows creators to make fully customized railway networks, without +// having to work around the base game's existing tracks. +// + +// Path to the default train tracks file. +constexpr char DEFAULT_TRACKS_XML[] = "common:/data/levels/rdr3/traintracks.xml"; + +struct CTrainTrackPool; +static std::string g_trainTrackOverridePath = ""; + +namespace streaming +{ + // RDR3 only: Set the path for a train track XML file to load instead of the default one. + void DLL_EXPORT SetTrainTrackOverridePath(const std::string& path) + { + g_trainTrackOverridePath = path; + } +} + +// Pointer to the original function that loads train tracks from the XML file. +static void (*g_origLoadTrackXML)(const char*, CTrainTrackPool*); + +static void LoadTrackXML(const char* fileName, CTrainTrackPool* dstPool) +{ + // If the file name we were given is the default one for train tracks, and the override path isn't empty, + // replace the default file name with our override. + // (We have to check for the train tracks file name because this same function is also used to load the trolley cables.) + if (strcmp(fileName, DEFAULT_TRACKS_XML) == 0 && !g_trainTrackOverridePath.empty()) + { + trace("Replacing %s with %s\n", DEFAULT_TRACKS_XML, g_trainTrackOverridePath.data()); + fileName = g_trainTrackOverridePath.data(); + } + + g_origLoadTrackXML(fileName, dstPool); +} + +static HookFunction hookFunction([]() +{ + // Intercept calls to LoadTrackXML() to insert our override check. + { + auto location = hook::get_call(hook::get_pattern("E8 ? ? ? ? 48 8D 0D ? ? ? ? E8 ? ? ? ? EB 44")); + g_origLoadTrackXML = hook::trampoline(location, LoadTrackXML); + } + + OnKillNetworkDone.Connect([]() + // Clear the override path so that the client loads the default tracks + // the next time they connect to a server. + { + g_trainTrackOverridePath.clear(); + } + ); +});