308 lines
11 KiB
GDScript
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)
|
|
|