Knowledge from the fieldRedM

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

learning:35

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 []. So REGISTER_TRACK_JUNCTION has 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

Back to documentation