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.5,
"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)Used for toggle switches that alternate between two states.
{
"type": "toggle",
"threshold": 0.5,
"action_activate": { ... },
"action_deactivate": { ... }
}
action_activate.action_deactivate.Used for analog levers or sliders with multiple threshold points.
{
"type": "linear",
"thresholds": [
{ "threshold": 0.2, "action_activate": { ... }, "action_deactivate": { ... } },
{ "threshold": 0.7, "action_activate": { ... }, "action_deactivate": { ... } }
]
}
Maps an analog controller input to a continuous value in-game.
{
"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.| Name | Description |
|---|---|
hold | Whether to continuously hold this value. Useful for levers which automatically reset. (such as the Tube Deadman or some brake levers) |
notify | Whether to enable the in-game notifier when changing values to display the current value (defaults to true but can be explicitly disabled) |
An alternative to DirectControl for locomotives that don't work with direct control.
{
"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" }
}
Maps an analog controller input to a continuous value in-game using the HTTP API.
{
"type": "api_control",
"controls": "Throttle1",
"input_value": {
"min": 0.0,
"max": 1.0,
"invert": true
}
}
step or steps to quantize values.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
}
{
"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
}
min / max: Range of values.step: Optional increment size.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.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 | ✅ 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 to note that the number of step_thresholds should match 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.Happy simming! 🚂