Files
airplane-mode/scripts/hotas_mapper.gd

316 lines
9.2 KiB
GDScript3
Raw Normal View History

extends CanvasLayer
@export var jet_path: NodePath
@export var config_path: String = "user://hotas_mapping.cfg"
@export var axis_capture_threshold: float = 0.35
@export var toggle_action: String = "toggle_hotas_mapper"
@export var panel_position: Vector2 = Vector2(12, 12)
@export var panel_size: Vector2 = Vector2(520, 360)
const AXIS_CONFIG = [
{
"name": "Roll",
"axis_prop": "roll_axis",
"invert_prop": "roll_invert",
"device_prop": "roll_device_id",
},
{
"name": "Pitch",
"axis_prop": "pitch_axis",
"invert_prop": "pitch_invert",
"device_prop": "pitch_device_id",
},
{
"name": "Yaw",
"axis_prop": "yaw_axis",
"invert_prop": "yaw_invert",
"device_prop": "yaw_device_id",
},
{
"name": "Throttle",
"axis_prop": "throttle_axis",
"invert_prop": "throttle_invert",
"signed_prop": "throttle_signed",
"device_prop": "throttle_device_id",
},
{
"name": "Strafe",
"axis_prop": "strafe_axis",
"invert_prop": "strafe_invert",
"device_prop": "strafe_device_id",
},
{
"name": "Lift",
"axis_prop": "lift_axis",
"invert_prop": "lift_invert",
"device_prop": "lift_device_id",
},
]
var _jet: Node
var _panel: PanelContainer
var _status_label: Label
var _device_label: Label
var _rows := {}
var _listening_axis: String = ""
func _ready() -> void:
_jet = get_node_or_null(jet_path)
_build_ui()
_load_config()
_refresh_labels()
func _unhandled_input(event: InputEvent) -> void:
if _is_toggle_event(event):
visible = not visible
if not visible:
_listening_axis = ""
_refresh_labels()
get_viewport().set_input_as_handled()
func _input(event: InputEvent) -> void:
if not visible:
return
if _listening_axis.is_empty():
return
if event is InputEventJoypadMotion:
if abs(event.axis_value) < axis_capture_threshold:
return
_apply_axis_mapping(_listening_axis, event.axis, event.device)
_listening_axis = ""
_status_label.text = "Axis captured."
_refresh_labels()
func _build_ui() -> void:
_panel = PanelContainer.new()
_panel.name = "HotasMapper"
_panel.anchor_left = 0.0
_panel.anchor_top = 0.0
_panel.anchor_right = 0.0
_panel.anchor_bottom = 0.0
_panel.offset_left = panel_position.x
_panel.offset_top = panel_position.y
_panel.offset_right = panel_position.x + panel_size.x
_panel.offset_bottom = panel_position.y + panel_size.y
add_child(_panel)
var root := VBoxContainer.new()
root.size_flags_vertical = Control.SIZE_EXPAND_FILL
root.size_flags_horizontal = Control.SIZE_EXPAND_FILL
root.add_theme_constant_override("separation", 8)
_panel.add_child(root)
var header := HBoxContainer.new()
header.size_flags_horizontal = Control.SIZE_EXPAND_FILL
root.add_child(header)
var title := Label.new()
title.text = "HOTAS Axis Mapper"
header.add_child(title)
var spacer := Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
header.add_child(spacer)
var close_button := Button.new()
close_button.text = "Close"
close_button.pressed.connect(_on_close_pressed)
header.add_child(close_button)
_device_label = Label.new()
_device_label.text = _get_device_text()
root.add_child(_device_label)
_status_label = Label.new()
_status_label.text = "Click Map, then move the desired axis. Toggle: %s" % _get_toggle_hint()
root.add_child(_status_label)
for config in AXIS_CONFIG:
root.add_child(_build_axis_row(config))
func _build_axis_row(config: Dictionary) -> Control:
var row := HBoxContainer.new()
row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_theme_constant_override("separation", 8)
var name_label := Label.new()
name_label.text = config.name
name_label.custom_minimum_size = Vector2(120, 0)
row.add_child(name_label)
var map_button := Button.new()
map_button.text = "Map"
map_button.pressed.connect(_on_map_pressed.bind(config.axis_prop))
row.add_child(map_button)
var axis_label := Label.new()
axis_label.text = "Axis: ?"
axis_label.custom_minimum_size = Vector2(120, 0)
row.add_child(axis_label)
var device_label := Label.new()
device_label.text = "Dev: ?"
device_label.custom_minimum_size = Vector2(160, 0)
row.add_child(device_label)
var invert_check := CheckBox.new()
invert_check.text = "Invert"
invert_check.toggled.connect(_on_invert_toggled.bind(config.invert_prop))
row.add_child(invert_check)
var signed_check: CheckBox = null
if config.has("signed_prop"):
signed_check = CheckBox.new()
signed_check.text = "Signed"
signed_check.toggled.connect(_on_signed_toggled.bind(config.signed_prop))
row.add_child(signed_check)
_rows[config.axis_prop] = {
"axis_label": axis_label,
"device_label": device_label,
"map_button": map_button,
"invert_check": invert_check,
"signed_check": signed_check,
}
return row
func _on_map_pressed(axis_prop: String) -> void:
if _listening_axis == axis_prop:
_listening_axis = ""
_status_label.text = "Mapping cancelled."
else:
_listening_axis = axis_prop
_status_label.text = "Listening for %s axis..." % axis_prop
_refresh_labels()
func _on_close_pressed() -> void:
_listening_axis = ""
visible = false
func _on_invert_toggled(pressed: bool, invert_prop: String) -> void:
if _jet == null:
return
_jet.set(invert_prop, pressed)
_save_config()
func _on_signed_toggled(pressed: bool, signed_prop: String) -> void:
if _jet == null:
return
_jet.set(signed_prop, pressed)
_save_config()
func _apply_axis_mapping(axis_prop: String, axis_index: int, device_id: int) -> void:
if _jet == null:
return
_jet.set(axis_prop, axis_index)
if device_id >= 0:
var device_prop = _get_device_prop(axis_prop)
if not device_prop.is_empty():
_jet.set(device_prop, device_id)
_jet.set("joy_device_id", device_id)
_save_config()
_device_label.text = _get_device_text()
func _refresh_labels() -> void:
if _jet == null:
return
_device_label.text = _get_device_text()
for config in AXIS_CONFIG:
var axis_prop: String = config.axis_prop
var row = _rows.get(axis_prop, null)
if row == null:
continue
row["axis_label"].text = "Axis: %d" % int(_jet.get(axis_prop))
var device_prop: String = config.device_prop
var device_id: int = int(_jet.get(device_prop))
row["device_label"].text = _get_device_label(device_id)
var invert_prop: String = config.invert_prop
row["invert_check"].set_pressed_no_signal(bool(_jet.get(invert_prop)))
if config.has("signed_prop") and row["signed_check"] != null:
row["signed_check"].set_pressed_no_signal(bool(_jet.get(config.signed_prop)))
var map_button: Button = row["map_button"]
if _listening_axis == axis_prop:
map_button.text = "Listening..."
else:
map_button.text = "Map"
func _load_config() -> void:
if _jet == null:
return
var config := ConfigFile.new()
var err = config.load(config_path)
if err != OK:
return
for entry in AXIS_CONFIG:
var axis_prop: String = entry.axis_prop
if config.has_section_key("axes", axis_prop):
_jet.set(axis_prop, int(config.get_value("axes", axis_prop)))
var invert_prop: String = entry.invert_prop
if config.has_section_key("invert", invert_prop):
_jet.set(invert_prop, bool(config.get_value("invert", invert_prop)))
var device_prop: String = entry.device_prop
if config.has_section_key("devices", device_prop):
_jet.set(device_prop, int(config.get_value("devices", device_prop)))
if entry.has("signed_prop"):
var signed_prop: String = entry.signed_prop
if config.has_section_key("flags", signed_prop):
_jet.set(signed_prop, bool(config.get_value("flags", signed_prop)))
if config.has_section_key("device", "joy_device_id"):
_jet.set("joy_device_id", int(config.get_value("device", "joy_device_id")))
func _save_config() -> void:
if _jet == null:
return
var config := ConfigFile.new()
for entry in AXIS_CONFIG:
var axis_prop: String = entry.axis_prop
config.set_value("axes", axis_prop, int(_jet.get(axis_prop)))
var invert_prop: String = entry.invert_prop
config.set_value("invert", invert_prop, bool(_jet.get(invert_prop)))
var device_prop: String = entry.device_prop
config.set_value("devices", device_prop, int(_jet.get(device_prop)))
if entry.has("signed_prop"):
var signed_prop: String = entry.signed_prop
config.set_value("flags", signed_prop, bool(_jet.get(signed_prop)))
config.set_value("device", "joy_device_id", int(_jet.get("joy_device_id")))
config.save(config_path)
func _get_device_text() -> String:
if _jet == null:
return "Device: none"
var device_id: int = int(_jet.get("joy_device_id"))
if device_id < 0:
var pads = Input.get_connected_joypads()
if pads.is_empty():
return "Device: none"
device_id = pads[0]
var name = Input.get_joy_name(device_id)
return "Device: %s (id %d)" % [name, device_id]
func _get_device_label(device_id: int) -> String:
if device_id < 0:
return "Dev: auto"
var name = Input.get_joy_name(device_id)
if name.is_empty():
return "Dev: %d" % device_id
return "Dev: %s (%d)" % [name, device_id]
func _get_device_prop(axis_prop: String) -> String:
for entry in AXIS_CONFIG:
if entry.axis_prop == axis_prop:
return entry.device_prop
return ""
func _is_toggle_event(event: InputEvent) -> bool:
if InputMap.has_action(toggle_action):
return event.is_action_pressed(toggle_action)
if event is InputEventKey and event.pressed and not event.echo:
return event.keycode == KEY_F1
return false
func _get_toggle_hint() -> String:
if InputMap.has_action(toggle_action):
return toggle_action
return "F1"