Files
room_glitch/core/games/arcade_controls.gd
T

175 lines
6.9 KiB
GDScript

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