diff --git a/mirror-godot-app/creator/selection/inspector/categories/inspector_category_toggle_button.gd b/mirror-godot-app/creator/selection/inspector/categories/inspector_category_toggle_button.gd index 4764dd90..b2ebc55c 100644 --- a/mirror-godot-app/creator/selection/inspector/categories/inspector_category_toggle_button.gd +++ b/mirror-godot-app/creator/selection/inspector/categories/inspector_category_toggle_button.gd @@ -6,6 +6,10 @@ signal inspector_category_visibility_changed(new_visibility: bool) @export var expand_speed: float = 10.0 @export var properties: Node +## The text to show as a tooltip when hovering. +## Use this instead of Godot's tooltip_text property. +@export var hover_tooltip_text: String = "" + var _is_category_visible: bool = false var _properties_child: Control var _plus_texture: TextureRect @@ -55,3 +59,17 @@ func set_category_visible(new_is_visible): func _on_toggle_button_pressed(): set_category_visible(not _is_category_visible) + + +func _on_hoverable_button_mouse_entered() -> void: + if hover_tooltip_text == "": + return + GameUI.set_hover_tooltip_text(hover_tooltip_text) + + +func _on_hoverable_button_mouse_exited() -> void: + GameUI.hide_hover_tooltip_text() + + +func _on_hoverable_button_pressed() -> void: + GameUI.hide_hover_tooltip_text() diff --git a/mirror-godot-app/creator/selection/inspector/inspector.gd b/mirror-godot-app/creator/selection/inspector/inspector.gd index 39d73274..0e1b2bcc 100644 --- a/mirror-godot-app/creator/selection/inspector/inspector.gd +++ b/mirror-godot-app/creator/selection/inspector/inspector.gd @@ -15,6 +15,7 @@ const _ENVIRONMENT_CATEGORY = preload("res://creator/selection/inspector/categor const _LIGHT_CATEGORY = preload("res://creator/selection/inspector/categories/inspector_light.tscn") const _PHYSICS_CATEGORY = preload("res://creator/selection/inspector/categories/inspector_physics.tscn") const _VISIBILITY_CATEGORY = preload("res://creator/selection/inspector/categories/inspector_visibility.tscn") +const _SCRIPT_OBJECT_VARS_CATEGORY = preload("res://creator/selection/inspector/script/inspector_script_object_vars.tscn") const _SCRIPT_INSTANCE_CATEGORY = preload("res://creator/selection/inspector/script/inspector_script_instance.tscn") const _MODEL_NODES_CATEGORY = preload("res://creator/selection/inspector/nodes/inspector_model_nodes.tscn") const _EXTRA_NODE_CATEGORY = preload("res://creator/selection/inspector/nodes/inspector_extra_node.tscn") @@ -43,6 +44,7 @@ var _deletion_target_category: InspectorCategoryBase @onready var _categories: VBoxContainer = _tab_cont.get_node(^"Properties/MarginContainer/Categories") @onready var _script_main_vbox: VBoxContainer = _tab_cont.get_node(^"Scripting/MarginContainer/VBoxContainer") +@onready var _script_obj_vars: Control = _script_main_vbox.get_node(^"ScriptObjectVars") @onready var _script_instances: VBoxContainer = _script_main_vbox.get_node(^"ScriptInstances") @onready var _script_add_button: Button = _script_main_vbox.get_node(^"AddScriptButton") @onready var _model_nodes_main_vbox: VBoxContainer = _tab_cont.get_node(^"Nodes/MarginContainer/VBoxContainer") @@ -189,6 +191,7 @@ func inspect_nodes(new_nodes: Array[Node], force_rebuild: bool = false) -> void: # The first step is to delete old categories if they exist. _remove_old_category_children(_categories) _remove_old_category_children(_script_instances) + _remove_old_category_children(_script_obj_vars) _remove_old_category_children(_model_nodes) # Update a few misc things. _button_sound.refresh() @@ -278,6 +281,7 @@ func _setup_new_categories(target_nodes: Array[Node]) -> void: prim_model_cat.request_convert_to_local.connect(_on_request_convert_prim_model_to_local.bind(target_node)) target_node.scripts_changed.connect(_on_scripts_changed) _setup_script_instances(target_node) + _setup_script_obj_vars(target_node) _script_main_vbox.show() _tab_cont.tabs_visible = true _setup_extra_model_nodes(target_node) @@ -287,6 +291,7 @@ func _setup_new_categories(target_nodes: Array[Node]) -> void: _tab_cont.current_tab = 1 target_node.scripts_changed.connect(_on_scripts_changed) _setup_script_instances(target_node) + _setup_script_obj_vars(target_node) _script_main_vbox.show() else: _tab_cont.current_tab = 0 @@ -390,6 +395,28 @@ func _setup_script_instances(space_object_or_global_scripts: Node) -> void: _setup_script_instance(script_instance) +func _setup_script_obj_vars(space_object_or_global_scripts: Node) -> void: + # Set up script object variables inspector, if it has any. + var object_variables: Dictionary + if space_object_or_global_scripts.has_meta(&"MirrorScriptObjectVariables"): + object_variables = space_object_or_global_scripts.get_meta(&"MirrorScriptObjectVariables") + if object_variables.is_empty(): + if not _is_any_script_instance_gdscript(space_object_or_global_scripts): + return + var obj_var_cat = _SCRIPT_OBJECT_VARS_CATEGORY.instantiate() + obj_var_cat.setup(space_object_or_global_scripts, Util.can_local_user_edit_scripts()) + _script_obj_vars.add_child(obj_var_cat) + obj_var_cat.setup_object_vars(object_variables) + + +func _is_any_script_instance_gdscript(space_object_or_global_scripts: Node) -> bool: + var script_instances: Array[ScriptInstance] = space_object_or_global_scripts.get_script_instances() + for script_inst in script_instances: + if script_inst is GDScriptInstance: + return true + return false + + func _setup_script_instance(script_instance: ScriptInstance) -> void: var cat: InspectorCategoryBase = _setup_category(script_instance.target_node, _SCRIPT_INSTANCE_CATEGORY, "", _script_instances) cat.setup(script_instance) diff --git a/mirror-godot-app/creator/selection/inspector/inspector.tscn b/mirror-godot-app/creator/selection/inspector/inspector.tscn index 4088a5ca..f111fc80 100644 --- a/mirror-godot-app/creator/selection/inspector/inspector.tscn +++ b/mirror-godot-app/creator/selection/inspector/inspector.tscn @@ -92,6 +92,11 @@ metadata/_edit_layout_mode = 1 [node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/TabContainer/Scripting/MarginContainer"] layout_mode = 2 +[node name="ScriptObjectVars" type="MarginContainer" parent="VBoxContainer/TabContainer/Scripting/MarginContainer/VBoxContainer"] +layout_mode = 2 +theme_override_constants/margin_top = 0 +theme_override_constants/margin_bottom = 8 + [node name="ScriptInstances" type="VBoxContainer" parent="VBoxContainer/TabContainer/Scripting/MarginContainer/VBoxContainer"] layout_mode = 2 size_flags_horizontal = 3 diff --git a/mirror-godot-app/creator/selection/inspector/script/inspector_script_entry_inputs.gd b/mirror-godot-app/creator/selection/inspector/script/inspector_script_entry_inputs.gd index 84eade0f..8c244112 100644 --- a/mirror-godot-app/creator/selection/inspector/script/inspector_script_entry_inputs.gd +++ b/mirror-godot-app/creator/selection/inspector/script/inspector_script_entry_inputs.gd @@ -56,11 +56,8 @@ func _setup_parameter(parameter_name: String, parameter_data: Array) -> void: func _on_parameter_changed(value, which: Control) -> void: - var parameters_for_entry: Dictionary = _target_script_instance.entry_parameters[_entry_id] - var param_name = which.label_text - var param_data = parameters_for_entry[param_name] - param_data[1] = value - _target_script_instance.apply_inspector_parameter_values() + var param_name: String = which.label_text + _target_script_instance.set_inspector_parameter_input_value(_entry_id, param_name, value) _target_script_instance.script_instance_changed() @@ -69,12 +66,7 @@ func _on_toggle_button_inspector_category_visibility_changed(new_visibility: boo func _on_create_parameter(parameter_port_array: Array) -> void: - for entry_block in _target_script_instance.script_builder.entry_blocks: - if entry_block.entry_id == _entry_id: - entry_block.parameters.create_inspector_parameter(parameter_port_array) - entry_block.reset_entry_output_ports() - break - _target_script_instance.sync_script_inst_params_with_script_data() + _target_script_instance.create_inspector_parameter_input(_entry_id, parameter_port_array) _target_script_instance.script_data_contents_changed() refresh_inspected_nodes.emit() diff --git a/mirror-godot-app/creator/selection/inspector/script/inspector_script_entry_inputs.tscn b/mirror-godot-app/creator/selection/inspector/script/inspector_script_entry_inputs.tscn index c4c38d3e..b7743cad 100644 --- a/mirror-godot-app/creator/selection/inspector/script/inspector_script_entry_inputs.tscn +++ b/mirror-godot-app/creator/selection/inspector/script/inspector_script_entry_inputs.tscn @@ -7,6 +7,9 @@ [node name="InspectorScriptEntryInputs" instance=ExtResource("1_amxtn")] script = ExtResource("2_adill") +[node name="ToggleButton" parent="CategoryTitle" index="0" node_paths=PackedStringArray("properties")] +properties = NodePath("../../Properties") + [node name="Text" parent="CategoryTitle/ToggleButton/Name" index="3"] text = "" diff --git a/mirror-godot-app/creator/selection/inspector/script/inspector_script_object_vars.gd b/mirror-godot-app/creator/selection/inspector/script/inspector_script_object_vars.gd new file mode 100644 index 00000000..f69376fe --- /dev/null +++ b/mirror-godot-app/creator/selection/inspector/script/inspector_script_object_vars.gd @@ -0,0 +1,72 @@ +extends InspectorCategoryBase + + +const _TRASH_BUTTON = preload("res://creator/selection/inspector/script/entry_input_trash_button.tscn") + +var target_node: Node # SpaceObject or SpaceGlobalScripts +var _is_editable: bool = false +var _object_vars: Dictionary +var _property_list: Control + + +## Must run before _ready() +func setup(target_object: Node, is_editable: bool) -> void: + target_node = target_object + _property_list = $Properties/MarginContainer/PropertyList + _is_editable = is_editable + + +func setup_object_vars(object_vars: Dictionary) -> void: + _object_vars = object_vars + for obj_var_name in _object_vars: + _setup_object_var(obj_var_name, _object_vars[obj_var_name]) + if _property_list.get_child_count() < 5: + if _property_list.get_child_count() != 0: + $CategoryTitle/ToggleButton.hover_tooltip_text = "" + set_visible_to_maximum_size() + + +func _setup_object_var(obj_var_name: String, obj_var_value: Variant) -> void: + var obj_var_type: int = typeof(obj_var_value) + if obj_var_type == TYPE_NIL or not obj_var_type in ScriptParameterCreationMenu.INSPECTOR_PRIMITIVE_SCENES: + return + var obj_var_scene = ScriptParameterCreationMenu.INSPECTOR_PRIMITIVE_SCENES[obj_var_type].instantiate() + obj_var_scene.label_text = obj_var_name + obj_var_scene.reset_value = Serialization.type_convert_any(_get_default_value_of_obj_var(obj_var_name), obj_var_type) + # Be careful, the order matters here! Value editors with setters + # that use onready vars can only be used after adding as a child, + # and then we need to refresh if a refresh method exists. + _property_list.add_child(obj_var_scene) + obj_var_scene.current_value = obj_var_value + if obj_var_scene.has_method(&"refresh"): + obj_var_scene.refresh() + obj_var_scene.value_changed.connect(_on_object_var_changed.bind(obj_var_scene)) + if _is_editable and GameplaySettings.script_show_add_inspector_input: + var trash_button: Node = _TRASH_BUTTON.instantiate() + obj_var_scene.add_child(trash_button) + trash_button.pressed.connect(_on_delete_object_var.bind(obj_var_name)) + + +func _on_object_var_changed(value, which: Control) -> void: + var obj_var_name: String = which.label_text + Zone.script_network_sync.set_variable_on_node(target_node, obj_var_name, value) + + +func _on_delete_object_var(obj_var_name: String) -> void: + Zone.script_network_sync.set_variable_on_node(target_node, obj_var_name, null) + for child in _property_list.get_children(): + child.cleanup_and_delete() + _property_list.remove_child(child) + var vars = target_node.get_meta(&"MirrorScriptObjectVariables") + vars.erase(obj_var_name) + setup_object_vars(vars) + + +func _get_default_value_of_obj_var(obj_var_name: String) -> Variant: + var script_instances: Array[ScriptInstance] = target_node.get_script_instances() + for script_inst in script_instances: + if script_inst.has_method(&"get_default_value_of_exposed_variable"): + var def = script_inst.get_default_value_of_exposed_variable(obj_var_name) + if def != null: + return def + return null diff --git a/mirror-godot-app/creator/selection/inspector/script/inspector_script_object_vars.tscn b/mirror-godot-app/creator/selection/inspector/script/inspector_script_object_vars.tscn new file mode 100644 index 00000000..fcad62ff --- /dev/null +++ b/mirror-godot-app/creator/selection/inspector/script/inspector_script_object_vars.tscn @@ -0,0 +1,18 @@ +[gd_scene load_steps=3 format=3 uid="uid://bhbojmr3nuwrc"] + +[ext_resource type="PackedScene" uid="uid://dkxqj3l0xm8uw" path="res://creator/selection/inspector/categories/inspector_category_base.tscn" id="1_ov2tq"] +[ext_resource type="Script" path="res://creator/selection/inspector/script/inspector_script_object_vars.gd" id="2_abj1i"] + +[node name="InspectorScriptObjectVars" instance=ExtResource("1_ov2tq")] +script = ExtResource("2_abj1i") + +[node name="ToggleButton" parent="CategoryTitle" index="0" node_paths=PackedStringArray("properties")] +properties = NodePath("../../Properties") +hover_tooltip_text = "Use @export in GDScript" + +[node name="Text" parent="CategoryTitle/ToggleButton/Name" index="3"] +text = "OBJECT VARIABLES" + +[connection signal="mouse_entered" from="CategoryTitle/ToggleButton" to="CategoryTitle/ToggleButton" method="_on_hoverable_button_mouse_entered"] +[connection signal="mouse_exited" from="CategoryTitle/ToggleButton" to="CategoryTitle/ToggleButton" method="_on_hoverable_button_mouse_exited"] +[connection signal="pressed" from="CategoryTitle/ToggleButton" to="CategoryTitle/ToggleButton" method="_on_hoverable_button_pressed"] diff --git a/mirror-godot-app/script/editor/abstract_script_editor.gd b/mirror-godot-app/script/editor/abstract_script_editor.gd index 9c9a0205..21de7ebb 100644 --- a/mirror-godot-app/script/editor/abstract_script_editor.gd +++ b/mirror-godot-app/script/editor/abstract_script_editor.gd @@ -4,6 +4,7 @@ extends Control signal request_save_script_as_asset(script_instance: ScriptInstance) signal request_show_entry_creation_dialog(target_node: Node) +signal request_show_custom_entry_creation_dialog(target_node: Node) signal request_toggle_variable_editor() signal request_script_editor_visibility(is_visible: bool) signal request_track_recently_used_space_script(script_instance: ScriptInstance) diff --git a/mirror-godot-app/script/editor/entry_creation/entry_create_dialog.gd b/mirror-godot-app/script/editor/entry_creation/entry_create_dialog.gd index 354111b8..3c78cc9c 100644 --- a/mirror-godot-app/script/editor/entry_creation/entry_create_dialog.gd +++ b/mirror-godot-app/script/editor/entry_creation/entry_create_dialog.gd @@ -13,22 +13,39 @@ func _ready() -> void: func populate_and_show(target_node: Node) -> void: title = "Create Script Entry" - _entry_creation_menu.populate_selection_tree(target_node) + _entry_creation_menu.populate_selection_tree(target_node, false) GameUI.grab_input_lock(self) popup_centered() _entry_creation_menu.focus_search_bar() +func populate_and_show_for_custom(target_node: Node) -> void: + _entry_creation_menu.populate_selection_tree(target_node, true) + _show_custom_entry_menu() + GameUI.grab_input_lock(self) + _entry_creation_menu.focus_search_bar() + + func _on_confirmed() -> void: var signal_dict = _entry_creation_menu.get_desired_signal_signature() if signal_dict == null: - title = "Create Entry With Custom Signal" - popup_centered() + _show_custom_entry_menu() return var block_dict: Dictionary = signal_dict.duplicate(false) create_entry_block.emit(block_dict) +func _show_custom_entry_menu() -> void: + title = "Create Entry With Custom Signal" + popup_centered() + _entry_creation_menu.size = Vector2.ZERO + size = Vector2i.ZERO + await get_tree().process_frame + popup_centered() + _entry_creation_menu.size = Vector2.ZERO + size = Vector2i.ZERO + + func _on_script_entry_creation_menu_confirmed() -> void: hide() _on_confirmed() diff --git a/mirror-godot-app/script/editor/entry_creation/entry_creation_menu.gd b/mirror-godot-app/script/editor/entry_creation/entry_creation_menu.gd index e1ac2bb5..8c97056c 100644 --- a/mirror-godot-app/script/editor/entry_creation/entry_creation_menu.gd +++ b/mirror-godot-app/script/editor/entry_creation/entry_creation_menu.gd @@ -21,15 +21,18 @@ func setup(signal_tree_populator: ScriptEntrySignalTreePopulator) -> void: _signal_selection.setup(signal_tree_populator) -func populate_selection_tree(target_node: Node) -> void: +func populate_selection_tree(target_node: Node, for_custom: bool) -> void: _target_node = target_node _custom_signal_parameters.clear() _add_input_button.show() _custom_signal_name.text = "" _signal_parameters_label.text = "Signal Inputs:" - _signal_selection.populate_selection_tree(target_node) - _signal_selection.show() - _custom_signal.hide() + if for_custom: + _show_custom_entry_menu() + else: + _signal_selection.populate_selection_tree(target_node) + _signal_selection.show() + _custom_signal.hide() func focus_search_bar() -> void: @@ -43,14 +46,17 @@ func get_desired_signal_signature(): return _get_custom_signal_signature() +func _show_custom_entry_menu() -> void: + _signal_selection.hide() + _custom_signal.show() + + func _get_selected_signal_signature(): var signal_dict = _signal_selection.get_selected_signal() if signal_dict == null: return null if String(signal_dict["signal"]) == "custom_signal": - _signal_selection.hide() - _custom_signal.show() - _add_input_button.show() + _show_custom_entry_menu() return null return signal_dict @@ -68,10 +74,14 @@ func _get_custom_signal_signature(): if not existing_signature.is_empty(): existing_signature = existing_signature.duplicate() existing_signature["path"] = "self" + existing_signature["type"] = "entry" return existing_signature var ret = { + "entry_id": "self_" + signal_name + "_" + str(randi() % 1000000), + "name": "On " + signal_name.capitalize(), "path": "self", "signal": signal_name, + "type": "entry", } if not _custom_signal_parameters.is_empty(): ret["signalParameters"] = _custom_signal_parameters.duplicate(true) diff --git a/mirror-godot-app/script/editor/script_editor.gd b/mirror-godot-app/script/editor/script_editor.gd index 138a42e6..bbf30ebf 100644 --- a/mirror-godot-app/script/editor/script_editor.gd +++ b/mirror-godot-app/script/editor/script_editor.gd @@ -227,6 +227,10 @@ func _on_request_show_entry_creation_dialog(target_node: Node) -> void: _script_entry_creation_dialog.populate_and_show(target_node) +func _on_request_show_custom_entry_creation_dialog(target_node: Node) -> void: + _script_entry_creation_dialog.populate_and_show_for_custom(target_node) + + func _on_script_entry_creation_dialog_create_entry_block(block_json: Dictionary) -> void: if _gd_script_editor.visible: if block_json["type"] == "entry": diff --git a/mirror-godot-app/script/editor/script_editor.tscn b/mirror-godot-app/script/editor/script_editor.tscn index c12ccb7c..50878897 100644 --- a/mirror-godot-app/script/editor/script_editor.tscn +++ b/mirror-godot-app/script/editor/script_editor.tscn @@ -41,11 +41,13 @@ script = ExtResource("7_w8ott") [connection signal="request_save_script_as_asset" from="VisualScriptEditor" to="." method="_on_request_save_script_as_asset"] [connection signal="request_script_editor_visibility" from="VisualScriptEditor" to="." method="set_visual_script_editor_visibility"] +[connection signal="request_show_custom_entry_creation_dialog" from="VisualScriptEditor" to="." method="_on_request_show_custom_entry_creation_dialog"] [connection signal="request_show_entry_creation_dialog" from="VisualScriptEditor" to="." method="_on_request_show_entry_creation_dialog"] [connection signal="request_toggle_variable_editor" from="VisualScriptEditor" to="." method="_on_request_toggle_variable_editor"] [connection signal="request_track_recently_used_space_script" from="VisualScriptEditor" to="." method="_on_request_track_recently_used_space_script"] [connection signal="request_save_script_as_asset" from="GDScriptEditor" to="." method="_on_request_save_script_as_asset"] [connection signal="request_script_editor_visibility" from="GDScriptEditor" to="." method="set_gd_script_editor_visibility"] +[connection signal="request_show_custom_entry_creation_dialog" from="GDScriptEditor" to="." method="_on_request_show_custom_entry_creation_dialog"] [connection signal="request_show_entry_creation_dialog" from="GDScriptEditor" to="." method="_on_request_show_entry_creation_dialog"] [connection signal="request_toggle_variable_editor" from="GDScriptEditor" to="." method="_on_request_toggle_variable_editor"] [connection signal="request_track_recently_used_space_script" from="GDScriptEditor" to="." method="_on_request_track_recently_used_space_script"] diff --git a/mirror-godot-app/script/gd/gdscript_entry.gd b/mirror-godot-app/script/gd/gdscript_entry.gd index d03deebe..cbc40af5 100644 --- a/mirror-godot-app/script/gd/gdscript_entry.gd +++ b/mirror-godot-app/script/gd/gdscript_entry.gd @@ -174,6 +174,21 @@ func _value_to_gdscript_literal(value: Variant, type_enum: int) -> String: return str(value) +func connect_entry_signal(script_instance_object: Object, inst_entry_parameters: Dictionary) -> void: + # Trivial case: No inspector parameter inputs, no bindv needed. + if inst_entry_parameters.is_empty(): + entry_node.connect(entry_signal, Callable(script_instance_object, function_name)) + return + # If the user added inspector parameters via "Add Input", we need to bind them to the signal. + var insp_inputs: Array = [] + if inst_entry_parameters.has(entry_id): + var params_for_entry: Dictionary = inst_entry_parameters[entry_id] + for insp_param_name in params_for_entry: + var insp_param_data: Array = params_for_entry[insp_param_name] + insp_inputs.append(insp_param_data[1]) + entry_node.connect(entry_signal, Callable(script_instance_object, function_name).bindv(insp_inputs)) + + static func create_node_for_entry_signal(entry_signal: String) -> Node: if entry_signal == "timeout": var timer: Timer = Timer.new() diff --git a/mirror-godot-app/script/gd/gdscript_instance.gd b/mirror-godot-app/script/gd/gdscript_instance.gd index 55a3bdde..89f9c9ed 100644 --- a/mirror-godot-app/script/gd/gdscript_instance.gd +++ b/mirror-godot-app/script/gd/gdscript_instance.gd @@ -7,16 +7,16 @@ signal gdscript_compile_success() const _EMPTY_SCRIPT: String = """# Welcome to The Mirror-flavored GDScript! # This is like normal GDScript, but you must not write class\u200B_name or ext\u200Bends. +# You can print to the notification area using `Notify.info(title, message)`. # You may use functions and variables just like you would in normal GDScript. -# Use `target_object` to refer to the object (SpaceObject or global) the script -# is attached to instead of `self`, as `self` refers to the script itself. # The Mirror provides support for multiple scripts per object, you can use # members of SpaceObject the same way you use inherited members in Godot. -# For example, `print(position)` will print a SpaceObject's position. -# You can print to the notification area using `Notify.info(title, message)`. +# For example, `Notify.info("pos", str(position))` will print a SpaceObject's position. +# Use `target_object` to refer to the object (SpaceObject or global) the script +# is attached to instead of `self`, as `self` refers to the script itself. -# Called when a SpaceObject finishes loading. +# Called when a SpaceObject and script finish loading. func _ready() -> void: pass # Replace with function body. @@ -26,9 +26,7 @@ func _process(delta: float) -> void: pass # Replace with function body. """ -const _SCRIPT_PREPROCESS_PREFIX: String = """extends Object - -signal tmusergdscript_runtime_error(error_str: String, frame_index: int, line_num: int, func_name: String) +const _SCRIPT_PREPROCESS_PREFIX: String = """extends TMUserGDScriptBase func get_object_variable(variable_name: String) -> Variant: @@ -40,10 +38,12 @@ func has_object_variable(variable_name: String) -> bool: func set_object_variable(variable_name: String, variable_value: Variant) -> void: + if variable_name in self: + set(variable_name, variable_value) Mirror.set_object_variable(target_object, variable_name, variable_value) -func tween_object_variable(variable_name: String, to_value: Variant, duration: float, trans: Tween.TransitionType, easing: Tween.EaseType) -> void: +func tween_object_variable(variable_name: String, to_value: Variant, duration: float, trans: Tween.TransitionType = Tween.TRANS_LINEAR, easing: Tween.EaseType = Tween.EASE_IN_OUT) -> void: Mirror.tween_object_variable(target_object, variable_name, to_value, duration, trans, easing) @@ -66,7 +66,12 @@ static var _SCRIPT_TEXT_DENYLIST: Array[RegEx] = [ RegEx.create_from_string("\\bDirAccess\\b") ] +static var _EXPOSE_VAR_REGEX: RegEx = RegEx.create_from_string("@export var ([_a-zA-Z][_a-zA-Z0-9]{0,30})\\b[^=\\n]*(= )?([^=\\n]*)") +static var _EXPRESSION = Expression.new() + var _entries: Array[GDScriptEntry] = [] +var _exposed_var_names: PackedStringArray = [] +var _exposed_var_default_values: Dictionary = {} var _source_code: String var gdscript_code: TMUserGDScript = TMUserGDScript.new() var script_instance_object: Object @@ -155,6 +160,12 @@ func get_default_value_of_entry_inspector_parameter(entry_id: String, parameter_ return null +func get_default_value_of_exposed_variable(variable_name: String) -> Variant: + if _exposed_var_default_values.has(variable_name): + return _exposed_var_default_values[variable_name] + return null + + func get_entry_line_numbers() -> PackedInt32Array: var ret := PackedInt32Array() for entry in _entries: @@ -179,7 +190,12 @@ func set_source_code(source_code: String) -> void: func create_entry(entry_json: Dictionary) -> void: - var new_function_name: String = entry_json["name"].to_snake_case() + var new_function_name: String + if entry_json.has("name"): + new_function_name = entry_json["name"].to_snake_case() + else: + new_function_name = "on_" + entry_json["signal"] + entry_json["function"] = new_function_name for entry in _entries: if entry.function_name == new_function_name: return # We already have an entry for this, no need to make a new entry. @@ -210,6 +226,15 @@ func _sync_entry_params_with_gdscript_code() -> void: script_entries_changed.emit() +func create_inspector_parameter_input(entry_id: String, parameter_port_array: Array) -> void: + for gdscript_entry in _entries: + if gdscript_entry.entry_id == entry_id: + gdscript_entry.entry_parameters.create_inspector_parameter(parameter_port_array) + _source_code = gdscript_entry.sync_gdscript_code_with_entry(_source_code) + break + sync_script_inst_params_with_script_data() + + ## Ensure the script instance entry inspector parameters match the ## signature of the script's data. Since a script may be used by ## multiple objects, parameters may get out of sync without this code. @@ -222,10 +247,10 @@ func sync_script_inst_params_with_script_data() -> void: entry_parameters[entry_id] = new_params if entry_id in old_entry_parameters: var old_params: Dictionary = old_entry_parameters[entry_id] - for old_key in old_params: - if old_key in new_params: - var old_param_array: Array = old_params[old_key] - var new_param_array: Array = new_params[old_key] + for param_key in old_params: + if param_key in new_params: + var old_param_array: Array = old_params[param_key] + var new_param_array: Array = new_params[param_key] new_param_array[1] = old_param_array[1] entry_parameters.sort() apply_inspector_parameter_values() @@ -256,6 +281,7 @@ func _preprocess_and_apply_code() -> void: error_message_dict["line"] -= _SCRIPT_PREPROCESS_LINE_COUNT gdscript_compile_error.emit(error_code, error_messages) return + _update_exposed_variables() gdscript_compile_success.emit() if not can_execute(): # Don't even init the script if it can't execute. @@ -263,13 +289,20 @@ func _preprocess_and_apply_code() -> void: return # Instantiate the successfully loaded script. script_instance_object = gdscript_code.new() + _sync_exposed_variables_with_spaceobj_spacevars() + # Connect the signals. + script_instance_object.load_exposed_vars.connect(_on_load_exposed_vars) + script_instance_object.save_exposed_vars.connect(_on_save_exposed_vars) script_instance_object.tmusergdscript_runtime_error.connect(_on_tmusergdscript_runtime_error) for entry in _entries: - entry.entry_node.connect(entry.entry_signal, Callable(script_instance_object, entry.function_name)) + entry.connect_entry_signal(script_instance_object, entry_parameters) # Supplementary entry callbacks. Keep this in sync with GDScript CodeEdit load_entry_connection_decoration. if target_node is SpaceObject: if _source_code.contains("func _ready("): - target_node.setup_done.connect(Callable(script_instance_object, &"_ready")) + if target_node._is_setup: + script_instance_object.call(&"_ready") + else: + target_node.setup_done.connect(Callable(script_instance_object, &"_ready")) if _source_code.contains("func _physics_process("): Zone.physics_process_every_frame.connect(Callable(script_instance_object, &"_physics_process")) if _source_code.contains("func _process("): @@ -295,6 +328,36 @@ func _sync_gdscript_code_with_entries() -> void: _source_code = entry.sync_gdscript_code_with_entry(_source_code) +func _sync_exposed_variables_with_spaceobj_spacevars() -> void: + #_update_exposed_variables() + for var_name in _exposed_var_names: + if Mirror.has_object_variable(target_node, var_name): + script_instance_object.set(var_name, Mirror.get_object_variable(target_node, var_name)) + else: + Mirror.set_object_variable(target_node, var_name, script_instance_object.get(var_name)) + + +func _on_load_exposed_vars() -> void: + for var_name in _exposed_var_names: + script_instance_object.set(var_name, Mirror.get_object_variable(target_node, var_name)) + + +func _on_save_exposed_vars() -> void: + for var_name in _exposed_var_names: + Mirror.set_object_variable(target_node, var_name, script_instance_object.get(var_name)) + + +func _update_exposed_variables() -> void: + _exposed_var_names.clear() + var matches: Array[RegExMatch] = _EXPOSE_VAR_REGEX.search_all(_source_code) + for mat in matches: + var var_name: String = mat.get_string(1) + _exposed_var_names.append(var_name) + if mat.get_group_count() >= 3: + _EXPRESSION.parse(mat.get_string(3)) + _exposed_var_default_values[var_name] = _EXPRESSION.execute() + + ## This method handles runtime error messages similar to VisualScriptInstance's `_on_block_message` method. func _on_tmusergdscript_runtime_error(error_str: String, frame_index: int, line_num: int, func_name: String) -> void: line_num -= _SCRIPT_PREPROCESS_LINE_COUNT diff --git a/mirror-godot-app/script/gd/mirror_singleton.gd b/mirror-godot-app/script/gd/mirror_singleton.gd index ca7a093d..03c4ae19 100644 --- a/mirror-godot-app/script/gd/mirror_singleton.gd +++ b/mirror-godot-app/script/gd/mirror_singleton.gd @@ -3,6 +3,28 @@ class_name Mirror extends Object +# Misc. +static func get_friendly_name(value: Variant) -> String: + if value is SpaceObject: + return value.get_space_object_name() + elif value is Player: + return value.get_player_name() + elif value is Node: + return value.name + elif value == null: + return "" + elif not is_instance_valid(value): + return "" + else: + # For non-Node objects, str() is the best we can do. + return str(value) + + +static func print_in_chat(attached_object: Object, message: String, range_radius: float = INF) -> void: + GameUI.chat_ui.send_message_from_object(attached_object, message, range_radius) + + +# Global variables. static func get_global_variable(variable_name: String) -> Variant: return Zone.script_network_sync.get_global_variable(variable_name) @@ -15,10 +37,11 @@ static func set_global_variable(variable_name: String, variable_value: Variant) Zone.script_network_sync.set_global_variable(variable_name, variable_value) -static func tween_global_variable(variable_name: String, to_value: Variant, duration: float, trans: Tween.TransitionType, easing: Tween.EaseType) -> void: +static func tween_global_variable(variable_name: String, to_value: Variant, duration: float, trans: Tween.TransitionType = Tween.TRANS_LINEAR, easing: Tween.EaseType = Tween.EASE_IN_OUT) -> void: Zone.script_network_sync.tween_global_variable(variable_name, to_value, duration, trans, easing) +# Object variables. static func get_object_variable(variable_object: Object, variable_name: String) -> Variant: var object_variables = variable_object.get_meta(&"MirrorScriptObjectVariables") return TMDataUtil.get_variable_by_json_path_string(object_variables, variable_name) @@ -45,8 +68,31 @@ static func set_object_variable(variable_object: Object, variable_name: String, Zone.script_network_sync.object_variable_changed.emit(variable_object, variable_name, variable_value) -static func tween_object_variable(variable_node: Node, variable_name: String, to_value: Variant, duration: float, trans: Tween.TransitionType, easing: Tween.EaseType) -> void: +static func tween_object_variable(variable_node: Node, variable_name: String, to_value: Variant, duration: float, trans: Tween.TransitionType = Tween.TRANS_LINEAR, easing: Tween.EaseType = Tween.EASE_IN_OUT) -> void: # Note: Unlike setting a variable, this does not apply immediately. # Since tweening does not have an effect on the same frame anyway, # we can afford to wait for the server to only do a synced tween. Zone.script_network_sync.tween_variable_on_node(variable_node, variable_name, to_value, duration, trans, easing) + + +# Object properties. +static func get_object_property(property_object: Object, property_name: StringName) -> Variant: + return property_object.get(property_name) + + +static func has_object_property(property_object: Object, property_name: StringName) -> bool: + return property_name in property_object + + +static func set_object_property(property_object: Object, property_name: StringName, property_value: Variant) -> void: + property_object.set(property_name, property_value) + + +static func tween_object_property(property_object: Object, property_name: StringName, to_value: Variant, duration: float, trans: Tween.TransitionType = Tween.TRANS_LINEAR, easing: Tween.EaseType = Tween.EASE_IN_OUT) -> void: + if not property_object is Node: + Notify.error("Cannot tween object property", "The target object is not a node, but tweening properties is only supported on nodes for now.") + return + # Note: Unlike setting a property, this does not apply immediately. + # Since tweening does not have an effect on the same frame anyway, + # we can afford to wait for the server to only do a synced tween. + Zone.script_network_sync.tween_property_on_node(property_object, property_name, to_value, duration, trans, easing) diff --git a/mirror-godot-app/script/gd/tmusergdscript_base.gd b/mirror-godot-app/script/gd/tmusergdscript_base.gd new file mode 100644 index 00000000..e6b6fcdf --- /dev/null +++ b/mirror-godot-app/script/gd/tmusergdscript_base.gd @@ -0,0 +1,6 @@ +class_name TMUserGDScriptBase extends Object + + +signal load_exposed_vars() +signal save_exposed_vars() +signal tmusergdscript_runtime_error(error_str: String, frame_index: int, line_num: int, func_name: String) diff --git a/mirror-godot-app/script/network_sync.gd b/mirror-godot-app/script/network_sync.gd index 170de3a4..627b3b7d 100644 --- a/mirror-godot-app/script/network_sync.gd +++ b/mirror-godot-app/script/network_sync.gd @@ -314,7 +314,7 @@ func _set_global_variables_network(variables: Dictionary) -> void: received_variable_data_change.emit() -func tween_global_variable(variable_name: String, to_value: Variant, duration: float, trans: Tween.TransitionType, easing: Tween.EaseType) -> void: +func tween_global_variable(variable_name: String, to_value: Variant, duration: float, trans: Tween.TransitionType = Tween.TRANS_LINEAR, easing: Tween.EaseType = Tween.EASE_IN_OUT) -> void: _net_queue_tweened_global_variables[variable_name] = [to_value, duration, trans, easing] @@ -338,7 +338,7 @@ func _tween_global_variables_network(tweened_variables: Dictionary) -> void: ## Tween a user variable we store inside of a Dictionary using MethodTweener. -func _tween_variable_in_dict(variables_dict: Dictionary, variable_name: String, from_value: Variant, to_value: Variant, duration: float, trans: Tween.TransitionType, easing: Tween.EaseType) -> void: +func _tween_variable_in_dict(variables_dict: Dictionary, variable_name: String, from_value: Variant, to_value: Variant, duration: float, trans: Tween.TransitionType = Tween.TRANS_LINEAR, easing: Tween.EaseType = Tween.EASE_IN_OUT) -> void: var split_path: PackedStringArray = TMDataUtil.split_json_path_string(variable_name) var method_callable: Callable = _tween_variable_callback_method.bind(variables_dict, split_path) var tween: Tween = get_tree().create_tween() @@ -412,7 +412,7 @@ func _set_property_on_node(node: Node, property: StringName, value: Variant) -> node.set(property, value) -func tween_property_on_node(node: Node, property: StringName, to_value: Variant, duration: float, trans: Tween.TransitionType, easing: Tween.EaseType) -> void: +func tween_property_on_node(node: Node, property: StringName, to_value: Variant, duration: float, trans: Tween.TransitionType = Tween.TRANS_LINEAR, easing: Tween.EaseType = Tween.EASE_IN_OUT) -> void: if to_value is Object: Notify.error("Tween Failed", "Tweening an object is not a sensible operation. Aborting.") return @@ -446,7 +446,7 @@ func _tween_properties_on_nodes_network(nodes_tweened_properties: Dictionary) -> _save_tween_results_for_later(node_path, node_tweened_properties, _set_queue_properties_on_nodes) -func _tween_property_on_node(node: Node, property: StringName, to_value: Variant, duration: float, trans: Tween.TransitionType, easing: Tween.EaseType) -> void: +func _tween_property_on_node(node: Node, property: StringName, to_value: Variant, duration: float, trans: Tween.TransitionType = Tween.TRANS_LINEAR, easing: Tween.EaseType = Tween.EASE_IN_OUT) -> void: if not ScriptPropertyRegistration.has_registered_property(property): return var tween: Tween = get_tree().create_tween() @@ -508,13 +508,21 @@ func _set_variable_on_node(node: Node, variable_name: String, variable_value: Va node.set_meta(&"MirrorScriptObjectVariables", {}) var object_variables: Dictionary = node.get_meta(&"MirrorScriptObjectVariables") TMDataUtil.set_variable_by_json_path_string(object_variables, variable_name, variable_value) + # If this is an exposed script variable, set it in the script. + if node.has_method(&"get_script_instances") and Zone.is_host(): + var script_instances: Array[ScriptInstance] = node.get_script_instances() + for script_inst in script_instances: + if script_inst is GDScriptInstance and is_instance_valid(script_inst.script_instance_object): + var obj = script_inst.script_instance_object + if variable_name in obj: + obj.set(variable_name, variable_value) # Keep track of all node variables ever set for use with the variable editor. var node_path: NodePath = node.get_path() var node_variables_in_all: Dictionary = _all_set_variables_on_nodes.get_or_add(node_path, {}) TMDataUtil.set_variable_by_json_path_string(node_variables_in_all, variable_name, variable_value) -func tween_variable_on_node(node: Node, variable: String, to_value: Variant, duration: float, trans: Tween.TransitionType, easing: Tween.EaseType) -> void: +func tween_variable_on_node(node: Node, variable: String, to_value: Variant, duration: float, trans: Tween.TransitionType = Tween.TRANS_LINEAR, easing: Tween.EaseType = Tween.EASE_IN_OUT) -> void: if to_value is Object: Notify.error("Tween Failed", "Tweening an Object value is not a sensible operation. Aborting.") return @@ -549,7 +557,7 @@ func _tween_variables_on_nodes_network(nodes_tweened_variables: Dictionary) -> v received_variable_data_change.emit() -func _tween_variable_on_node(node: Node, variable_name: String, to_value: Variant, duration: float, trans: Tween.TransitionType, easing: Tween.EaseType) -> void: +func _tween_variable_on_node(node: Node, variable_name: String, to_value: Variant, duration: float, trans: Tween.TransitionType = Tween.TRANS_LINEAR, easing: Tween.EaseType = Tween.EASE_IN_OUT) -> void: if not node.has_meta(&"MirrorScriptObjectVariables"): node.set_meta(&"MirrorScriptObjectVariables", {}) var object_variables: Dictionary = node.get_meta(&"MirrorScriptObjectVariables") diff --git a/mirror-godot-app/script/script_instance.gd b/mirror-godot-app/script/script_instance.gd index 17a75ad8..065d2422 100644 --- a/mirror-godot-app/script/script_instance.gd +++ b/mirror-godot-app/script/script_instance.gd @@ -87,8 +87,11 @@ func is_script_instance_setup() -> bool: func setup(node: Node, script_inst_dict: Dictionary) -> void: target_node = node - var script_entity_data: Dictionary = await Net.script_client.get_script_entity(script_id) - setup_script_entity_data(script_entity_data) + var script_entity_data: Dictionary = Net.script_client.get_script_entity(script_id) + # If it's empty, we don't have the data yet. The setup_script_entity_data + # function will be ran later when we receive the data. + if not script_entity_data.is_empty(): + setup_script_entity_data(script_entity_data) func setup_script_instance_data(script_inst_dict: Dictionary) -> void: @@ -145,6 +148,17 @@ func update_script_entity_data_from_network(script_entity_data: Dictionary) -> v # Parameter methods. +func create_inspector_parameter_input(entry_id: String, parameter_port_array: Array) -> void: + assert(false, "This method must be overridden in a derived class.") + + +func set_inspector_parameter_input_value(entry_id: String, param_name: String, new_value: Variant) -> void: + var parameters_for_entry: Dictionary = entry_parameters[entry_id] + var param_data: Array = parameters_for_entry[param_name] + param_data[1] = new_value + apply_inspector_parameter_values() + + func _load_entry_parameters_from_json(entry_parameter_json: Dictionary) -> void: assert(entry_parameters.is_empty(), "This method expects to only be called once. If calling multiple times is needed, add support for that.") entry_parameters = entry_parameter_json.duplicate(true) diff --git a/mirror-godot-app/script/visual/blocks/misc/get_friendly_name.gd b/mirror-godot-app/script/visual/blocks/misc/get_friendly_name.gd index 6d92d3aa..eb92a3e2 100644 --- a/mirror-godot-app/script/visual/blocks/misc/get_friendly_name.gd +++ b/mirror-godot-app/script/visual/blocks/misc/get_friendly_name.gd @@ -4,19 +4,7 @@ extends ScriptBlock func evaluate() -> void: evaluate_inputs() var value = inputs[0].value - if value is SpaceObject: - outputs[0].value = value.get_space_object_name() - elif value is Player: - outputs[0].value = value.get_player_name() - elif value is Node: - outputs[0].value = value.name - elif value == null: - outputs[0].value = "" - elif not is_instance_valid(value): - outputs[0].value = "" - else: - # For non-Node objects, str() is the best we can do. - outputs[0].value = str(value) + outputs[0].value = Mirror.get_friendly_name(value) func get_script_block_type() -> String: diff --git a/mirror-godot-app/script/visual/blocks/script_block.gd b/mirror-godot-app/script/visual/blocks/script_block.gd index bca35f36..5824fdc7 100644 --- a/mirror-godot-app/script/visual/blocks/script_block.gd +++ b/mirror-godot-app/script/visual/blocks/script_block.gd @@ -109,6 +109,8 @@ func _setup_base(block_json: Dictionary) -> void: graph_position = Serialization.array_to_vector2(block_json["position"]) if block_json.has("name"): graph_name = block_json["name"] + elif block_json.has("signal"): + graph_name = "On " + String(block_json["signal"]).capitalize() else: graph_name = String(block_json["type"]).capitalize() diff --git a/mirror-godot-app/script/visual/editor/block_creation/script_block_creation_dialog.gd b/mirror-godot-app/script/visual/editor/block_creation/script_block_creation_dialog.gd index c8b5632c..8ea17872 100644 --- a/mirror-godot-app/script/visual/editor/block_creation/script_block_creation_dialog.gd +++ b/mirror-godot-app/script/visual/editor/block_creation/script_block_creation_dialog.gd @@ -112,7 +112,7 @@ func _on_confirmed(): var desired_block_json: Dictionary = _script_block_creation_menu.get_desired_block_json() if desired_block_json.is_empty(): return - if desired_block_json["type"] == "entry" and not desired_block_json.has("signal"): + if desired_block_json["type"] == "entry" and desired_block_json["signal"] == &"custom_signal": request_show_entry_creation_dialog.emit() return create_block.emit(desired_block_json) diff --git a/mirror-godot-app/script/visual/editor/visual_script_editor.gd b/mirror-godot-app/script/visual/editor/visual_script_editor.gd index dbed5727..ccf8e5e5 100644 --- a/mirror-godot-app/script/visual/editor/visual_script_editor.gd +++ b/mirror-godot-app/script/visual/editor/visual_script_editor.gd @@ -148,7 +148,7 @@ func _on_request_block_creation(constraint: int, data_type: int, index: int, fro func _on_request_entry_creation(where: Vector2) -> void: _creation_position = where - request_show_entry_creation_dialog.emit(_script_instance.target_node) + request_show_custom_entry_creation_dialog.emit(_script_instance.target_node) func _on_request_input_value_edit(graph_node: ScriptBlockGraphNode, input_port: ScriptBlock.ScriptBlockInputPort) -> void: diff --git a/mirror-godot-app/script/visual/visual_script_instance.gd b/mirror-godot-app/script/visual/visual_script_instance.gd index 24e5c924..09c4cac7 100644 --- a/mirror-godot-app/script/visual/visual_script_instance.gd +++ b/mirror-godot-app/script/visual/visual_script_instance.gd @@ -137,6 +137,15 @@ func _create_event_node(entry_block: ScriptBlockEntryBase) -> void: entry_block.entry_node = event_node +func create_inspector_parameter_input(entry_id: String, parameter_port_array: Array) -> void: + for entry_block in script_builder.entry_blocks: + if entry_block.entry_id == entry_id: + entry_block.parameters.create_inspector_parameter(parameter_port_array) + entry_block.reset_entry_output_ports() + break + sync_script_inst_params_with_script_data() + + ## Ensure the script instance entry inspector parameters match the ## signature of the script's data. Since a script may be used by ## multiple objects, parameters may get out of sync without this code.