Diverting a player-driven train at switches: CFX junction API fails on replace_traintrack_file maps; brute-force SetTrainTrackJunctionSwitch over ALL indices is what works
Context
You want a player-driven mission train (_CREATE_MISSION_TRAIN 0xC239DBD9A57D2A71) to take a different branch at a track fork. The obvious modern path is the CFX custom-junction API: REGISTER_TRACK_JUNCTION (0x35F743B5) + SET_TRACK_JUNCTION_ACTIVE (0x537B449D), keyed on the train's current track index/node.
The trap (verified on a server using replace_traintrack_file with custom/3rd-party tracks, build 1491)
The CFX junction API is silently dead on replaced tracks:
GET_TRACK_NODE_COUNT(0x896A0C11) returns 0 for every track index 0–31.GET_TRAIN_CURRENT_TRACK_NODE(0xE015E854) is pinned at 0;GET_TRACK_INDEX_OF_TRAIN(0x865FEC2FA899F29C) pinned at 1 — neither reports live position as the train drives across forks.GET_ALL_TRACK_JUNCTIONS(0x81A08523) returns[]. SoREGISTER_TRACK_JUNCTIONhas no node graph to bind to and never diverts the train.
Also note: CFX int-return natives in the Lua runtime need a Citizen.ResultAsInteger() hint or they come back as false — but even with that, the underlying node data is empty here.
A second trap: the R* native _SET_TRAIN_TRACK_JUNCTION_SWITCH (0xE6C5E2125EB210C1) called for ONE specific (trackHash, junctionIndex) does nothing — toggling just FREIGHT_GROUP junction 1 or 2 had no effect on the train's route.
The fix (verified — diverted a player-driven train from one branch to the other at a live fork)
Use the Lua named wrapper SetTrainTrackJunctionSwitch(trackHash, junctionIndex, toggle) and brute-force every junction index across every track hash to the same state. There is no per-junction targeting; it's a global "all switches on / all switches off" toggle:
local tracks = {'FREIGHT_GROUP','TRAINS3','BRAITHWAITES2_TRACK_CONFIG',
'TRAINS_OLD_WEST01','TRAINS_OLD_WEST03','TRAINS_NB1','TRAINS_INTERSECTION1_ANN'}
-- add any custom track config names your map adds
local function setAllSwitches(toggle)
for counter = 0, 49 do
for _, model in ipairs(tracks) do
SetTrainTrackJunctionSwitch(joaat(model), counter, toggle)
end
end
end
SetAllJunctionsCleared() (no args) resets all junctions to default.
Use the named wrapper, not Citizen.InvokeNative(0xE6C5E2125EB210C1, ...) — the single-index InvokeNative calls were what no-op'd in testing. The brute-force-all approach is the only thing that produced a visible divert.
Linked natives
_CREATE_MISSION_TRAIN(0xC239DBD9A57D2A71)GET_TRACK_INDEX_OF_TRAIN(0x865FEC2FA899F29C)_SET_TRAIN_TRACK_JUNCTION_SWITCH(0xE6C5E2125EB210C1)
Tags: trains, vehicles, track-junction, railroad, switch, mission-train
Category: natives
Source: scandi-railroad-poc
Created: 2026-05-22T17:57:38.274Z