🎮 Controller Configuration Format

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.


📦 Overview

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:

TypeDescription
MomentarySingle-press controls that trigger on threshold crossing
ToggleOn/off state controls that alternate between actions
LinearMulti-threshold controls for fine-grained behavior
DirectControlDirect value mapping to the game
SyncControlSynchronized control via keypresses
ApiControlHTTP API-based value mapping
VirtualActionVirtual controls actions

Each assignment type has a specific use case and behavior, described below.


🧩 Assignment Types

🔘 Momentary

Used for buttons that act while held.

{
  "type": "momentary",
  "threshold": 0.5,
  "action_activate": { ... },
  "action_deactivate": { ... }
}

🔁 Toggle

Used for toggle switches that alternate between two states.

{
  "type": "toggle",
  "threshold": 0.5,
  "action_activate": { ... },
  "action_deactivate": { ... }
}

📈 Linear

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": { ... } }
  ]
}

🎚️ DirectControl

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
}

Options

NameDescription
holdWhether to continuously hold this value. Useful for levers which automatically reset. (such as the Tube Deadman or some brake levers)
notifyWhether to enable the in-game notifier when changing values to display the current value (defaults to true but can be explicitly disabled)

🧭 SyncControl

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" }
}

🎚️ ApiControl

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
  }
}

⚙️ Action Types

Each assignment triggers an action when activated (and optionally when deactivated). Actions can be:

🖱️ Key Presses

{
  "keys": "W",
  "press_time": 0.1,
  "wait_time": 0.05
}

🎛️ Direct Control Action

{
  "controls": "Throttle1",
  "value": 0.5,
  "hold": false,
  "relative": false
}

🎛️ Api Control Action

{
  "controls": "Throttle1",
  "api_value": 0.5
}

🎛️ Virtual Action

{
  "type": "virtual",
  "control": "virtual:MyVirtualControl",
  "value": 0.1
}

🔧 Input Value Mapping

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
}

📏 Step Thresholds

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 }
    ]
  }
}

Threshold Properties

PropertyDescriptionRequired
thresholdThe actual threshold value where the step begins✅ Yes
threshold_endThe end value for range-based thresholds (optional)No
threshold_toleranceThe tolerance around the threshold (optional)No

How It Works

  1. 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).

  2. 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.

  3. 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.

  4. Default Tolerance: If no tolerance is specified, a default tolerance is calculated based on the number of steps (approximately half the step size).

  5. 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.

Use Cases


🔁 Conditional Assignments

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.


🏗️ Profile Structure

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"
  ]
}

Root Properties

PropertyDescriptionRequired
nameProfile name✅ Yes
extendsProfile to extend fromNo
auto_selectAuto-detection supportNo
controlsArray of control definitionsNo
controllerController-specific infoNo
rail_class_informationSupported rail classesNo

Controller Section

The controller section contains controller-specific information. It is optional and is mostly available for profile sharing.

"controller": {
  "usb_id": "...",
  "mapping": { ... },
  "calibration": { ... }
}

Rail Class Information

The rail_class_information section is an array of supported rail class names:

"rail_class_information": [{ "class_name": "..." }]

✅ Best Practices


Happy simming! 🚂