This document describes the structure and semantics of the configuration system used to map game controllers (e.g., joysticks, gamepads) to controls in Train Sim World and Train Simulator Classic. It is designed to be flexible, extensible, and friendly to both analog and digital input devices.
Each control on a game controller can be assigned an action. Assignments describe when and how the actions are triggered based on the input. Actions describe what happens when triggered.
All assignments conform to a top-level enum ControllerProfileControlAssignment, which contains the following variants:
| Type | Description |
|---|---|
Momentary | Single-press controls that trigger on threshold crossing |
Toggle | On/off state controls that alternate between actions |
Linear | Multi-threshold controls for fine-grained behavior |
DirectControl | Direct value mapping to the game |
SyncControl | Synchronized control via keypresses |
ApiControl | HTTP API-based value mapping |
VirtualAction | Virtual controls actions |
Each assignment type has a specific use case and behavior, described below.
Used for buttons that act while held.
{
"type": "momentary",
"threshold": 0.9,
"match": "exceeds",
"action_activate": { ... },
"action_deactivate": { ... }
}
threshold.threshold. (optional - by default if the action_activate defines a keystroke to be held; it will be released automatically when releasing the gamepad control)| Property | Description | Required |
|---|---|---|
threshold | The threshold value that triggers the action | ✅ Yes |
match | How to interpret the threshold. Defaults to "exceeds" where the action is executed when the value exceeds the threshold. Can also be set to "equals" for an exact comparison | No |
action_activate | The action to execute when the threshold is exceeded | ✅ Yes |
action_deactivate | The action to execute when the threshold is no longer exceeded. Defaults to releasing the previously activated key(s) | No |
conditions | Optional conditions that must be met for the assignment to execute | No |
rail_class_information | Optional list of rail classes this assignment applies to | No |
{
"type": "momentary",
"threshold": 0.9,
"action_activate": {
"keys": "h"
}
}
{
"type": "momentary",
"threshold": 0.9,
"action_activate": {
"keys": "w",
"press_time": 0.2,
"wait_time": 0.2
}
}
{
"type": "momentary",
"threshold": 0.9,
"action_activate": {
"controls": "Throttle_{SIDE}",
"value": 1.0,
"hold": true,
"enable_api_fallback": true
},
"action_deactivate": {
"controls": "Throttle_{SIDE}",
"value": 0.0,
"enable_api_fallback": true
}
}
Momentary assignments can be conditioned on other control values:
{
"type": "momentary",
"threshold": 0.9,
"conditions": [
{
"control": "mylever",
"operator": "gte",
"value": 0.5
}
],
"action_activate": {
"keys": "h"
}
}
In the above example, the assignment will only execute if mylever exceeds 0.5.
Momentary assignments support the following action types:
hold, notify, enable_api_fallback, use_normalized, max_change_ratehold, max_change_rateUsed for toggle switches that alternate between two states.
{
"type": "toggle",
"threshold": 0.9,
"match": "exceeds",
"action_activate": { ... },
"action_deactivate": { ... }
}
action_activate.action_deactivate.| Property | Description | Required |
|---|---|---|
threshold | The threshold value that triggers the toggle action | ✅ Yes |
match | How to interpret the threshold. Defaults to "exceeds" where the action is executed when the value exceeds the threshold. Can also be set to "equals" for an exact comparison | No |
action_activate | The action to execute on the first toggle (when transitioning from off to on) | ✅ Yes |
action_deactivate | The action to execute on the second toggle (when transitioning from on to off) | ✅ Yes |
conditions | Optional conditions that must be met for the assignment to execute | No |
rail_class_information | Optional list of rail classes this assignment applies to | No |
The Toggle assignment maintains internal state to track whether it has been activated. The behavior follows this logic:
action_activate is executed.action_deactivate is executed.{
"type": "toggle",
"threshold": 0.9,
"action_activate": {
"keys": "x"
},
"action_deactivate": {
"keys": "shift+x"
}
}
Pressing the button once triggers x, pressing again triggers shift+x.
{
"type": "toggle",
"threshold": 0.9,
"action_activate": {
"controls": "MarkerLight_R",
"value": 1
},
"action_deactivate": {
"controls": "MarkerLight_R",
"value": 0.5
}
}
Pressing the button once sets the marker light to 1, pressing again sets it to 0.5.
{
"type": "toggle",
"threshold": 0.9,
"action_activate": {
"keys": "w",
"press_time": 0.2,
"wait_time": 0.2
},
"action_deactivate": {
"keys": "s",
"press_time": 0.2,
"wait_time": 0.2
}
}
Toggle assignments can be conditioned on other control values:
{
"type": "toggle",
"threshold": 0.9,
"conditions": [
{
"control": "mylever",
"operator": "gte",
"value": 0.5
}
],
"action_activate": {
"keys": "h"
},
"action_deactivate": {
"keys": "shift+h"
}
}
In the above example, the toggle will only execute if mylever exceeds 0.5.
{
"type": "toggle",
"threshold": 0.5,
"match": "equals",
"action_activate": {
"keys": "a"
},
"action_deactivate": {
"keys": "b"
}
}
This toggle only triggers when the input value exactly equals 0.5.
Toggle assignments support the following action types:
hold, notify, enable_api_fallback, use_normalized, max_change_ratehold, max_change_rateUsed for analog levers or sliders with multiple threshold points.
{
"type": "linear",
"neutral": 0.5,
"thresholds": [
{ "value": 0.2, "action_activate": { ... } },
{ "value": 0.5, "action_activate": { ... } },
{ "value": 0.8, "action_activate": { ... } }
]
}
value_end and value_step.| Property | Description | Required |
|---|---|---|
thresholds | Array of threshold definitions with actions | ✅ Yes |
neutral | Optional neutral/idle value for value normalization (e.g., 0.5 for centering 0-1 range to -1 to 1) | No |
conditions | Optional conditions that must be met for the assignment to execute | No |
rail_class_information | Optional list of rail classes this assignment applies to | No |
| Property | Description | Required |
|---|---|---|
value | The threshold value to exceed. Can use named references from calibration | ✅ Yes |
value_end | End value for auto-generating thresholds (exclusive) | No |
value_step | Step increment for auto-generating thresholds between value and value_end | No |
action_activate | The action to execute when the threshold is exceeded | ✅ Yes |
action_deactivate | The action to execute when the value falls below the threshold | No |
When value_end and value_step are provided, the system automatically generates multiple thresholds between value and value_end:
{
"type": "linear",
"thresholds": [
{
"value": 0.3,
"value_end": 0.6,
"value_step": 0.05,
"action_activate": { "keys": "w" }
}
]
}
This generates thresholds at: 0.3, 0.35, 0.40, 0.45, 0.50, 0.55 (all triggering the same action).
The neutral property allows you to map the input value range around a neutral point. This is useful for levers that have a centered neutral position:
{
"type": "linear",
"neutral": 0.5,
"thresholds": [
{ "value": -0.5, "action_activate": { "keys": "s" } },
{ "value": 0.5, "action_activate": { "keys": "w" } }
]
}
With neutral: 0.5, a raw input of 0.0 becomes -1.0 (neutralized), and 1.0 becomes 1.0.
The Linear assignment uses asymmetric threshold logic (signifying applying power as opposed to mathmatical operations):
Linear assignments track which thresholds are currently exceeding and which were previously passed:
action_activate is triggered.action_deactivate is triggered if defined.action_deactivate is not defined but action_activate uses keys, the keys are released.{
"type": "linear",
"thresholds": [
{ "value": 0.2, "action_activate": { "keys": "a" } },
{ "value": 0.5, "action_activate": { "keys": "d" } },
{ "value": 0.8, "action_activate": { "keys": "f" } }
]
}
As the lever moves from 0 to 1:
adf{
"type": "linear",
"neutral": 0.5,
"thresholds": [
{ "value": -0.5, "action_activate": { "keys": "s" } },
{ "value": 0.5, "action_activate": { "keys": "w" } }
]
}
With a neutral value of 0.5, the lever's centered position (0.5 raw) is treated as 0 (neutralized), allowing negative and positive threshold values.
{
"type": "linear",
"thresholds": [
{
"value": 0.0,
"value_end": 0.5,
"value_step": 0.1,
"action_activate": { "keys": "1" }
},
{
"value": 0.6,
"value_end": 1.0,
"value_step": 0.1,
"action_activate": { "keys": "2" }
}
]
}
This creates 6 thresholds for each range (0.0, 0.1, 0.2, 0.3, 0.4, 0.5 and 0.6, 0.7, 0.8, 0.9, 1.0), all triggering the same action within each range.
Linear assignments can be conditioned on other control values:
{
"type": "linear",
"conditions": [
{
"control": "mylever",
"operator": "gte",
"value": 0.5
}
],
"thresholds": [
{ "value": 0.2, "action_activate": { "keys": "a" } },
{ "value": 0.7, "action_activate": { "keys": "d" } }
]
}
In the above example, the assignment will only execute if mylever exceeds 0.5.
Linear assignments support the following action types:
hold, notify, enable_api_fallback, use_normalized, max_change_ratehold, max_change_rateMaps an analog controller input to a continuous value in-game. This is the primary method for controlling cab levers and other continuous controls in Train Sim World, Train Simulator Classic and Wonders of Sodor.
{
"type": "direct_control",
"controls": "Throttle1",
"input_value": {
"min": 0.0,
"max": 1.0,
"invert": true
},
"notify": true
}
step or steps to quantize values.{SIDE} placeholder to automatically select the correct side of the cab. This is specifically for controls named Throttle_F or Throttle_B where the F and B mark the side of the cab.
Note: some locomotives don't use the F and R placeholders. The Czech route locomotives for example use 1 and 2. To support this you can use the expanded placeholder which defines which characters to use for front and back: {SIDE:F:B} [example with 1/2: {SIDE:1:2}]| Name | Description | Required |
|---|---|---|
controls | The UE4SS control identifier to control (e.g., Throttle, AutomaticBrake, IndependentBrake) | ✅ Yes |
input_value | Defines the input value constraints and mapping | ✅ Yes |
control_range | Remaps a partial input range to a full 0-1 or 0,-1 output range | No |
hold | Whether to continuously hold this value. Useful for levers which automatically reset (such as the Deadman or some brake levers) | No |
use_normalized | Whether to use normalized values (-1 to 1) instead of raw values (0 to 1) [rarely used] | No |
enable_api_fallback | Whether to enable fallback to the TSW API if direct control is unavailable | No |
notify | Whether to enable the in-game notifier when changing values to display the current value (defaults to true) | No |
conditions | Optional conditions that must be met for the assignment to execute | No |
rail_class_information | Optional list of rail classes this assignment applies to | No |
| Name | Description | Required |
|---|---|---|
min | The minimum reachable value in the game cab | ✅ Yes |
max | The maximum reachable value in the game cab | ✅ Yes |
step | The step increment to auto-generate discrete values (alternative to steps) | No |
steps | Array of discrete values. Can include null to create free range zones between detents | No |
invert | Whether to reverse the input value direction (mapping 0-1 to 1-0) | No |
max_change_rate | The maximum rate at which the control value can change per frame (rarely necessary) | No |
step_thresholds | Custom threshold definitions for each step (see step_thresholds) | No |
The step_thresholds option allows you to define custom threshold values for each step in a direct control mapping. This is particularly useful when you want to remap a continuous analog input to match a notched control (e.g., a throttle with detents) or when you need to create custom value ranges.
{
"type": "direct_control",
"controls": "Throttle1",
"input_value": {
"min": 0.1,
"max": 1.0,
"steps": [0.1, null, 1.0],
"step_thresholds": [
{ "threshold": 0.2, "threshold_tolerance": 0.05 },
{ "threshold": 0.5, "threshold_end": 0.6, "threshold_tolerance": 0.03 },
{ "threshold": 0.8 }
]
}
}
Threshold Properties:
| Property | Description | Required |
|---|---|---|
threshold | The actual threshold value where the step begins (can use named references from calibration) | ✅ Yes |
threshold_end | The end value for range-based thresholds (optional) | No |
threshold_tolerance | The tolerance around the threshold (optional) | No |
How It Works:
Single Threshold: When only threshold is specified, it defines a single point value. The control will snap to this value when the input matches the threshold (+- the tolerance).
Range Threshold: When both threshold and threshold_end are specified, it defines a range. The control will accept any input value within this range and map it proportionally. This is mostly useful for free range steps.
Tolerance: The threshold_tolerance defines how much deviation from the threshold is acceptable. For example, if threshold is 0.5 and threshold_tolerance is 0.05, the control will accept input values between 0.45 and 0.55.
Default Tolerance: If no tolerance is specified, a default tolerance is calculated based on the number of steps (approximately half the step size).
Free Range Zones: When a step is marked as a free range zone (using null in the steps array), it gets special handling with no tolerance by default. The threshold defines the boundaries of the free range.
Note: The number of step_thresholds should match the number of steps as each step threshold definition corresponds to each step.
Use Cases:
The control_range property allows you to remap a partial input range to a full 0-1 or 0,-1 output range. This can be useful for mapping a single physical lever or control to multiple in-game controls while retaining a full 0-1 direct control range.
{
"type": "direct_control",
"controls": "Throttle1",
"input_value": {
"min": 0.0,
"max": 1.0
},
"control_range": {
"start": 0.2,
"end": 0.8
}
}
In the above example, an input value of 0.2 maps to 0.0, 0.8 maps to 1.0, and values outside this range are clamped. This effectively remaps the 0.2-0.8 input range to the full 0-1 output range.
Control Range Properties:
| Property | Description | Required |
|---|---|---|
start | The start value of the input range to remap (depending on value direction, this is the min or max) | ✅ Yes |
end | The end value of the input range to remap | ✅ Yes |
The steps array can include null values to create free range zones between detents. This is useful for controls that are partly notched and partly free.
{
"type": "direct_control",
"controls": "AutomaticBrake1",
"input_value": {
"min": 0,
"max": 0.8,
"steps": [0, 0.125, null, 0.6, 0.7, 0.8]
}
}
In the above example:
null creates a free range zone between 0.125 and 0.6Direct Control assignments can be conditioned on other control values, allowing the same physical control to map to different game controls based on conditions.
{
"type": "direct_control",
"conditions": [
{
"control": "mylever",
"operator": "lt",
"value": 0.5
}
],
"controls": "IndependentBrake",
"input_value": {
"min": 0.25,
"max": 1,
"invert": true
}
}
In the above example, the IndependentBrake is only controlled when the Reverser (mylever) is less than 0.5. When the Reverser exceeds 0.5, a separate Direct Control assignment would map to DynamicBrake.
{
"type": "direct_control",
"controls": "Throttle_{SIDE}",
"input_value": {
"min": 0,
"max": 1
}
}
{
"type": "direct_control",
"controls": "AutomaticBrake_{SIDE}",
"input_value": {
"min": 0,
"max": 0.8,
"steps": [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.8]
}
}
An alternative to DirectControl for locomotives that don't work with direct control. Unlike DirectControl which directly sets values, SyncControl uses a state machine approach that reads the current in-game state and uses keypresses to incrementally reach the desired target value.
Note: this control mode is generally not recommended anymore since there is no reason to use it in favor of direct control or API control.
How It Works:
sync_control_valueaction_increase or action_decrease) to move toward the target-1 (decreasing), 0 (idle), 1 (increasing){
"type": "sync_control",
"identifier": "Reverser1",
"input_value": {
"min": -1.0,
"max": 1.0,
"steps": [-1.0, 0.0, 1.0]
},
"action_increase": { "keys": "PageUp" },
"action_decrease": { "keys": "PageDown" }
}
| Name | Description | Required |
|---|---|---|
type | Must be "sync_control" | ✅ Yes |
identifier | The sync control identifier to control; can be identified using the Cab Debugger | ✅ Yes |
input_value | Defines the input value constraints and mapping (same as DirectControl) | ✅ Yes |
action_increase | The key action to press when increasing the control value | ✅ Yes |
action_decrease | The key action to press when decreasing the control value | ✅ Yes |
control_range | Remaps a partial input range to a full 0-1 or 0,-1 output range | No |
conditions | Optional conditions that must be met for the assignment to execute | No |
rail_class_information | Optional list of rail classes this assignment applies to | No |
SyncControl uses the same input_value properties as DirectControl:
| Name | Description | Required |
|---|---|---|
min | The minimum reachable value in the game cab | ✅ Yes |
max | The maximum reachable value in the game cab | ✅ Yes |
step | The step increment to auto-generate discrete values | No |
steps | Array of discrete values. Can include null to create free range zones between detents | No |
invert | Whether to reverse the input value direction | No |
max_change_rate | The maximum rate at which the control value can change per frame | No |
step_thresholds | Custom threshold definitions for each step (see Step Thresholds) | No |
SyncControl maintains an internal state machine for each controlled identifier:
Moving States:
-1 (Decreasing): The control is actively decreasing toward the target0 (Idle): The control is at the target value or no movement is needed1 (Increasing): The control is actively increasing toward the targetMargin of Error:
The system uses a margin of error of 0.005 to determine when to stop moving. This prevents micro-adjustments and ensures smooth stopping at target values.
State Transitions:
target > current and abs(target - current) > 0.005 and not already movingtarget < current and abs(target - current) > 0.005 and not already movingMaps an analog controller input to a continuous value in-game using the HTTP API. Runs at 15fps processing loop.
{
"type": "api_control",
"controls": "Throttle1",
"input_value": {
"min": 0.0,
"max": 1.0
}
}
| Property | Description | Required |
|---|---|---|
controls | The direct/api control name to control; can be identified using the Cab Debugger | ✅ Yes |
input_value | Defines the input value constraints | ✅ Yes |
control_range | Remaps partial input ranges to full 0-1 or 0,-1 output ranges | No |
hold | Boolean to keep the control active after input is released | No |
| Property | Description | Required |
|---|---|---|
min | The minimum reachable value in the game cab | ✅ Yes |
max | The maximum reachable value in the game cab | ✅ Yes |
max_change_rate | The maximum rate at which this control can change (useful for realistic throttle simulation) | No |
step / steps | Quantize values to discrete steps or define free range zones | No |
invert | Whether to invert the input value before calculating the game value | No |
| Property | Description | Required |
|---|---|---|
start | The starting value of the partial range | ✅ Yes |
end | The ending value of the partial range | ✅ Yes |
Processing Behavior:
max_change_ratehold: true: Control maintains state after input is releasedExample with ControlRange:
{
"type": "api_control",
"controls": "Throttle1",
"input_value": {
"min": 0.0,
"max": 1.0
},
"control_range": {
"start": 0.3,
"end": 0.7
}
}
Maps the partial range 0.3-0.7 to full 0-1 output range
Example with MaxChangeRate:
{
"type": "api_control",
"controls": "Throttle1",
"input_value": {
"min": 0.0,
"max": 1.0,
"max_change_rate": 0.1
}
}
Throttle can only change by 0.1 per frame, required for certain locomotives / controls - but rare
Example with Hold:
{
"type": "api_control",
"controls": "Brake1",
"input_value": {
"min": 0.0,
"max": 1.0
},
"hold": true
}
Brake maintains applied state after button release
Each assignment triggers an action when activated (and optionally when deactivated). Actions can be:
{
"keys": "W",
"press_time": 0.1,
"wait_time": 0.05
}
{
"controls": "Throttle1",
"value": 0.5,
"hold": false,
"relative": false
}
| Property | Description | Required |
|---|---|---|
controls | The UE4SS control identifier to control | ✅ Yes |
value | The value to send to the control | ✅ Yes |
max_change_rate | The maximum rate at which the control value can change per frame | No |
relative | Whether to use the value as a relative adjustment instead of absolute | No |
hold | Whether to continuously hold the value by sending it repeatedly | No |
use_normalized | Whether to use normalized values instead of raw values (rarely used) | No |
notify | Whether to enable the in-game notifier when changing values | No |
enable_api_fallback | Whether to enable fallback to the TSW API if direct control is unavailable | No |
{
"controls": "Throttle1",
"api_value": 0.5
}
{
"type": "virtual",
"control": "virtual:MyVirtualControl",
"value": 0.1
}
Used by DirectControl, SyncControl, and ApiControl to map axis input to control values.
{
"min": -1.0,
"max": 1.0,
"step": 0.1,
"steps": [0.0, 0.2, null, 0.5, null, 1.0],
"invert": true,
"max_change_rate": 0.05
}
min / max: Range of values.step: Optional increment size for auto-generating discrete values.steps: Optional list of discrete valid values. Can be used with null values to create zones of free motion between detents.invert: Whether to reverse the axis direction.max_change_rate: The maximum rate at which the control value can change per frame (optional).step_thresholds: Optional array of threshold definitions for remapping values to match notched or stepped controls.The step_thresholds option allows you to define custom threshold values for each step in a direct control mapping. This is particularly useful when you want to remap a continuous analog input to match a notched control (e.g., a throttle with detents) or when you need to create custom value ranges.
{
"type": "direct_control",
"controls": "Throttle1",
"input_value": {
"min": -1.0,
"max": 1.0,
"steps": [0.1, null, 1.0],
"step_thresholds": [
{ "threshold": 0.2, "threshold_tolerance": 0.05 },
{ "threshold": 0.5, "threshold_end": 0.6, "threshold_tolerance": 0.03 },
{ "threshold": 0.8 }
]
}
}
| Property | Description | Required |
|---|---|---|
threshold | The actual threshold value where the step begins (can use named references from calibration) | ✅ Yes |
threshold_end | The end value for range-based thresholds (optional) | No |
threshold_tolerance | The tolerance around the threshold (optional) | No |
Single Threshold: When only threshold is specified, it defines a single point value. The control will snap to this value when the input matches the threshold (+- the tolerance).
Range Threshold: When both threshold and threshold_end are specified, it defines a range. The control will accept any input value within this range and map it proportionally. This is mostly useful for free range steps.
Tolerance: The threshold_tolerance defines how much deviation from the threshold is acceptable. For example, if threshold is 0.5 and threshold_tolerance is 0.05, the control will accept input values between 0.45 and 0.55.
Default Tolerance: If no tolerance is specified, a default tolerance is calculated based on the number of steps (approximately half the step size).
Free Range Zones: When a step is marked as a free range zone (using null in the steps array), it gets special handling with no tolerance by default. The threshold defines the boundaries of the free range.
Note: It is important that the number of step_thresholds matches the number of steps as each step threshold definition corresponds to each step.
It is also possible to only execute assignments depending on one or more conditions. This can be used to create multi-key assignments (e.g., the action of a button changes depending on the position of a lever).
This can be added to any assignment using the conditions key:
{
"type": "momentary",
"conditions": [
{
"control": "mylever",
"operator": "gte",
"value": 0.5
}
]
}
In the above example, the assignment will only execute if mylever exceeds 0.5. At this time the supported operators are eq,gte, lte, gt, and lt.
A controller profile is defined in a JSON file with the following structure:
{
"name": "MyProfile",
"extends": "BaseProfile",
"auto_select": true,
"controls": [
{
"name": "Button1",
"type": "momentary",
"threshold": 0.5,
"action_activate": { "keys": "H" },
"action_deactivate": { "keys": "Shift+H" }
}
],
"controller": {
"usb_id": "0x1234",
"mapping": "Standard",
"calibration": { ... }
},
"rail_class_information": [
"Class 40",
"Class 42",
"Class 43"
]
}
| Property | Description | Required |
|---|---|---|
name | Profile name | ✅ Yes |
extends | Profile to extend from | No |
auto_select | Auto-detection support | No |
controls | Array of control definitions | No |
controller | Controller-specific info | No |
rail_class_information | Supported rail classes | No |
The controller section contains controller-specific information. It is optional and is mostly available for profile sharing.
"controller": {
"usb_id": "...",
"mapping": { ... },
"calibration": { ... }
}
usb_id: The USB device ID of the controllermapping: The mapping profile used for the controllercalibration: Calibration data for the controllerThe rail_class_information section is an array of supported rail class names:
"rail_class_information": [{ "class_name": "..." }]
DirectControl for stable, high-resolution mappings, especially lever controls.ApiControl if you are unable to or do not want to use DirectControl (ApiControl is less flexible and is less performant, but still provides a near direct control option).SyncControl if you want a direct control-like experience but want to use keybindings.Linear for fine-grained, manually configured lever behavior.Momentary for temporary actions like horn or bell.Toggle for switches with two states.VirtualAction for controlling virtual controls.DirectControl as an action within Momentary or Toggle assignments for discrete control value changes (e.g., setting brake levels with buttons).Happy simming! 🚂