extends SceneTree const ControllerMappingManagerType = preload( "res://settings/controller_mapping_manager.gd" ) const ControllerMappingPanelType = preload( "res://ui/controller_mapping_panel.gd" ) const GameUIType = preload("res://ui/game_ui.gd") func _init() -> void: call_deferred("_run") func _run() -> void: var manager := ControllerMappingManagerType.new() root.add_child(manager) await process_frame var failure: String = _validate_manager(manager) if failure.is_empty(): failure = _validate_portmaster_launcher() if failure.is_empty(): failure = _validate_auto_map(manager) if failure.is_empty(): failure = _validate_virtual_mouse_bounds() if failure.is_empty(): failure = _validate_controller_scroll_target() if failure.is_empty(): print("controller mapping validation passed") quit(0) return push_error(failure) quit(1) func _validate_manager(manager: ControllerMappingManagerType) -> String: var muos_mapping: String = ( ControllerMappingManagerType.build_muos_controller_mapping( "19000000010000000100000000010000", "muOS-Keys", ) ) for expected_binding: String in [ "a:b0", "b:b1", "x:b3", "y:b2", "leftshoulder:b4", "rightshoulder:b5", "lefttrigger:b10", "righttrigger:b11", "back:b6", "start:b7", "guide:b8", "leftstick:b9", "rightstick:b12", "leftx:a0", "righty:a3", ]: if expected_binding not in muos_mapping: return "muOS compatibility mapping omitted " + expected_binding if not muos_mapping.begins_with( "19000000010000000100000000010000,muOS-Keys," ): return "muOS compatibility mapping does not use the runtime guid" if not ControllerMappingManagerType.is_muos_controller_name( "muOS-Keys" ): return "muOS controller name was not recognized" if ControllerMappingManagerType.is_muos_controller_name( "ordinary controller" ): return "muOS mapping would affect unrelated controllers" if not ControllerMappingManagerType.should_install_muos_compatibility_mapping( "muOS-Keys", false, ): return "unknown legacy muOS controller did not receive fallback mapping" if ControllerMappingManagerType.should_install_muos_compatibility_mapping( "muOS-Keys", true, ): return "recognized muOS controller would be replaced at runtime" if ControllerMappingManagerType.should_install_muos_compatibility_mapping( "ordinary controller", false, ): return "muOS fallback would affect an unrelated unknown controller" var defaults: Dictionary = manager.get_active_bindings() if defaults.size() != ControllerMappingManagerType.ROLE_ORDER.size(): return "default mapping covers %d of %d controller roles: %s" % [ defaults.size(), ControllerMappingManagerType.ROLE_ORDER.size(), str(defaults.keys()), ] var trigger_button := InputEventJoypadButton.new() trigger_button.device = manager.get_active_device_id() trigger_button.button_index = JOY_BUTTON_MISC1 trigger_button.pressed = true var trigger_binding: Dictionary = manager.binding_from_event( ControllerMappingManagerType.ROLE_POINTER_MODIFIER, trigger_button, ) if str(trigger_binding.get("kind", "")) != "button": return "trigger role did not accept a button-backed handheld trigger" var trigger_motion := InputEventJoypadMotion.new() trigger_motion.device = manager.get_active_device_id() trigger_motion.axis = JOY_AXIS_TRIGGER_RIGHT trigger_motion.axis_value = 1.0 trigger_binding = manager.binding_from_event( ControllerMappingManagerType.ROLE_POINTER_MODIFIER, trigger_motion, ) if str(trigger_binding.get("kind", "")) != "axis": return "trigger role did not accept an axis-backed controller trigger" var stick_button := manager.binding_from_event( ControllerMappingManagerType.ROLE_RIGHT_STICK_X, trigger_button, ) if not stick_button.is_empty(): return "stick axis role accepted a button" var keyboard_events_before: int = _keyboard_event_count(&"jump") var custom: Dictionary = defaults.duplicate(true) custom[str(ControllerMappingManagerType.ROLE_A)] = { "kind": "button", "button": int(JOY_BUTTON_X), } custom[str(ControllerMappingManagerType.ROLE_POINTER_MODIFIER)] = ( trigger_binding ) if not manager.replace_active_bindings(custom): return "valid custom mapping could not be saved" if not manager.has_custom_mapping(): return "saved custom mapping did not become active" if _keyboard_event_count(&"jump") != keyboard_events_before: return "controller remapping changed keyboard bindings" var active_button := InputEventJoypadButton.new() active_button.device = manager.get_active_device_id() active_button.button_index = JOY_BUTTON_X active_button.pressed = true if not manager.event_matches_role( active_button, ControllerMappingManagerType.ROLE_A, ): return "active controller event did not match its custom role" var inactive_button := active_button.duplicate() as InputEventJoypadButton inactive_button.device = manager.get_active_device_id() + 1 if manager.event_matches_role( inactive_button, ControllerMappingManagerType.ROLE_A, ): return "inactive controller event matched the active profile" var inactive_release := InputEventJoypadButton.new() inactive_release.device = manager.get_active_device_id() + 2 inactive_release.button_index = JOY_BUTTON_A inactive_release.pressed = false var active_device_before_release: int = manager.get_active_device_id() manager._input(inactive_release) if manager.get_active_device_id() != active_device_before_release: return "inactive controller button release stole ownership" for event: InputEvent in InputMap.action_get_events(&"jump"): if ( event is InputEventJoypadButton and event.device != manager.get_active_device_id() ): return "custom InputMap event was not scoped to the active device" manager._axis_rest_by_device["4"] = {str(JOY_AXIS_LEFT_X): 0.0} var drift := InputEventJoypadMotion.new() drift.device = 4 drift.axis = JOY_AXIS_LEFT_X drift.axis_value = 0.12 if manager._axis_motion_claims_device(drift): return "minor inactive-stick drift claimed controller ownership" manager._input(drift) if manager.get_active_device_id() != active_device_before_release: return "minor inactive-stick drift changed the active controller" drift.axis_value = 0.72 if not manager._axis_motion_claims_device(drift): return "intentional inactive-stick motion did not claim ownership" manager._input(drift) if manager.get_active_device_id() != drift.device: return "intentional stick motion did not change controller ownership" for event: InputEvent in InputMap.action_get_events(&"jump"): if event is InputEventJoypadButton and event.device != drift.device: return "active-device change did not rescope InputMap events" manager._set_active_controller(active_device_before_release) manager._axis_rest_by_device["5"] = {str(JOY_AXIS_LEFT_X): 0.0} manager._on_joy_connection_changed(5, false) if manager._axis_rest_by_device.has("5"): return "disconnected controller retained stale axis calibration" if not FileAccess.file_exists( ControllerMappingManagerType.PROFILE_PATH ): return "controller mapping was not stored device-locally" return "" func _validate_portmaster_launcher() -> String: const launcher_path: String = "res://scripts/portmaster/NETfishing.sh" if not FileAccess.file_exists(launcher_path): return "PortMaster launcher template is missing" var launcher: String = FileAccess.get_file_as_string(launcher_path) for expected_fragment: String in [ "19004ca6010000000100000000010000", "19000000010000000100000000010000", "a:b0,b:b1,x:b3,y:b2", "leftshoulder:b4,rightshoulder:b5", "lefttrigger:b10,righttrigger:b11", "guide:b8,start:b7,back:b6", "leftstick:b9", "rightstick:b12", "netfishing_controllerconfig=\"$GODOT_MUOS_MAPPING\"", "SDL_GAMECONTROLLERCONFIG=\"$netfishing_controllerconfig\" \\", ]: if expected_fragment not in launcher: return "PortMaster launcher omitted " + expected_fragment if "$'\\n'" in launcher: return "PortMaster launcher appends mappings across a Weston-unsafe newline" return "" func _validate_auto_map(manager: ControllerMappingManagerType) -> String: var panel := ControllerMappingPanelType.new() root.add_child(panel) panel.setup(manager) panel.open_panel() panel._begin_auto_map() for role: StringName in ControllerMappingManagerType.ROLE_ORDER: panel._process(ControllerMappingPanelType.CAPTURE_NEUTRAL_SECONDS) var binding := ( ControllerMappingManagerType.default_bindings()[str(role)] as Dictionary ) if role == ControllerMappingManagerType.ROLE_B: binding = ( ControllerMappingManagerType.default_bindings()[ str(ControllerMappingManagerType.ROLE_A) ] as Dictionary ) if str(binding.get("kind", "")) == "button": var button := InputEventJoypadButton.new() button.device = manager.get_active_device_id() button.button_index = int(binding.get("button", -1)) button.pressed = true manager.controller_input_observed.emit(button) else: var motion := InputEventJoypadMotion.new() motion.device = manager.get_active_device_id() motion.axis = int(binding.get("axis", -1)) motion.axis_value = float(binding.get("direction", -1.0)) manager.controller_input_observed.emit(motion) if panel._auto_map_active: return "auto-map did not complete after every requested input" if not manager.has_custom_mapping(): return "completed auto-map did not activate its profile" if panel._binding_buttons.size() != ( ControllerMappingManagerType.ROLE_ORDER.size() ): return "manual override list does not expose every mapped role" var conflict_button := panel._binding_buttons.get( ControllerMappingManagerType.ROLE_A ) as Button var conflict_style := conflict_button.get_theme_stylebox( &"normal" ) as StyleBoxFlat if ( conflict_style == null or conflict_style.bg_color != UtilityPageStyle.OCEAN_DANGER ): return "duplicate controller bindings were not marked in red" manager.set_binding( ControllerMappingManagerType.ROLE_B, ControllerMappingManagerType.default_bindings()[ str(ControllerMappingManagerType.ROLE_B) ] as Dictionary, ) panel._begin_manual_capture( ControllerMappingManagerType.ROLE_POINTER_MODIFIER ) panel._process(ControllerMappingPanelType.CAPTURE_NEUTRAL_SECONDS) if "left trigger" in panel._progress_label.text.to_lower(): return "manual remapping still dictates a specific physical input" if "any button" not in panel._progress_label.text.to_lower(): return "manual remapping does not request a generic controller input" panel._cancel_capture() panel._begin_auto_map() panel._process(ControllerMappingPanelType.CAPTURE_NEUTRAL_SECONDS) var lone_button := InputEventJoypadButton.new() lone_button.device = manager.get_active_device_id() lone_button.button_index = JOY_BUTTON_B lone_button.pressed = true manager.controller_input_observed.emit(lone_button) if not panel._auto_map_active or panel._auto_map_index != 1: return "a single controller button cancelled auto-map" panel._cancel_capture() panel._begin_auto_map() panel._process(ControllerMappingPanelType.CAPTURE_NEUTRAL_SECONDS) var left_bumper := InputEventJoypadButton.new() left_bumper.device = manager.get_active_device_id() left_bumper.button_index = JOY_BUTTON_LEFT_SHOULDER left_bumper.pressed = true manager.controller_input_observed.emit(left_bumper) var right_bumper := InputEventJoypadButton.new() right_bumper.device = manager.get_active_device_id() right_bumper.button_index = JOY_BUTTON_RIGHT_SHOULDER right_bumper.pressed = true manager.controller_input_observed.emit(right_bumper) panel._process(ControllerMappingPanelType.CANCEL_COMBO_HOLD_SECONDS - 0.01) if not panel._auto_map_active: return "bumper combo cancelled auto-map before the hold threshold" panel._process(0.02) if panel._auto_map_active or panel.is_capturing(): return "sustained bumper combo did not cancel auto-map" panel._begin_auto_map() panel._process(ControllerMappingPanelType.CAPTURE_NEUTRAL_SECONDS) var held_left := InputEventJoypadMotion.new() held_left.device = manager.get_active_device_id() held_left.axis = JOY_AXIS_LEFT_X held_left.axis_value = -1.0 for _index: int in 4: manager.controller_input_observed.emit(held_left) if panel._auto_map_index != 0: return "auto-map accepted repeated analog motion without a neutral gate" var first_button := InputEventJoypadButton.new() first_button.device = manager.get_active_device_id() first_button.button_index = JOY_BUTTON_A first_button.pressed = true manager.controller_input_observed.emit(first_button) if panel._auto_map_index != 1 or not panel._waiting_for_neutral: return "auto-map did not gate the next step after a captured input" manager.controller_input_observed.emit(first_button) if panel._auto_map_index != 1: return "held input advanced more than one auto-map step" panel._cancel_capture() return "" func _validate_virtual_mouse_bounds() -> String: var window_size := Vector2(3840.0, 2160.0) var bounds: Rect2 = GameUIType.get_virtual_mouse_window_bounds(window_size) var expected_margin := Vector2(42.0, 42.0) if not bounds.position.is_equal_approx(expected_margin): return "4K virtual cursor minimum bounds are incorrect: %s" % bounds if not bounds.end.is_equal_approx(window_size - expected_margin): return "4K virtual cursor maximum bounds are incorrect: %s" % bounds return "" func _validate_controller_scroll_target() -> String: var game_ui := GameUIType.new() var page := VBoxContainer.new() var scroll := ScrollContainer.new() var content := VBoxContainer.new() var button := Button.new() var footer_button := Button.new() root.add_child(page) page.add_child(scroll) scroll.add_child(content) content.add_child(button) page.add_child(footer_button) var target: ScrollContainer = game_ui._focused_scroll_container(button) if target != scroll: game_ui.free() page.free() return "focused menu content did not resolve its scroll container" target = game_ui._focused_scroll_container(footer_button) if target != scroll: game_ui.free() page.free() return "menu footer focus did not resolve the page's only scroll container" scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED if game_ui._focused_scroll_container(button) != null: game_ui.free() page.free() return "controller scrolling ignored a disabled scroll container" game_ui.free() page.free() return "" func _keyboard_event_count(action: StringName) -> int: var count: int = 0 for event: InputEvent in InputMap.action_get_events(action): if event is InputEventKey or event is InputEventMouseButton: count += 1 return count