extends Node signal changed ## Machine-local bindings; joypads are resolved by identity, never "All Devices". const FILE := "user://arcade_controls.cfg" const STEPS := [ ["arcade_start", "Press PLAYER ONE START"], ["arcade_menu", "Press PLAYER ONE SELECT / RETURN TO MENU"], ["arcade_up", "Move PLAYER ONE joystick UP"], ["arcade_down", "Move PLAYER ONE joystick DOWN"], ["arcade_left", "Move PLAYER ONE joystick LEFT"], ["arcade_right", "Move PLAYER ONE joystick RIGHT"], ["arcade_action_1", "Press FIRE / LEFT MISSILE BATTERY"], ["arcade_action_2", "Press SUPERZAPPER / CENTER MISSILE BATTERY"], ["arcade_action_3", "Press RIGHT MISSILE BATTERY"], ["robotron_fire_up", "Choose ROBOTRON FIRE UP — button or second joystick"], ["robotron_fire_down", "Choose ROBOTRON FIRE DOWN — button or second joystick"], ["robotron_fire_left", "Choose ROBOTRON FIRE LEFT — button or second joystick"], ["robotron_fire_right", "Choose ROBOTRON FIRE RIGHT — button or second joystick"], ] var bindings: Dictionary = {} var problem := "" var path := FILE func _ready() -> void: process_mode = Node.PROCESS_MODE_ALWAYS load_profile() Input.joy_connection_changed.connect(_devices_changed) func _input(event: InputEvent) -> void: handle_quit(event) func handle_quit(event: InputEvent) -> bool: if event is InputEventKey and event.pressed and not event.echo and (event.physical_keycode == KEY_ESCAPE or event.keycode == KEY_ESCAPE): get_viewport().set_input_as_handled() get_tree().quit() return true return false func devices() -> Array: var found: Array = [] for id in Input.get_connected_joypads(): found.append({"id": id, "guid": Input.get_joy_guid(id), "name": Input.get_joy_name(id)}) return found func matches(identity: Dictionary, connected: Array) -> Array: return connected.filter(func(d): return d.guid == identity.guid and d.name == identity.name) func encode(event: InputEvent, connected: Array) -> Dictionary: var result := {} if event is InputEventKey: if event.physical_keycode in [KEY_ESCAPE, KEY_F1]: return {} result = {"kind": "key", "code": event.physical_keycode, "logical": event.keycode if event.physical_keycode == 0 else 0} elif event is InputEventJoypadButton: result = {"kind": "button", "code": event.button_index} elif event is InputEventJoypadMotion: if absf(event.axis_value) < .65: return {} result = {"kind": "axis", "code": event.axis, "sign": -1 if event.axis_value < 0 else 1} elif event is InputEventMouseButton: if event.button_index > MOUSE_BUTTON_MIDDLE: return {} return {"kind": "mouse", "code": event.button_index} else: return {} if event is InputEventJoypadButton or event is InputEventJoypadMotion: var current := connected.filter(func(d): return d.id == event.device) if current.is_empty(): return {} var identity: Dictionary = current[0].duplicate() var same := matches(identity, connected) identity["slot"] = same.find(current[0]) identity["count"] = same.size() identity.erase("id") result["controller"] = identity return result func resolve(binding: Dictionary, connected: Array) -> int: var identity: Dictionary = binding.controller var same := matches(identity, connected) # An absent twin must not silently hand its controls to the remaining twin. if same.size() != int(identity.count): return -1 return int(same[int(identity.slot)].id) if int(identity.slot) < same.size() else -1 func decode(binding: Dictionary, connected: Array) -> InputEvent: var event: InputEvent match binding.kind: "key": event = InputEventKey.new() event.physical_keycode = int(binding.code) event.keycode = int(binding.get("logical", 0)) "mouse": event = InputEventMouseButton.new() event.button_index = int(binding.code) "button": event = InputEventJoypadButton.new() event.button_index = int(binding.code) "axis": event = InputEventJoypadMotion.new() event.axis = int(binding.code) event.axis_value = float(binding.sign) _: return null if binding.has("controller"): var id := resolve(binding, connected) if id < 0: return null event.device = id return event func valid(data: Dictionary) -> bool: for step in STEPS: if not data.get(step[0]) is Dictionary: return false var b: Dictionary = data[step[0]] if b.get("kind", "") not in ["key", "mouse", "button", "axis"]: return false if not b.get("code") is int or b.code < 0: return false if b.kind == "axis" and b.get("sign", 0) not in [-1, 1]: return false if b.kind in ["axis", "button"]: if not b.get("controller") is Dictionary: return false var c: Dictionary = b.controller if not c.get("guid") is String or not c.get("name") is String: return false if not c.get("slot") is int or not c.get("count") is int: return false if c.slot < 0 or c.count <= c.slot: return false return true func load_profile() -> bool: var config := ConfigFile.new() if config.load(path) != OK: return false var data = config.get_value("controls", "bindings", {}) if config.get_value("controls", "version", 0) != 1 or not data is Dictionary or not valid(data): problem = "Saved controls could not be read. Please run setup again." return false bindings = data apply_profile() return true func save_profile(data: Dictionary) -> Error: if not valid(data): return ERR_INVALID_DATA var config := ConfigFile.new() config.set_value("controls", "version", 1) config.set_value("controls", "bindings", data) var error := config.save(path + ".tmp") if error != OK: return error error = DirAccess.rename_absolute(ProjectSettings.globalize_path(path + ".tmp"), ProjectSettings.globalize_path(path)) if error != OK: return error bindings = data.duplicate(true) apply_profile() return OK func apply_profile() -> void: if bindings.is_empty(): return problem = "" var connected := devices() for action in bindings: var event := decode(bindings[action], connected) replace_action(action, event) if action in ["arcade_up", "arcade_down", "arcade_left", "arcade_right"]: replace_action(action.replace("arcade_", "robotron_move_"), event) if event == null: problem = "A saved controller is missing or the controller order changed. Reconnect it or run Control Setup." elif bindings[action].has("controller") and int(bindings[action].controller.count) > 1 and problem.is_empty(): problem = "Identical controllers detected: check PLAYER ONE after reconnecting USB devices. Run setup if they swap." changed.emit() func replace_action(action: String, event: InputEvent) -> void: Input.action_release(action) InputMap.action_erase_events(action) InputMap.action_set_deadzone(action, .35) if event != null: InputMap.action_add_event(action, event) func _devices_changed(_device: int, _connected: bool) -> void: apply_profile.call_deferred() func describe(binding: Dictionary) -> String: var event := decode(binding, devices()) var text := event.as_text() if event != null else "%s %s (not connected)" % [binding.kind, binding.code] if binding.has("controller"): text += " — %s #%d" % [binding.controller.name, int(binding.controller.slot) + 1] return text