Files
MovementTests/addons/guide/remapping/guide_remapper.gd
2025-05-27 19:20:46 +02:00

308 lines
11 KiB
GDScript

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)