Skip to content

Commit

Permalink
feat(rdr3): Manifest statement for custom train tracks
Browse files Browse the repository at this point in the history
  • Loading branch information
Sage-of-Mirrors committed Jul 2, 2024
1 parent 4755faf commit 0496a31
Show file tree
Hide file tree
Showing 3 changed files with 291 additions and 0 deletions.
13 changes: 13 additions & 0 deletions code/components/citizen-resources-gta/src/ResourcesTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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());
Expand Down
215 changes: 215 additions & 0 deletions code/components/gta-core-rdr3/src/PatchTrainTrackCrashes.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
#include <StdInc.h>

#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<int8_t(uint32_t)> _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<CTrainTrackPool*>(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<uintptr_t>(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<char>("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<intptr_t>(location) + 9 + loopBodyPos;

// Skip the original test + jnz instructions.
const auto failPtr = reinterpret_cast<intptr_t>(location) + 9;

patchStub.Init(successPtr, failPtr);
hook::nop(location, 9);
hook::jump(location, patchStub.GetCode());
}
});
63 changes: 63 additions & 0 deletions code/components/gta-core-rdr3/src/PatchTrainTrackFileOverride.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#include <StdInc.h>
#include <Hooking.h>
#include <Hooking.Stubs.h>
#include <Hooking.Patterns.h>
#include <GameInit.h>

//
// 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();
}
);
});

0 comments on commit 0496a31

Please sign in to comment.