Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(extra-natives/rdr3): Add natives for train tracks #2575

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 163 additions & 0 deletions code/components/extra-natives-rdr3/src/TrackNatives.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
#include "StdInc.h"
#include <Hooking.h>
#include <MinHook.h>
#include <ScriptEngine.h>
#include <ICoreGameInit.h>
#include <GameInit.h>
#include "GamePrimitives.h"

#include <ResourceManager.h>
#include <ResourceMetaDataComponent.h>
#include <VFSManager.h>

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<void(const char* path, CTrainTrackPool* tracksPool)> _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<void(CTrainTrackPool* tracksPool)> _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<void(CTrainTrackPool* tracksPool)> _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<void()> _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<CTrainTrackPool*>(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<int>(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<int>(0);

if (trackCount == 0 || targetIdx >= trackCount)
{
scriptContext.SetResult<int>(0);
}
else
{
scriptContext.SetResult<int>(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<fx::Resource> resource = resourceManager->GetResource(scriptContext.GetArgument<const char*>(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<const char*>(1);

fwRefContainer<vfs::Stream> 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);
});
});
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 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<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());
}
});
Loading
Loading