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)