class_name ControlBindings extends RefCounted const PATH := "user://controls.cfg" const ACTIONS := ["forward", "back", "left", "right", "jump", "sprint", "crouch", "fire", "aim", "reload", "switch", "grenade", "flash", "melee", "interact", "shield", "ultimate", "scoreboard", "pause", "fullscreen"] const LABELS := ["Vorwärts", "Rückwärts", "Links", "Rechts", "Springen / Hochziehen", "Sprinten", "Ducken / Rutschen", "Schießen", "Zielen / ADS", "Nachladen", "Waffe wechseln", "Frag-Granate", "Flashbang", "Nahkampf", "Interagieren / Seilaufzug", "Energieschild", "Claymore", "Scoreboard", "Menü / Pause", "Vollbild"] const KEYS := [KEY_W, KEY_S, KEY_A, KEY_D, KEY_SPACE, KEY_SHIFT, KEY_CTRL, 0, 0, KEY_R, KEY_Q, KEY_G, KEY_F, KEY_V, KEY_E, KEY_B, KEY_H, KEY_TAB, KEY_ESCAPE, KEY_F11] const HELD := ["forward", "back", "left", "right", "sprint", "crouch", "fire", "aim", "scoreboard"] static var bindings: Dictionary = {} static var hints: Dictionary = {} static func defaults() -> void: bindings.clear() for index in ACTIONS.size(): var action: String = ACTIONS[index] bindings[action] = [{"kind": "key", "code": int(KEYS[index])}, {}] if action == "fire": bindings[action][0] = {"kind": "mouse", "code": MOUSE_BUTTON_LEFT} if action == "aim": bindings[action][0] = {"kind": "mouse", "code": MOUSE_BUTTON_RIGHT} apply() static func setup(path: String = PATH) -> void: defaults() var config := ConfigFile.new() if config.load(path) != OK: return for action: String in ACTIONS: if not config.has_section_key("bindings", action): continue var slots: Variant = config.get_value("bindings", action, null) if not slots is Array or slots.size() != 2: continue var valid := true for item: Variant in slots: if not item is Dictionary or (not item.is_empty() and clean(item).is_empty()): valid = false if valid and allowed(action, clean(slots[0])) and allowed(action, clean(slots[1])): bindings[action] = [clean(slots[0]), clean(slots[1])] # A corrupted configuration must never remove the way out of a match. if bindings.pause[0].is_empty() and bindings.pause[1].is_empty(): bindings.pause[0] = {"kind": "key", "code": KEY_ESCAPE} apply() static func clean(value: Dictionary) -> Dictionary: var code: Variant = value.get("code", null) if not code is int or code <= 0: return {} var kind := str(value.get("kind", "")) if kind == "mouse" and code <= MOUSE_BUTTON_XBUTTON2: return {"kind": kind, "code": code} if kind == "key" and code < (1 << 25): return {"kind": kind, "code": code} return {} static func encode(event: InputEvent) -> Dictionary: if event is InputEventKey: return {"kind": "key", "code": int(event.physical_keycode if event.physical_keycode != 0 else event.keycode)} if event is InputEventMouseButton: return clean({"kind": "mouse", "code": int(event.button_index)}) return {} static func decode(value: Dictionary) -> InputEvent: if value.is_empty(): return null if value.kind == "key": var event := InputEventKey.new() event.physical_keycode = int(value.code) return event var event := InputEventMouseButton.new() event.button_index = int(value.code) return event static func apply() -> void: hints.clear() for action: String in ACTIONS: if not InputMap.has_action(action): InputMap.add_action(action) InputMap.action_erase_events(action) for slot: Dictionary in bindings[action]: var event := decode(slot) if event != null: InputMap.action_add_event(action, event) hints[action] = "—" for slot: Dictionary in bindings[action]: if not slot.is_empty(): hints[action] = label(slot) break static func conflicts(action: String, slot: int, event: Dictionary) -> Array[Dictionary]: var result: Array[Dictionary] = [] for other: String in ACTIONS: for index in 2: if other == action and index == slot: continue if not event.is_empty() and bindings[other][index] == event: result.append({"action": other, "slot": index}) return result static func assign(action: String, slot: int, event: Dictionary, swap: bool = false) -> bool: if not action in ACTIONS or slot < 0 or slot > 1: return false var value := clean(event) if not event.is_empty() and value.is_empty(): return false if not allowed(action, value): return false if action == "pause" and value.is_empty() and bindings[action][1 - slot].is_empty(): return false var collisions := conflicts(action, slot, value) if not collisions.is_empty() and not swap: return false var old: Dictionary = bindings[action][slot].duplicate() for collision in collisions: if not allowed(str(collision.action), old): return false if collision.action == "pause" and old.is_empty() and bindings.pause[1 - int(collision.slot)].is_empty(): return false for collision in collisions: bindings[collision.action][collision.slot] = old.duplicate() bindings[action][slot] = value apply() return true static func allowed(action: String, value: Dictionary) -> bool: return value.is_empty() or not (value.kind == "mouse" and int(value.code) in [MOUSE_BUTTON_WHEEL_UP, MOUSE_BUTTON_WHEEL_DOWN, MOUSE_BUTTON_WHEEL_LEFT, MOUSE_BUTTON_WHEEL_RIGHT] and action in HELD) static func save(path: String = PATH) -> Error: var config := ConfigFile.new() for action: String in ACTIONS: config.set_value("bindings", action, bindings[action]) return config.save(path) static func label(value: Dictionary) -> String: if value.is_empty(): return "—" if value.kind == "mouse": return ["", "Maus 1", "Maus 2", "Maus 3", "Rad ↑", "Rad ↓", "Rad ←", "Rad →", "Maus 4", "Maus 5"][int(value.code)] var key := int(value.code) if DisplayServer.get_name() == "headless" else DisplayServer.keyboard_get_keycode_from_physical(int(value.code)) return OS.get_keycode_string(key if key != 0 else int(value.code)) static func hint(action: String) -> String: return str(hints.get(action, action))