Files
Blockline/scripts/control_bindings.gd

118 lines
5.7 KiB
GDScript

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