generated from SGDA/GodotExampleProject
feat: made sure the aspect ration fit a pixel art game and added useful addons
This commit is contained in:
376
addons/guide/remapping/guide_input_detector.gd
Normal file
376
addons/guide/remapping/guide_input_detector.gd
Normal file
@ -0,0 +1,376 @@
|
||||
@tool
|
||||
## Helper node for detecting inputs. Detects the next input matching a specification and
|
||||
## emits a signal with the detected input.
|
||||
class_name GUIDEInputDetector
|
||||
extends Node
|
||||
|
||||
## The device type for which the input should be filtered.
|
||||
enum DeviceType {
|
||||
## Only detect input from keyboard.
|
||||
KEYBOARD = 1,
|
||||
## Only detect input from the mouse.
|
||||
MOUSE = 2,
|
||||
## Only detect input from joysticks/gamepads.
|
||||
JOY = 4
|
||||
# touch doesn't make a lot of sense as this is usually
|
||||
# not remappable.
|
||||
}
|
||||
|
||||
## Which joy index should be used for detected joy events
|
||||
enum JoyIndex {
|
||||
# Use -1, so the detected input will match any joystick
|
||||
ANY = 0,
|
||||
# Use the actual index of the detected joystick.
|
||||
DETECTED = 1
|
||||
}
|
||||
|
||||
enum DetectionState {
|
||||
# The detector is currently idle.
|
||||
IDLE = 0,
|
||||
# The detector is currently counting down before starting the detection.
|
||||
COUNTDOWN = 3,
|
||||
# The detector is currently detecting input.
|
||||
DETECTING = 1,
|
||||
# The detector has finished detecting but is waiting for input to be released.
|
||||
WAITING_FOR_INPUT_CLEAR = 2,
|
||||
}
|
||||
|
||||
## A countdown between initiating a dection and the actual start of the
|
||||
## detection. This is useful because when the user clicks a button to
|
||||
## start a detection, we want to make sure that the player is actually
|
||||
## ready (and not accidentally moves anything). If set to 0, no countdown
|
||||
## will be started.
|
||||
@export_range(0, 2, 0.1, "or_greater") var detection_countdown_seconds:float = 0.5
|
||||
|
||||
## Minimum amplitude to detect any axis.
|
||||
@export_range(0, 1, 0.1, "or_greater") var minimum_axis_amplitude:float = 0.2
|
||||
|
||||
## If any of these inputs is encountered, the detector will
|
||||
## treat this as "abort detection".
|
||||
@export var abort_detection_on:Array[GUIDEInput] = []
|
||||
|
||||
## Which joy index should be returned for detected joy events.
|
||||
@export var use_joy_index:JoyIndex = JoyIndex.ANY
|
||||
|
||||
## Whether trigger buttons on controllers should be detected when
|
||||
## then action value type is limited to boolean.
|
||||
@export var allow_triggers_for_boolean_actions:bool = true
|
||||
|
||||
## Emitted when the detection has started (e.g. countdown has elapsed).
|
||||
## Can be used to signal this to the player.
|
||||
signal detection_started()
|
||||
|
||||
## Emitted when the input detector detects an input of the given type.
|
||||
## If detection was aborted the given input is null.
|
||||
signal input_detected(input:GUIDEInput)
|
||||
|
||||
# The timer for the detection countdown.
|
||||
var _timer:Timer
|
||||
|
||||
# Our copy of the input state
|
||||
var _input_state:GUIDEInputState
|
||||
# The current state of the detection.
|
||||
var _status:DetectionState = DetectionState.IDLE
|
||||
# Mapping contexts that were active when the detection started. We need to restore these once the detection is
|
||||
# finished or aborted.
|
||||
var _saved_mapping_contexts:Array[GUIDEMappingContext] = []
|
||||
|
||||
# The last detected input.
|
||||
var _last_detected_input:GUIDEInput = null
|
||||
|
||||
func _ready():
|
||||
# don't run the process function if we are not detecting to not waste resources
|
||||
set_process(false)
|
||||
_timer = Timer.new()
|
||||
_input_state = GUIDEInputState.new()
|
||||
_timer.one_shot = true
|
||||
add_child(_timer, false, Node.INTERNAL_MODE_FRONT)
|
||||
_timer.timeout.connect(_begin_detection)
|
||||
|
||||
|
||||
## Whether the input detector is currently detecting input.
|
||||
var is_detecting:bool:
|
||||
get: return _status != DetectionState.IDLE
|
||||
|
||||
var _value_type:GUIDEAction.GUIDEActionValueType
|
||||
var _device_types:Array[DeviceType] = []
|
||||
|
||||
## Detects a boolean input type.
|
||||
func detect_bool(device_types:Array[DeviceType] = []) -> void:
|
||||
detect(GUIDEAction.GUIDEActionValueType.BOOL, device_types)
|
||||
|
||||
|
||||
## Detects a 1D axis input type.
|
||||
func detect_axis_1d(device_types:Array[DeviceType] = []) -> void:
|
||||
detect(GUIDEAction.GUIDEActionValueType.AXIS_1D, device_types)
|
||||
|
||||
|
||||
## Detects a 2D axis input type.
|
||||
func detect_axis_2d(device_types:Array[DeviceType] = []) -> void:
|
||||
detect(GUIDEAction.GUIDEActionValueType.AXIS_2D, device_types)
|
||||
|
||||
|
||||
## Detects a 3D axis input type.
|
||||
func detect_axis_3d(device_types:Array[DeviceType] = []) -> void:
|
||||
detect(GUIDEAction.GUIDEActionValueType.AXIS_3D, device_types)
|
||||
|
||||
|
||||
## Detects the given input type. If device types are given
|
||||
## will only detect inputs from the given device types.
|
||||
## Otherwise will detect inputs from all supported device types.
|
||||
func detect(value_type:GUIDEAction.GUIDEActionValueType,
|
||||
device_types:Array[DeviceType] = []) -> void:
|
||||
if device_types == null:
|
||||
push_error("Device types must not be null. Supply an empty array if you want to detect input from all devices.")
|
||||
return
|
||||
|
||||
|
||||
# If we are already detecting, abort this.
|
||||
if _status == DetectionState.DETECTING or _status == DetectionState.WAITING_FOR_INPUT_CLEAR:
|
||||
for input in abort_detection_on:
|
||||
input._end_usage()
|
||||
|
||||
# and start a new detection.
|
||||
_status = DetectionState.COUNTDOWN
|
||||
|
||||
_value_type = value_type
|
||||
_device_types = device_types
|
||||
_timer.stop()
|
||||
_timer.start(detection_countdown_seconds)
|
||||
|
||||
## This is called by the timer when the countdown has elapsed.
|
||||
func _begin_detection():
|
||||
# set status to detecting
|
||||
_status = DetectionState.DETECTING
|
||||
# reset and clear the input state
|
||||
_input_state._clear()
|
||||
_input_state._reset()
|
||||
|
||||
# enable all abort detection inputs
|
||||
for input in abort_detection_on:
|
||||
input._state = _input_state
|
||||
input._begin_usage()
|
||||
|
||||
# we also use this inside the editor where the GUIDE
|
||||
# singleton is not active. Here we don't need to enable
|
||||
# and disable the mapping contexts.
|
||||
if not Engine.is_editor_hint():
|
||||
# save currently active mapping contexts
|
||||
_saved_mapping_contexts = GUIDE.get_enabled_mapping_contexts()
|
||||
|
||||
# disable all mapping contexts
|
||||
for context in _saved_mapping_contexts:
|
||||
GUIDE.disable_mapping_context(context)
|
||||
|
||||
detection_started.emit()
|
||||
|
||||
|
||||
## Aborts a running detection. If no detection currently runs
|
||||
## does nothing.
|
||||
func abort_detection() -> void:
|
||||
_timer.stop()
|
||||
# if we are currently detecting, deliver the null result
|
||||
# which will gracefully shut down everything
|
||||
if _status == DetectionState.DETECTING:
|
||||
_deliver(null)
|
||||
|
||||
# in any other state we don't need to do anything
|
||||
|
||||
## This is called while we are waiting for input to be released.
|
||||
func _process(delta: float) -> void:
|
||||
# if we are not detecting, we don't need to do anything
|
||||
if _status != DetectionState.WAITING_FOR_INPUT_CLEAR:
|
||||
set_process(false)
|
||||
return
|
||||
|
||||
# check if the input is still actuated. We do this to avoid the problem
|
||||
# of this input accidentally triggering something in the mapping contexts
|
||||
# when we enable them again.
|
||||
for input in abort_detection_on:
|
||||
if input._value.is_finite() and input._value.length() > 0:
|
||||
# we still have input, so we are still waiting
|
||||
# retry next frame
|
||||
return
|
||||
|
||||
# if we are here, the input is no longer actuated
|
||||
|
||||
# tear down the inputs
|
||||
for input in abort_detection_on:
|
||||
input._end_usage()
|
||||
|
||||
# restore the mapping contexts
|
||||
# but only when not running in the editor
|
||||
if not Engine.is_editor_hint():
|
||||
for context in _saved_mapping_contexts:
|
||||
GUIDE.enable_mapping_context(context)
|
||||
|
||||
# set status to idle
|
||||
_status = DetectionState.IDLE
|
||||
# and deliver the detected input
|
||||
input_detected.emit(_last_detected_input)
|
||||
|
||||
## This is called in any state when input is received.
|
||||
func _input(event:InputEvent) -> void:
|
||||
if _status == DetectionState.IDLE:
|
||||
return
|
||||
|
||||
# feed the event into the state
|
||||
_input_state._input(event)
|
||||
|
||||
# while detecting, we're the only ones consuming input and we eat this input
|
||||
# to not accidentally trigger built-in Godot mappings (e.g. UI stuff)
|
||||
get_viewport().set_input_as_handled()
|
||||
# but we still feed it into GUIDE's global state so this state stays
|
||||
# up to date. This should have no effect because we disabled all mapping
|
||||
# contexts.
|
||||
if not Engine.is_editor_hint():
|
||||
GUIDE.inject_input(event)
|
||||
|
||||
if _status == DetectionState.DETECTING:
|
||||
# check if any abort input will trigger
|
||||
for input in abort_detection_on:
|
||||
# if it triggers, we abort
|
||||
if input._value.is_finite() and input._value.length() > 0:
|
||||
abort_detection()
|
||||
return
|
||||
|
||||
# check if the event matches the device type we are
|
||||
# looking for
|
||||
if not _matches_device_types(event):
|
||||
return
|
||||
|
||||
# then check if it can be mapped to the desired
|
||||
# value type
|
||||
match _value_type:
|
||||
GUIDEAction.GUIDEActionValueType.BOOL:
|
||||
_try_detect_bool(event)
|
||||
GUIDEAction.GUIDEActionValueType.AXIS_1D:
|
||||
_try_detect_axis_1d(event)
|
||||
GUIDEAction.GUIDEActionValueType.AXIS_2D:
|
||||
_try_detect_axis_2d(event)
|
||||
GUIDEAction.GUIDEActionValueType.AXIS_3D:
|
||||
_try_detect_axis_3d(event)
|
||||
|
||||
|
||||
func _matches_device_types(event:InputEvent) -> bool:
|
||||
if _device_types.is_empty():
|
||||
return true
|
||||
|
||||
if event is InputEventKey:
|
||||
return _device_types.has(DeviceType.KEYBOARD)
|
||||
|
||||
if event is InputEventMouse:
|
||||
return _device_types.has(DeviceType.MOUSE)
|
||||
|
||||
if event is InputEventJoypadButton or event is InputEventJoypadMotion:
|
||||
return _device_types.has(DeviceType.JOY)
|
||||
|
||||
return false
|
||||
|
||||
|
||||
func _try_detect_bool(event:InputEvent) -> void:
|
||||
if event is InputEventKey and event.is_released():
|
||||
var result := GUIDEInputKey.new()
|
||||
result.key = event.physical_keycode
|
||||
result.shift = event.shift_pressed
|
||||
result.control = event.ctrl_pressed
|
||||
result.meta = event.meta_pressed
|
||||
result.alt = event.alt_pressed
|
||||
_deliver(result)
|
||||
return
|
||||
|
||||
if event is InputEventMouseButton and event.is_released():
|
||||
var result := GUIDEInputMouseButton.new()
|
||||
result.button = event.button_index
|
||||
_deliver(result)
|
||||
return
|
||||
|
||||
if event is InputEventJoypadButton and event.is_released():
|
||||
var result := GUIDEInputJoyButton.new()
|
||||
result.button = event.button_index
|
||||
result.joy_index = _find_joy_index(event.device)
|
||||
_deliver(result)
|
||||
|
||||
if allow_triggers_for_boolean_actions:
|
||||
# only allow joypad trigger buttons
|
||||
if not (event is InputEventJoypadMotion):
|
||||
return
|
||||
if event.axis != JOY_AXIS_TRIGGER_LEFT and \
|
||||
event.axis != JOY_AXIS_TRIGGER_RIGHT:
|
||||
return
|
||||
|
||||
var result := GUIDEInputJoyAxis1D.new()
|
||||
result.axis = event.axis
|
||||
result.joy_index = _find_joy_index(event.device)
|
||||
_deliver(result)
|
||||
|
||||
|
||||
|
||||
func _try_detect_axis_1d(event:InputEvent) -> void:
|
||||
if event is InputEventMouseMotion:
|
||||
var result := GUIDEInputMouseAxis1D.new()
|
||||
# Pick the direction in which the mouse was moved more.
|
||||
if abs(event.relative.x) > abs(event.relative.y):
|
||||
result.axis = GUIDEInputMouseAxis1D.GUIDEInputMouseAxis.X
|
||||
else:
|
||||
result.axis = GUIDEInputMouseAxis1D.GUIDEInputMouseAxis.Y
|
||||
_deliver(result)
|
||||
return
|
||||
|
||||
if event is InputEventJoypadMotion:
|
||||
if abs(event.axis_value) < minimum_axis_amplitude:
|
||||
return
|
||||
|
||||
var result := GUIDEInputJoyAxis1D.new()
|
||||
result.axis = event.axis
|
||||
result.joy_index = _find_joy_index(event.device)
|
||||
_deliver(result)
|
||||
|
||||
|
||||
func _try_detect_axis_2d(event:InputEvent) -> void:
|
||||
if event is InputEventMouseMotion:
|
||||
var result := GUIDEInputMouseAxis2D.new()
|
||||
_deliver(result)
|
||||
return
|
||||
|
||||
if event is InputEventJoypadMotion:
|
||||
if event.axis_value < minimum_axis_amplitude:
|
||||
return
|
||||
|
||||
var result := GUIDEInputJoyAxis2D.new()
|
||||
match event.axis:
|
||||
JOY_AXIS_LEFT_X, JOY_AXIS_LEFT_Y:
|
||||
result.x = JOY_AXIS_LEFT_X
|
||||
result.y = JOY_AXIS_LEFT_Y
|
||||
JOY_AXIS_RIGHT_X, JOY_AXIS_RIGHT_Y:
|
||||
result.x = JOY_AXIS_RIGHT_X
|
||||
result.y = JOY_AXIS_RIGHT_Y
|
||||
_:
|
||||
# not supported for detection
|
||||
return
|
||||
result.joy_index = _find_joy_index(event.device)
|
||||
_deliver(result)
|
||||
return
|
||||
|
||||
|
||||
func _try_detect_axis_3d(event:InputEvent) -> void:
|
||||
# currently no input for 3D
|
||||
pass
|
||||
|
||||
|
||||
func _find_joy_index(device_id:int) -> int:
|
||||
if use_joy_index == JoyIndex.ANY:
|
||||
return -1
|
||||
|
||||
var pads := Input.get_connected_joypads()
|
||||
for i in pads.size():
|
||||
if pads[i] == device_id:
|
||||
return i
|
||||
|
||||
return -1
|
||||
|
||||
func _deliver(input:GUIDEInput) -> void:
|
||||
_last_detected_input = input
|
||||
_status = DetectionState.WAITING_FOR_INPUT_CLEAR
|
||||
# enable processing so we can check if the input is released before we re-enable GUIDE's mapping contexts
|
||||
set_process(true)
|
1
addons/guide/remapping/guide_input_detector.gd.uid
Normal file
1
addons/guide/remapping/guide_input_detector.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://db27ccgomq455
|
307
addons/guide/remapping/guide_remapper.gd
Normal file
307
addons/guide/remapping/guide_remapper.gd
Normal file
@ -0,0 +1,307 @@
|
||||
class_name GUIDERemapper
|
||||
|
||||
## Emitted when the bound input of an item changes.
|
||||
signal item_changed(item:ConfigItem, input:GUIDEInput)
|
||||
|
||||
var _remapping_config:GUIDERemappingConfig = GUIDERemappingConfig.new()
|
||||
var _mapping_contexts:Array[GUIDEMappingContext] = []
|
||||
|
||||
const GUIDESet = preload("../guide_set.gd")
|
||||
|
||||
## Loads the default bindings as they are currently configured in the mapping contexts and a mapping
|
||||
## config for editing. Note that the given mapping config will not be modified, so editing can be
|
||||
## cancelled. Call get_mapping_config to get the modified mapping config.
|
||||
func initialize(mapping_contexts:Array[GUIDEMappingContext], remapping_config:GUIDERemappingConfig):
|
||||
_remapping_config = remapping_config.duplicate() if remapping_config != null else GUIDERemappingConfig.new()
|
||||
|
||||
_mapping_contexts.clear()
|
||||
|
||||
for mapping_context in mapping_contexts:
|
||||
if not is_instance_valid(mapping_context):
|
||||
push_error("Cannot add null mapping context. Ignoring.")
|
||||
return
|
||||
_mapping_contexts.append(mapping_context)
|
||||
|
||||
|
||||
## Returns the mapping config with all modifications applied.
|
||||
func get_mapping_config() -> GUIDERemappingConfig:
|
||||
return _remapping_config.duplicate()
|
||||
|
||||
|
||||
func set_custom_data(key:Variant, value:Variant):
|
||||
_remapping_config.custom_data[key] = value
|
||||
|
||||
|
||||
func get_custom_data(key:Variant, default:Variant = null) -> Variant:
|
||||
return _remapping_config.custom_data.get(key, default)
|
||||
|
||||
|
||||
func remove_custom_data(key:Variant) -> void:
|
||||
_remapping_config.custom_data.erase(key)
|
||||
|
||||
|
||||
## Returns all remappable items. Can be filtered by context, display category or
|
||||
## action.
|
||||
func get_remappable_items(context:GUIDEMappingContext = null,
|
||||
display_category:String = "",
|
||||
action:GUIDEAction = null) -> Array[ConfigItem]:
|
||||
|
||||
if action != null and not action.is_remappable:
|
||||
push_warning("Action filter was set but filtered action is not remappable.")
|
||||
return []
|
||||
|
||||
|
||||
var result:Array[ConfigItem] = []
|
||||
for a_context:GUIDEMappingContext in _mapping_contexts:
|
||||
if context != null and context != a_context:
|
||||
continue
|
||||
for action_mapping:GUIDEActionMapping in a_context.mappings:
|
||||
var mapped_action:GUIDEAction = action_mapping.action
|
||||
# filter non-remappable actions
|
||||
if not mapped_action.is_remappable:
|
||||
continue
|
||||
|
||||
# if action filter is set, only pick mappings for this action
|
||||
if action != null and action != mapped_action:
|
||||
continue
|
||||
|
||||
# make config items
|
||||
for index:int in action_mapping.input_mappings.size():
|
||||
var input_mapping:GUIDEInputMapping = action_mapping.input_mappings[index]
|
||||
if input_mapping.override_action_settings and not input_mapping.is_remappable:
|
||||
# skip non-remappable items
|
||||
continue
|
||||
|
||||
# Calculate effective display category
|
||||
var effective_display_category:String = \
|
||||
_get_effective_display_category(mapped_action, input_mapping)
|
||||
|
||||
# if display category filter is set, only pick mappings
|
||||
# in this category
|
||||
if display_category.length() > 0 and effective_display_category != display_category:
|
||||
continue
|
||||
|
||||
var item = ConfigItem.new(a_context, action_mapping.action, index, input_mapping)
|
||||
item_changed.connect(item._item_changed)
|
||||
result.append(item)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
static func _get_effective_display_category(action:GUIDEAction, input_mapping:GUIDEInputMapping) -> String:
|
||||
var result:String = ""
|
||||
if input_mapping.override_action_settings:
|
||||
result = input_mapping.display_category
|
||||
|
||||
if result.is_empty():
|
||||
result = action.display_category
|
||||
|
||||
return result
|
||||
|
||||
|
||||
static func _get_effective_display_name(action:GUIDEAction, input_mapping:GUIDEInputMapping) -> String:
|
||||
var result:String = ""
|
||||
if input_mapping.override_action_settings:
|
||||
result = input_mapping.display_name
|
||||
|
||||
if result.is_empty():
|
||||
result = action.display_name
|
||||
|
||||
return result
|
||||
|
||||
static func _is_effectively_remappable(action:GUIDEAction, input_mapping:GUIDEInputMapping) -> bool:
|
||||
return action.is_remappable and ((not input_mapping.override_action_settings) or input_mapping.is_remappable)
|
||||
|
||||
|
||||
static func _get_effective_value_type(action:GUIDEAction, input_mapping:GUIDEInputMapping) -> GUIDEAction.GUIDEActionValueType:
|
||||
if input_mapping.override_action_settings and input_mapping.input != null:
|
||||
return input_mapping.input._native_value_type()
|
||||
|
||||
return action.action_value_type
|
||||
|
||||
|
||||
## Returns a list of all collisions in all contexts when this new input would be applied to the config item.
|
||||
func get_input_collisions(item:ConfigItem, input:GUIDEInput) -> Array[ConfigItem]:
|
||||
if not _check_item(item):
|
||||
return []
|
||||
var result:Array[ConfigItem] = []
|
||||
|
||||
if input == null:
|
||||
# no item collides with absent input
|
||||
return result
|
||||
|
||||
# walk over all known contexts and find any mappings.
|
||||
for context:GUIDEMappingContext in _mapping_contexts:
|
||||
for action_mapping:GUIDEActionMapping in context.mappings:
|
||||
for index:int in action_mapping.input_mappings.size():
|
||||
var action := action_mapping.action
|
||||
if context == item.context and action == item.action and index == item.index:
|
||||
# collisions with self are allowed
|
||||
continue
|
||||
|
||||
var input_mapping:GUIDEInputMapping = action_mapping.input_mappings[index]
|
||||
var bound_input:GUIDEInput = input_mapping.input
|
||||
# check if this is currently overridden
|
||||
if _remapping_config._has(context, action, index):
|
||||
bound_input = _remapping_config._get_bound_input_or_null(context, action, index)
|
||||
|
||||
# We have a collision
|
||||
if bound_input != null and bound_input.is_same_as(input):
|
||||
var collision_item := ConfigItem.new(context, action, index, input_mapping)
|
||||
item_changed.connect(collision_item._item_changed)
|
||||
result.append(collision_item)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
## Gets the input currently bound to the action in the given context. Can be null if the input
|
||||
## is currently not bound.
|
||||
func get_bound_input_or_null(item:ConfigItem) -> GUIDEInput:
|
||||
if not _check_item(item):
|
||||
return null
|
||||
|
||||
# If the remapping config has a binding for this, this binding wins.
|
||||
if _remapping_config._has(item.context, item.action, item.index):
|
||||
return _remapping_config._get_bound_input_or_null(item.context, item.action, item.index)
|
||||
|
||||
# otherwise return the default binding for this action in the context
|
||||
for action_mapping:GUIDEActionMapping in item.context.mappings:
|
||||
if action_mapping.action == item.action:
|
||||
if action_mapping.input_mappings.size() > item.index:
|
||||
return action_mapping.input_mappings[item.index].input
|
||||
else:
|
||||
push_error("Action mapping does not have an index of ", item.index , ".")
|
||||
|
||||
return null
|
||||
|
||||
## Sets the bound input to the new value for the given config item. Ignores collisions
|
||||
## because collision resolution is highly game specific. Use get_input_collisions to find
|
||||
## potential collisions and then resolve them in a way that suits the game. Note that
|
||||
## bound input can be set to null, which deliberately unbinds the input. If you want
|
||||
## to restore the defaults, call restore_default instead.
|
||||
func set_bound_input(item:ConfigItem, input:GUIDEInput) -> void:
|
||||
if not _check_item(item):
|
||||
return
|
||||
|
||||
# first remove any custom binding we have
|
||||
_remapping_config._clear(item.context, item.action, item.index)
|
||||
|
||||
# Now check if the input is the same as the default
|
||||
var bound_input:GUIDEInput = get_bound_input_or_null(item)
|
||||
|
||||
if bound_input == null and input == null:
|
||||
item_changed.emit(item, input)
|
||||
return # nothing to do
|
||||
|
||||
if bound_input == null:
|
||||
_remapping_config._bind(item.context, item.action, input, item.index)
|
||||
item_changed.emit(item, input)
|
||||
return
|
||||
|
||||
if bound_input != null and input != null and bound_input.is_same_as(input):
|
||||
item_changed.emit(item, input)
|
||||
return # nothing to do
|
||||
|
||||
_remapping_config._bind(item.context, item.action, input, item.index)
|
||||
item_changed.emit(item, input)
|
||||
|
||||
|
||||
## Returns the default binding for the given config item.
|
||||
func get_default_input(item:ConfigItem) -> GUIDEInput:
|
||||
if not _check_item(item):
|
||||
return null
|
||||
|
||||
for mapping:GUIDEActionMapping in item.context.mappings:
|
||||
if mapping.action == item.action:
|
||||
# _check_item verifies the index exists, so no need to check here.
|
||||
return mapping.input_mappings[item.index].input
|
||||
|
||||
return null
|
||||
|
||||
|
||||
## Restores the default binding for the given config item. Note that this may
|
||||
## introduce a conflict if other bindings have bound conflicting input. You can
|
||||
## call get_default_input for the given item to get the default input and then
|
||||
## call get_input_collisions for that to find out whether you would get a collision.
|
||||
func restore_default_for(item:ConfigItem) -> void:
|
||||
if not _check_item(item):
|
||||
return
|
||||
|
||||
_remapping_config._clear(item.context, item.action, item.index)
|
||||
item_changed.emit(item, get_bound_input_or_null(item))
|
||||
|
||||
|
||||
|
||||
## Verifies that the given item is valid.
|
||||
func _check_item(item:ConfigItem) -> bool:
|
||||
if not _mapping_contexts.has(item.context):
|
||||
push_error("Given context is not known to this mapper. Did you call initialize()?")
|
||||
return false
|
||||
|
||||
var action_found := false
|
||||
var size_ok := false
|
||||
for mapping in item.context.mappings:
|
||||
if mapping.action == item.action:
|
||||
action_found = true
|
||||
if mapping.input_mappings.size() > item.index and item.index >= 0:
|
||||
size_ok = true
|
||||
break
|
||||
|
||||
if not action_found:
|
||||
push_error("Given action does not belong to the given context.")
|
||||
return false
|
||||
|
||||
if not size_ok:
|
||||
push_error("Given index does not exist for the given action's input binding.")
|
||||
|
||||
|
||||
if not item.action.is_remappable:
|
||||
push_error("Given action is not remappable.")
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
|
||||
class ConfigItem:
|
||||
## Emitted when the input to this item has changed.
|
||||
signal changed(input:GUIDEInput)
|
||||
|
||||
var _input_mapping:GUIDEInputMapping
|
||||
|
||||
## The display category for this config item
|
||||
var display_category:String:
|
||||
get: return GUIDERemapper._get_effective_display_category(action, _input_mapping)
|
||||
|
||||
## The display name for this config item.
|
||||
var display_name:String:
|
||||
get: return GUIDERemapper._get_effective_display_name(action, _input_mapping)
|
||||
|
||||
## Whether this item is remappable.
|
||||
var is_remappable:bool:
|
||||
get: return GUIDERemapper._is_effectively_remappable(action, _input_mapping)
|
||||
|
||||
## The value type for this config item.
|
||||
var value_type:GUIDEAction.GUIDEActionValueType:
|
||||
get: return GUIDERemapper._get_effective_value_type(action, _input_mapping)
|
||||
|
||||
var context:GUIDEMappingContext
|
||||
var action:GUIDEAction
|
||||
var index:int
|
||||
|
||||
func _init(context:GUIDEMappingContext, action:GUIDEAction, index:int, input_mapping:GUIDEInputMapping):
|
||||
self.context = context
|
||||
self.action = action
|
||||
self.index = index
|
||||
_input_mapping = input_mapping
|
||||
|
||||
## Checks whether this config item is the same as some other
|
||||
## e.g. refers to the same input mapping.
|
||||
func is_same_as(other:ConfigItem) -> bool:
|
||||
return context == other.context and \
|
||||
action == other.action and \
|
||||
index == other.index
|
||||
|
||||
func _item_changed(item:ConfigItem, input:GUIDEInput):
|
||||
if item.is_same_as(self):
|
||||
changed.emit(input)
|
||||
|
1
addons/guide/remapping/guide_remapper.gd.uid
Normal file
1
addons/guide/remapping/guide_remapper.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://nh78h8tprhwn
|
85
addons/guide/remapping/guide_remapping_config.gd
Normal file
85
addons/guide/remapping/guide_remapping_config.gd
Normal file
@ -0,0 +1,85 @@
|
||||
@icon("res://addons/guide/guide_internal.svg")
|
||||
## A remapping configuration. This only holds changes to the context mapping,
|
||||
## so to get the full input map you need to apply this on top of one or more
|
||||
## mapping contexts. The settings from this config take precedence over the
|
||||
## settings from the mapping contexts.
|
||||
class_name GUIDERemappingConfig
|
||||
extends Resource
|
||||
|
||||
## Dictionary with remapped inputs. Structure is:
|
||||
## {
|
||||
## mapping_context : {
|
||||
## action : {
|
||||
## index : bound input
|
||||
## ...
|
||||
## }, ...
|
||||
## }
|
||||
## The bound input can be NULL which means that this was deliberately unbound.
|
||||
@export var remapped_inputs:Dictionary = {}
|
||||
|
||||
## Dictionary for additional custom data to store (e.g. modifier settings, etc.)
|
||||
## Note that this data is completely under application control and it's the responsibility
|
||||
## of the application to ensure that this data is serializable and gets applied at
|
||||
## the necessary point in time.
|
||||
@export var custom_data:Dictionary = {}
|
||||
|
||||
## Binds the given input to the given action. Index can be given to have
|
||||
## alternative bindings for the same action.
|
||||
func _bind(mapping_context:GUIDEMappingContext, action:GUIDEAction, input:GUIDEInput, index:int = 0) -> void:
|
||||
if not remapped_inputs.has(mapping_context):
|
||||
remapped_inputs[mapping_context] = {}
|
||||
|
||||
if not remapped_inputs[mapping_context].has(action):
|
||||
remapped_inputs[mapping_context][action] = {}
|
||||
|
||||
remapped_inputs[mapping_context][action][index] = input
|
||||
|
||||
|
||||
## Unbinds the given input from the given action. This is a deliberate unbind
|
||||
## which means that the action should not be triggerable by the input anymore. It
|
||||
## its not the same as _clear.
|
||||
func _unbind(mapping_context:GUIDEMappingContext, action:GUIDEAction, index:int = 0) -> void:
|
||||
_bind(mapping_context, action, null, index)
|
||||
|
||||
|
||||
## Removes the given input action binding from this configuration. The action will
|
||||
## now have the default input that it has in the mapping_context. This is not the
|
||||
## same as _unbind.
|
||||
func _clear(mapping_context:GUIDEMappingContext, action:GUIDEAction, index:int = 0) -> void:
|
||||
if not remapped_inputs.has(mapping_context):
|
||||
return
|
||||
|
||||
if not remapped_inputs[mapping_context].has(action):
|
||||
return
|
||||
|
||||
remapped_inputs[mapping_context][action].erase(index)
|
||||
|
||||
if remapped_inputs[mapping_context][action].is_empty():
|
||||
remapped_inputs[mapping_context].erase(action)
|
||||
|
||||
if remapped_inputs[mapping_context].is_empty():
|
||||
remapped_inputs.erase(mapping_context)
|
||||
|
||||
|
||||
## Returns the bound input for the given action name and index. Returns null
|
||||
## if there is matching binding.
|
||||
func _get_bound_input_or_null(mapping_context:GUIDEMappingContext, action:GUIDEAction, index:int = 0) -> GUIDEInput:
|
||||
if not remapped_inputs.has(mapping_context):
|
||||
return null
|
||||
|
||||
if not remapped_inputs[mapping_context].has(action):
|
||||
return null
|
||||
|
||||
return remapped_inputs[mapping_context][action].get(index, null)
|
||||
|
||||
|
||||
## Returns whether or not this mapping has a configuration for the given combination (even if the
|
||||
## combination is set to null).
|
||||
func _has(mapping_context:GUIDEMappingContext, action:GUIDEAction, index:int = 0) -> bool:
|
||||
if not remapped_inputs.has(mapping_context):
|
||||
return false
|
||||
|
||||
if not remapped_inputs[mapping_context].has(action):
|
||||
return false
|
||||
|
||||
return remapped_inputs[mapping_context][action].has(index)
|
1
addons/guide/remapping/guide_remapping_config.gd.uid
Normal file
1
addons/guide/remapping/guide_remapping_config.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bjnghv2v2qu6w
|
Reference in New Issue
Block a user