Add support for custom train tracks - #5114
Conversation
Adds createTrainTrack and node position accessors to the Lua API (server-only for now), lets setTrainTrack accept a custom track instead of just the 4 default ones, and syncs custom tracks to clients through the entity-add packet. Also fixes the train track puresync field, which used a single byte that couldn't tell "no track" apart from "default track 0" and had no way to represent a custom track. Both the client and server side of that sync are updated to match. The client doesn't render custom tracks yet, it just knows enough to keep the network stream lined up.
Custom tracks synced from the server now become a real CClientTrainTrack element instead of just being read and discarded, so scripts and other client code can see the node data. Trains don't actually drive on these yet, that still needs the native track lookup hooked, which is next.
Adds the game-layer piece that was still missing: CTrainTrackManagerSA tracks custom node data per track ID, and a hook on CTrain::ProcessControl takes over for any train whose track ID isn't one of the game's 4 built-in ones (which the original function isn't safe to run for, since it indexes fixed-size arrays with that ID). Verified the hook address and the bytes it overwrites by disassembling gta_sa.exe in Ghidra first, since guessing wrong here means crashing every train, not just custom ones. The replacement only covers core movement: advancing along the track, wrapping or stopping at the ends, and carriages following the one ahead. Station stops, passenger boarding and the derail-on-sharp-turn check are part of the vanilla station system and don't apply to a track a script drew, so they're left out. Wires the whole path end to end: setTrainTrack and the ongoing puresync now resolve a custom track's element ID down to the byte-sized ID the native code understands, on both the RPC and puresync paths.
setTrainTrackNodePosition used to only update the server's copy, so any client that already had the track (or a train running on it) would drift out of sync silently. Now it broadcasts a new RPC to already-joined clients, and the client-side track element updates both its own node list and the data the movement hook actually reads, recomputing the distances that depend on it.
The game indexes its three track arrays by track ID with no bounds check, so a custom ID read past the end. Relocating those arrays keeps every ID in bounds and lets CTrain::ProcessControl drive custom tracks itself; engine audio, throttle, brakes and derailing all work without reimplementing them.
A train created next to a custom track lands on it rather than on the nearest built-in one; also drops the empty hook file and the game side track accessors left without callers.
CTrain::ProcessControl only skips the track arrays for a derailed train, so one still riding a freed track reads a null node pointer on its next frame. The server derails its own trains, but that arrives over the network far too late.
The game stores track coordinates in eighths of a unit and rail distances in thirds, so reading them back divides rather than multiplies; the built-in tracks were landing about fifty times outside the world.
FindPositionOnTrackFromCoors recomputes the rail distance but leaves the node index pointing into the node array of the track being left, so moving onto a shorter track read past its end and threw the train across the map.
Inserting it mid list shifted the id of every RPC below it, so a client and a server built either side of the change disagreed on all of them. Also drops a stale TODO and the track accessors nothing calls.
Every other element type is included in the initial full sync a joining player gets; train tracks weren't, so a track created before someone joined stayed invisible to them for good. The 4 built-in tracks are naturally skipped since they have no parent element.
getTrainTrackNodeCount, getTrainTrackNodePosition, and setTrainTrackNodePosition (plus the RPC that synced live edits to already-joined clients) were removed along with everything that existed only to support them: the server-side node accessors on CTrainTrack, the client-side CTrainTrackRPCs handler, and the now-dead SetNodePosition chain running through CClientTrainTrack and CTrainTrackSA down to the sdk interface. createTrainTrack, setTrainTrack, and getTrainTrack are untouched - tracks are still created with their full node list up front, which is all any of this ever needed.
Dropped noexcept from two functions that touch raw game memory, per FileEx's feedback on multitheftauto#5101. Removed an unused GetTrainTrack accessor, a stray forward declaration, and an old comment from before this branch existed.
Forgot to drop this when the native track call went in; it was never read anywhere past being stored. Checked and there's no cheap way to make it do something either: the native train code always treats tracks as closed loops, so supporting this for real would mean patching CTrain::ProcessControl, not just wiring a bool through.
c6f29df to
dd68f3b
Compare
|
What happens if the tracks intersect or cross each other? I remember there used to be an issue where GTA would end up in an infinite loop internally. Also, is it possible to switch from one track to another while the train is moving, like a railway switch? |
Crossing tracks is fine, in both senses: two separate tracks crossing don't interact at all, each one is its own independent node list. And a single track crossing itself doesn't confuse the game either, since it walks the nodes in the order you gave them, not by where they sit in space. The infinite loop you're remembering is #747; went and checked it against the actual game code. Turns out it's already fixed, since 2019 (adfbd69), just never got closed. The native wraparound in Switching tracks while moving already works, that's meant to let scripters build their own junctions out of multiple tracks. Keeping this PR small on purpose, so it stays easy to review and merge. I originally wanted to throw in more functions, fix train sync, node editing, and a few other things, but that was turning into way too many changes for one PR. Kept it to just this for now. |
|
|
Done. 😀 |
Trains on this branch only worked for whoever was driving them. The client dropped the vehicle from its sync list the moment you climbed out, so the server kept the position you left it at and boarding again failed the distance check and snapped the train back there. The receiving side also interpolated a world position onto a train the rail distance was already placing, which made it jump off the track and back. The new files pick up the standard header format while we're here.
|
Good one! |
Summary
Adds custom train track support to MTA:SA. Scripts can build a track from any list of points and put a train on it, instead of being limited to the game's 4 built-in tracks.
New functions:
createTrainTrack(nodePositions): creates a track element from a list of positions.setTrainTrack(vehicle, trainTrack)/getTrainTrack(vehicle): now also accept custom track elements, not just the 4 default tracks.getDefaultTrack(trackID): returns the element for one of the game's 4 built-in tracks.A custom track runs through the game's own train code unmodified, same physics, audio, station and derail logic, just fed a different set of track data. Track IDs are a byte (0xFF reserved for "no track"), so up to 255 tracks can exist at once.
Video
Motivation
GTA:SA only ships 4 built-in train tracks. Scripters wanting custom rail-based movement (roller coasters, custom subway systems, etc.) had no way to do this with real train physics, animation and audio, only workarounds like attaching vehicles to moving objects, which don't behave like an actual train.
This picks up where #250 (by qaisjp) left off; that PR aimed to do the same thing but was closed without being finished. Resolves #388.
Test plan
Tested manually with a local test resource. Verified: creating a custom track and spawning a train on it; a closed loop with several nodes and real curves, not just straight segments; a track with height changes along its length, to check the train handles slopes correctly; switching a train between a custom track and one of the game's default tracks; destroying a custom track while a train is on it, which derails the train; and a player joining or reconnecting mid-session still receiving existing custom tracks.
traintracktest.zip
Checklist