487 lines
16 KiB
GDScript
487 lines
16 KiB
GDScript
# This class provides a runner for scense to simulate interactions like keyboard or mouse
|
|
class_name GdUnitSceneRunnerImpl
|
|
extends GdUnitSceneRunner
|
|
|
|
|
|
var GdUnitFuncAssertImpl := ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE)
|
|
|
|
|
|
# mapping of mouse buttons and his masks
|
|
const MAP_MOUSE_BUTTON_MASKS := {
|
|
MOUSE_BUTTON_LEFT : MOUSE_BUTTON_MASK_LEFT,
|
|
MOUSE_BUTTON_RIGHT : MOUSE_BUTTON_MASK_RIGHT,
|
|
MOUSE_BUTTON_MIDDLE : MOUSE_BUTTON_MASK_MIDDLE,
|
|
# https://github.com/godotengine/godot/issues/73632
|
|
MOUSE_BUTTON_WHEEL_UP : 1 << (MOUSE_BUTTON_WHEEL_UP - 1),
|
|
MOUSE_BUTTON_WHEEL_DOWN : 1 << (MOUSE_BUTTON_WHEEL_DOWN - 1),
|
|
MOUSE_BUTTON_XBUTTON1 : MOUSE_BUTTON_MASK_MB_XBUTTON1,
|
|
MOUSE_BUTTON_XBUTTON2 : MOUSE_BUTTON_MASK_MB_XBUTTON2,
|
|
}
|
|
|
|
var _is_disposed := false
|
|
var _current_scene :Node = null
|
|
var _awaiter :GdUnitAwaiter = GdUnitAwaiter.new()
|
|
var _verbose :bool
|
|
var _simulate_start_time :LocalTime
|
|
var _last_input_event :InputEvent = null
|
|
var _mouse_button_on_press := []
|
|
var _key_on_press := []
|
|
var _action_on_press := []
|
|
var _curent_mouse_position :Vector2
|
|
|
|
# time factor settings
|
|
var _time_factor := 1.0
|
|
var _saved_iterations_per_second :float
|
|
var _scene_auto_free := false
|
|
|
|
|
|
func _init(p_scene :Variant, p_verbose :bool, p_hide_push_errors := false) -> void:
|
|
_verbose = p_verbose
|
|
_saved_iterations_per_second = Engine.get_physics_ticks_per_second()
|
|
set_time_factor(1)
|
|
# handle scene loading by resource path
|
|
if typeof(p_scene) == TYPE_STRING:
|
|
if !ResourceLoader.exists(p_scene):
|
|
if not p_hide_push_errors:
|
|
push_error("GdUnitSceneRunner: Can't load scene by given resource path: '%s'. The resource does not exists." % p_scene)
|
|
return
|
|
if !str(p_scene).ends_with(".tscn") and !str(p_scene).ends_with(".scn") and !str(p_scene).begins_with("uid://"):
|
|
if not p_hide_push_errors:
|
|
push_error("GdUnitSceneRunner: The given resource: '%s'. is not a scene." % p_scene)
|
|
return
|
|
_current_scene = load(p_scene).instantiate()
|
|
_scene_auto_free = true
|
|
else:
|
|
# verify we have a node instance
|
|
if not p_scene is Node:
|
|
if not p_hide_push_errors:
|
|
push_error("GdUnitSceneRunner: The given instance '%s' is not a Node." % p_scene)
|
|
return
|
|
_current_scene = p_scene
|
|
if _current_scene == null:
|
|
if not p_hide_push_errors:
|
|
push_error("GdUnitSceneRunner: Scene must be not null!")
|
|
return
|
|
_scene_tree().root.add_child(_current_scene)
|
|
# do finally reset all open input events when the scene is removed
|
|
_scene_tree().root.child_exiting_tree.connect(func f(child :Node) -> void:
|
|
if child == _current_scene:
|
|
_reset_input_to_default()
|
|
)
|
|
_simulate_start_time = LocalTime.now()
|
|
# we need to set inital a valid window otherwise the warp_mouse() is not handled
|
|
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
|
|
# set inital mouse pos to 0,0
|
|
var max_iteration_to_wait := 0
|
|
while get_global_mouse_position() != Vector2.ZERO and max_iteration_to_wait < 100:
|
|
Input.warp_mouse(Vector2.ZERO)
|
|
max_iteration_to_wait += 1
|
|
|
|
|
|
func _notification(what :int) -> void:
|
|
if what == NOTIFICATION_PREDELETE and is_instance_valid(self):
|
|
# reset time factor to normal
|
|
__deactivate_time_factor()
|
|
if is_instance_valid(_current_scene):
|
|
_scene_tree().root.remove_child(_current_scene)
|
|
# do only free scenes instanciated by this runner
|
|
if _scene_auto_free:
|
|
_current_scene.free()
|
|
_is_disposed = true
|
|
_current_scene = null
|
|
# we hide the scene/main window after runner is finished
|
|
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_MINIMIZED)
|
|
|
|
|
|
func _scene_tree() -> SceneTree:
|
|
return Engine.get_main_loop() as SceneTree
|
|
|
|
|
|
func simulate_action_pressed(action :String) -> GdUnitSceneRunner:
|
|
simulate_action_press(action)
|
|
simulate_action_release(action)
|
|
return self
|
|
|
|
|
|
func simulate_action_press(action :String) -> GdUnitSceneRunner:
|
|
__print_current_focus()
|
|
var event := InputEventAction.new()
|
|
event.pressed = true
|
|
event.action = action
|
|
_action_on_press.append(action)
|
|
return _handle_input_event(event)
|
|
|
|
|
|
func simulate_action_release(action :String) -> GdUnitSceneRunner:
|
|
__print_current_focus()
|
|
var event := InputEventAction.new()
|
|
event.pressed = false
|
|
event.action = action
|
|
_action_on_press.erase(action)
|
|
return _handle_input_event(event)
|
|
|
|
|
|
func simulate_key_pressed(key_code :int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner:
|
|
simulate_key_press(key_code, shift_pressed, ctrl_pressed)
|
|
simulate_key_release(key_code, shift_pressed, ctrl_pressed)
|
|
return self
|
|
|
|
|
|
func simulate_key_press(key_code :int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner:
|
|
__print_current_focus()
|
|
var event := InputEventKey.new()
|
|
event.pressed = true
|
|
event.keycode = key_code as Key
|
|
event.physical_keycode = key_code as Key
|
|
event.alt_pressed = key_code == KEY_ALT
|
|
event.shift_pressed = shift_pressed or key_code == KEY_SHIFT
|
|
event.ctrl_pressed = ctrl_pressed or key_code == KEY_CTRL
|
|
_apply_input_modifiers(event)
|
|
_key_on_press.append(key_code)
|
|
return _handle_input_event(event)
|
|
|
|
|
|
func simulate_key_release(key_code :int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner:
|
|
__print_current_focus()
|
|
var event := InputEventKey.new()
|
|
event.pressed = false
|
|
event.keycode = key_code as Key
|
|
event.physical_keycode = key_code as Key
|
|
event.alt_pressed = key_code == KEY_ALT
|
|
event.shift_pressed = shift_pressed or key_code == KEY_SHIFT
|
|
event.ctrl_pressed = ctrl_pressed or key_code == KEY_CTRL
|
|
_apply_input_modifiers(event)
|
|
_key_on_press.erase(key_code)
|
|
return _handle_input_event(event)
|
|
|
|
|
|
func set_mouse_pos(pos :Vector2) -> GdUnitSceneRunner:
|
|
var event := InputEventMouseMotion.new()
|
|
event.position = pos
|
|
event.global_position = get_global_mouse_position()
|
|
_apply_input_modifiers(event)
|
|
return _handle_input_event(event)
|
|
|
|
|
|
func get_mouse_position() -> Vector2:
|
|
if _last_input_event is InputEventMouse:
|
|
return _last_input_event.position
|
|
var current_scene := scene()
|
|
if current_scene != null:
|
|
return current_scene.get_viewport().get_mouse_position()
|
|
return Vector2.ZERO
|
|
|
|
|
|
func get_global_mouse_position() -> Vector2:
|
|
return Engine.get_main_loop().root.get_mouse_position()
|
|
|
|
|
|
func simulate_mouse_move(pos :Vector2) -> GdUnitSceneRunner:
|
|
var event := InputEventMouseMotion.new()
|
|
event.position = pos
|
|
event.relative = pos - get_mouse_position()
|
|
event.global_position = get_global_mouse_position()
|
|
_apply_input_mouse_mask(event)
|
|
_apply_input_modifiers(event)
|
|
return _handle_input_event(event)
|
|
|
|
|
|
func simulate_mouse_move_relative(relative: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner:
|
|
var tween := _scene_tree().create_tween()
|
|
_curent_mouse_position = get_mouse_position()
|
|
var final_position := _curent_mouse_position + relative
|
|
tween.tween_property(self, "_curent_mouse_position", final_position, time).set_trans(trans_type)
|
|
tween.play()
|
|
|
|
while not get_mouse_position().is_equal_approx(final_position):
|
|
simulate_mouse_move(_curent_mouse_position)
|
|
await _scene_tree().process_frame
|
|
return self
|
|
|
|
|
|
func simulate_mouse_move_absolute(position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner:
|
|
var tween := _scene_tree().create_tween()
|
|
_curent_mouse_position = get_mouse_position()
|
|
tween.tween_property(self, "_curent_mouse_position", position, time).set_trans(trans_type)
|
|
tween.play()
|
|
|
|
while not get_mouse_position().is_equal_approx(position):
|
|
simulate_mouse_move(_curent_mouse_position)
|
|
await _scene_tree().process_frame
|
|
return self
|
|
|
|
|
|
func simulate_mouse_button_pressed(buttonIndex :MouseButton, double_click := false) -> GdUnitSceneRunner:
|
|
simulate_mouse_button_press(buttonIndex, double_click)
|
|
simulate_mouse_button_release(buttonIndex)
|
|
return self
|
|
|
|
|
|
func simulate_mouse_button_press(buttonIndex :MouseButton, double_click := false) -> GdUnitSceneRunner:
|
|
var event := InputEventMouseButton.new()
|
|
event.button_index = buttonIndex
|
|
event.pressed = true
|
|
event.double_click = double_click
|
|
_apply_input_mouse_position(event)
|
|
_apply_input_mouse_mask(event)
|
|
_apply_input_modifiers(event)
|
|
_mouse_button_on_press.append(buttonIndex)
|
|
return _handle_input_event(event)
|
|
|
|
|
|
func simulate_mouse_button_release(buttonIndex :MouseButton) -> GdUnitSceneRunner:
|
|
var event := InputEventMouseButton.new()
|
|
event.button_index = buttonIndex
|
|
event.pressed = false
|
|
_apply_input_mouse_position(event)
|
|
_apply_input_mouse_mask(event)
|
|
_apply_input_modifiers(event)
|
|
_mouse_button_on_press.erase(buttonIndex)
|
|
return _handle_input_event(event)
|
|
|
|
|
|
func set_time_factor(time_factor := 1.0) -> GdUnitSceneRunner:
|
|
_time_factor = min(9.0, time_factor)
|
|
__activate_time_factor()
|
|
__print("set time factor: %f" % _time_factor)
|
|
__print("set physics physics_ticks_per_second: %d" % (_saved_iterations_per_second*_time_factor))
|
|
return self
|
|
|
|
|
|
func simulate_frames(frames: int, delta_milli :int = -1) -> GdUnitSceneRunner:
|
|
var time_shift_frames :int = max(1, frames / _time_factor)
|
|
for frame in time_shift_frames:
|
|
if delta_milli == -1:
|
|
await _scene_tree().process_frame
|
|
else:
|
|
await _scene_tree().create_timer(delta_milli * 0.001).timeout
|
|
return self
|
|
|
|
|
|
func simulate_until_signal(
|
|
signal_name :String,
|
|
arg0 :Variant = NO_ARG,
|
|
arg1 :Variant = NO_ARG,
|
|
arg2 :Variant = NO_ARG,
|
|
arg3 :Variant = NO_ARG,
|
|
arg4 :Variant = NO_ARG,
|
|
arg5 :Variant = NO_ARG,
|
|
arg6 :Variant = NO_ARG,
|
|
arg7 :Variant = NO_ARG,
|
|
arg8 :Variant = NO_ARG,
|
|
arg9 :Variant = NO_ARG) -> GdUnitSceneRunner:
|
|
var args :Array = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], NO_ARG)
|
|
await _awaiter.await_signal_idle_frames(scene(), signal_name, args, 10000)
|
|
return self
|
|
|
|
|
|
func simulate_until_object_signal(
|
|
source :Object,
|
|
signal_name :String,
|
|
arg0 :Variant = NO_ARG,
|
|
arg1 :Variant = NO_ARG,
|
|
arg2 :Variant = NO_ARG,
|
|
arg3 :Variant = NO_ARG,
|
|
arg4 :Variant = NO_ARG,
|
|
arg5 :Variant = NO_ARG,
|
|
arg6 :Variant = NO_ARG,
|
|
arg7 :Variant = NO_ARG,
|
|
arg8 :Variant = NO_ARG,
|
|
arg9 :Variant = NO_ARG) -> GdUnitSceneRunner:
|
|
var args :Array = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], NO_ARG)
|
|
await _awaiter.await_signal_idle_frames(source, signal_name, args, 10000)
|
|
return self
|
|
|
|
|
|
func await_func(func_name :String, args := []) -> GdUnitFuncAssert:
|
|
return GdUnitFuncAssertImpl.new(scene(), func_name, args)
|
|
|
|
|
|
func await_func_on(instance :Object, func_name :String, args := []) -> GdUnitFuncAssert:
|
|
return GdUnitFuncAssertImpl.new(instance, func_name, args)
|
|
|
|
|
|
func await_signal(signal_name :String, args := [], timeout := 2000 ) -> void:
|
|
await _awaiter.await_signal_on(scene(), signal_name, args, timeout)
|
|
|
|
|
|
func await_signal_on(source :Object, signal_name :String, args := [], timeout := 2000 ) -> void:
|
|
await _awaiter.await_signal_on(source, signal_name, args, timeout)
|
|
|
|
|
|
# maximizes the window to bring the scene visible
|
|
func maximize_view() -> GdUnitSceneRunner:
|
|
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
|
|
DisplayServer.window_move_to_foreground()
|
|
return self
|
|
|
|
|
|
func _property_exists(name :String) -> bool:
|
|
return scene().get_property_list().any(func(properties :Dictionary) -> bool: return properties["name"] == name)
|
|
|
|
|
|
func get_property(name :String) -> Variant:
|
|
if not _property_exists(name):
|
|
return "The property '%s' not exist checked loaded scene." % name
|
|
return scene().get(name)
|
|
|
|
|
|
func set_property(name :String, value :Variant) -> bool:
|
|
if not _property_exists(name):
|
|
push_error("The property named '%s' cannot be set, it does not exist!" % name)
|
|
return false;
|
|
scene().set(name, value)
|
|
return true
|
|
|
|
|
|
func invoke(
|
|
name :String,
|
|
arg0 :Variant = NO_ARG,
|
|
arg1 :Variant = NO_ARG,
|
|
arg2 :Variant = NO_ARG,
|
|
arg3 :Variant = NO_ARG,
|
|
arg4 :Variant = NO_ARG,
|
|
arg5 :Variant = NO_ARG,
|
|
arg6 :Variant = NO_ARG,
|
|
arg7 :Variant = NO_ARG,
|
|
arg8 :Variant = NO_ARG,
|
|
arg9 :Variant = NO_ARG) -> Variant:
|
|
var args :Array = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], NO_ARG)
|
|
if scene().has_method(name):
|
|
return scene().callv(name, args)
|
|
return "The method '%s' not exist checked loaded scene." % name
|
|
|
|
|
|
func find_child(name :String, recursive :bool = true, owned :bool = false) -> Node:
|
|
return scene().find_child(name, recursive, owned)
|
|
|
|
|
|
func _scene_name() -> String:
|
|
var scene_script :GDScript = scene().get_script()
|
|
var scene_name :String = scene().get_name()
|
|
if not scene_script:
|
|
return scene_name
|
|
if not scene_name.begins_with("@"):
|
|
return scene_name
|
|
return scene_script.resource_name.get_basename()
|
|
|
|
|
|
func __activate_time_factor() -> void:
|
|
Engine.set_time_scale(_time_factor)
|
|
Engine.set_physics_ticks_per_second((_saved_iterations_per_second * _time_factor) as int)
|
|
|
|
|
|
func __deactivate_time_factor() -> void:
|
|
Engine.set_time_scale(1)
|
|
Engine.set_physics_ticks_per_second(_saved_iterations_per_second as int)
|
|
|
|
|
|
# copy over current active modifiers
|
|
func _apply_input_modifiers(event :InputEvent) -> void:
|
|
if _last_input_event is InputEventWithModifiers and event is InputEventWithModifiers:
|
|
event.meta_pressed = event.meta_pressed or _last_input_event.meta_pressed
|
|
event.alt_pressed = event.alt_pressed or _last_input_event.alt_pressed
|
|
event.shift_pressed = event.shift_pressed or _last_input_event.shift_pressed
|
|
event.ctrl_pressed = event.ctrl_pressed or _last_input_event.ctrl_pressed
|
|
# this line results into reset the control_pressed state!!!
|
|
#event.command_or_control_autoremap = event.command_or_control_autoremap or _last_input_event.command_or_control_autoremap
|
|
|
|
|
|
# copy over current active mouse mask and combine with curren mask
|
|
func _apply_input_mouse_mask(event :InputEvent) -> void:
|
|
# first apply last mask
|
|
if _last_input_event is InputEventMouse and event is InputEventMouse:
|
|
event.button_mask |= _last_input_event.button_mask
|
|
if event is InputEventMouseButton:
|
|
var button_mask :int = MAP_MOUSE_BUTTON_MASKS.get(event.get_button_index(), 0)
|
|
if event.is_pressed():
|
|
event.button_mask |= button_mask
|
|
else:
|
|
event.button_mask ^= button_mask
|
|
|
|
|
|
# copy over last mouse position if need
|
|
func _apply_input_mouse_position(event :InputEvent) -> void:
|
|
if _last_input_event is InputEventMouse and event is InputEventMouseButton:
|
|
event.position = _last_input_event.position
|
|
|
|
|
|
## handle input action via Input modifieres
|
|
func _handle_actions(event :InputEventAction) -> bool:
|
|
if not InputMap.event_is_action(event, event.action, true):
|
|
return false
|
|
__print(" process action %s (%s) <- %s" % [scene(), _scene_name(), event.as_text()])
|
|
if event.is_pressed():
|
|
Input.action_press(event.action, InputMap.action_get_deadzone(event.action))
|
|
else:
|
|
Input.action_release(event.action)
|
|
return true
|
|
|
|
|
|
# for handling read https://docs.godotengine.org/en/stable/tutorials/inputs/inputevent.html?highlight=inputevent#how-does-it-work
|
|
func _handle_input_event(event :InputEvent) -> GdUnitSceneRunner:
|
|
if event is InputEventMouse:
|
|
Input.warp_mouse(event.position)
|
|
Input.parse_input_event(event)
|
|
|
|
if event is InputEventAction:
|
|
_handle_actions(event)
|
|
|
|
Input.flush_buffered_events()
|
|
var current_scene := scene()
|
|
if is_instance_valid(current_scene):
|
|
__print(" process event %s (%s) <- %s" % [current_scene, _scene_name(), event.as_text()])
|
|
if(current_scene.has_method("_gui_input")):
|
|
current_scene._gui_input(event)
|
|
if(current_scene.has_method("_unhandled_input")):
|
|
current_scene._unhandled_input(event)
|
|
current_scene.get_viewport().set_input_as_handled()
|
|
|
|
# save last input event needs to be merged with next InputEventMouseButton
|
|
_last_input_event = event
|
|
return self
|
|
|
|
|
|
func _reset_input_to_default() -> void:
|
|
# reset all mouse button to inital state if need
|
|
for m_button :int in _mouse_button_on_press.duplicate():
|
|
if Input.is_mouse_button_pressed(m_button):
|
|
simulate_mouse_button_release(m_button)
|
|
_mouse_button_on_press.clear()
|
|
|
|
for key_scancode :int in _key_on_press.duplicate():
|
|
if Input.is_key_pressed(key_scancode):
|
|
simulate_key_release(key_scancode)
|
|
_key_on_press.clear()
|
|
|
|
for action :String in _action_on_press.duplicate():
|
|
if Input.is_action_pressed(action):
|
|
simulate_action_release(action)
|
|
_action_on_press.clear()
|
|
|
|
Input.flush_buffered_events()
|
|
_last_input_event = null
|
|
|
|
|
|
func __print(message :String) -> void:
|
|
if _verbose:
|
|
prints(message)
|
|
|
|
|
|
func __print_current_focus() -> void:
|
|
if not _verbose:
|
|
return
|
|
var focused_node := scene().get_viewport().gui_get_focus_owner()
|
|
if focused_node:
|
|
prints(" focus checked %s" % focused_node)
|
|
else:
|
|
prints(" no focus set")
|
|
|
|
|
|
func scene() -> Node:
|
|
if is_instance_valid(_current_scene):
|
|
return _current_scene
|
|
if not _is_disposed:
|
|
push_error("The current scene instance is not valid anymore! check your test is valid. e.g. check for missing awaits.")
|
|
return null
|