From e158173104e586eeefb3e3d83fc5c94b432424b0 Mon Sep 17 00:00:00 2001 From: JeremyStarTM Date: Sun, 14 Jan 2024 21:52:38 +0100 Subject: [PATCH] Update source code - [Console.tscn, src/console.gd, src/console_info.gd, src/console_expressionscript.gd] Add (mostly uncommented) debugging console - [example/test.gd] Optimize example presentation controller - [src/loader.gd, src/pmana.gd] Move click overlay into Loader - [src/pmana.gd] Add check against invalid controller scripts - [src/pmana.gd] Create seperate pmana.shutdown() function to handle shutdowns during presentations - [src/preader.gd] Add checks against empty manifest variable - [src/preader.gd] Fix bug in get_authors() - Change/optimize various other smaller things --- .gitignore | 2 +- Console.tscn | 97 +++++++ docs/docs/reference/console.md | 60 +++++ example/test.gd | 1 + project.godot | 6 + src/clickoverlay.gd | 2 - src/console.gd | 457 ++++++++++++++++++++++++++++++++ src/console_expressionscript.gd | 33 +++ src/console_info.gd | 156 +++++++++++ src/loader.gd | 4 + src/misc.gd | 2 +- src/pmana.gd | 29 +- src/preader.gd | 32 ++- 13 files changed, 866 insertions(+), 15 deletions(-) create mode 100644 Console.tscn create mode 100644 docs/docs/reference/console.md create mode 100644 src/console.gd create mode 100644 src/console_expressionscript.gd create mode 100644 src/console_info.gd diff --git a/.gitignore b/.gitignore index 4709183..ab0abeb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -# Godot 4+ specific ignores .godot/ +bin/ diff --git a/Console.tscn b/Console.tscn new file mode 100644 index 0000000..94d11bc --- /dev/null +++ b/Console.tscn @@ -0,0 +1,97 @@ +[gd_scene load_steps=10 format=3 uid="uid://btyi16dkvbly1"] + +[ext_resource type="Script" path="res://src/console.gd" id="1_viv5y"] +[ext_resource type="FontFile" uid="uid://bl4vgye7bg8kf" path="res://docs/static/assets/fonts/FiraCode-Regular.ttf" id="2_ebuk0"] +[ext_resource type="FontFile" uid="uid://b5qsng8gpvos3" path="res://docs/static/assets/fonts/FiraCode-Bold.ttf" id="3_102to"] +[ext_resource type="FontFile" uid="uid://c5jkbqx10tj8e" path="res://docs/static/assets/fonts/FiraCode-Medium.ttf" id="4_jqfsc"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_u40jo"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_wj4aw"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_ime8b"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_hjtld"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_xa0gg"] + +[node name="Console" type="ColorRect"] +offset_right = 767.0 +offset_bottom = 426.0 +script = ExtResource("1_viv5y") + +[node name="Bar" type="ColorRect" parent="."] +layout_mode = 0 +offset_left = 2.5 +offset_top = 2.5 +offset_right = 764.5 +offset_bottom = 27.5 +color = Color(0.839216, 0.0196078, 0.196078, 1) + +[node name="X1" type="Line2D" parent="Bar"] +points = PackedVector2Array(760, 2, 739, 23) +width = 2.0 + +[node name="X2" type="Line2D" parent="Bar"] +points = PackedVector2Array(760, 23, 739, 2) +width = 2.0 + +[node name="CloseButton" type="Button" parent="Bar"] +layout_mode = 0 +offset_left = 737.0 +offset_right = 762.0 +offset_bottom = 25.0 +theme_override_styles/normal = SubResource("StyleBoxEmpty_u40jo") +theme_override_styles/hover = SubResource("StyleBoxEmpty_wj4aw") +theme_override_styles/pressed = SubResource("StyleBoxEmpty_ime8b") +theme_override_styles/disabled = SubResource("StyleBoxEmpty_hjtld") +theme_override_styles/focus = SubResource("StyleBoxEmpty_xa0gg") + +[node name="DragButton" type="Button" parent="Bar"] +layout_mode = 0 +offset_right = 737.0 +offset_bottom = 25.0 +theme_override_styles/normal = SubResource("StyleBoxEmpty_u40jo") +theme_override_styles/hover = SubResource("StyleBoxEmpty_wj4aw") +theme_override_styles/pressed = SubResource("StyleBoxEmpty_ime8b") +theme_override_styles/disabled = SubResource("StyleBoxEmpty_hjtld") +theme_override_styles/focus = SubResource("StyleBoxEmpty_xa0gg") + +[node name="Shell" type="ColorRect" parent="."] +layout_mode = 0 +offset_left = 2.5 +offset_top = 27.5 +offset_right = 764.5 +offset_bottom = 423.5 +color = Color(0, 0, 0, 1) + +[node name="Output" type="RichTextLabel" parent="Shell"] +layout_mode = 0 +offset_right = 762.0 +offset_bottom = 361.0 +theme_override_fonts/normal_font = ExtResource("2_ebuk0") +theme_override_fonts/bold_font = ExtResource("3_102to") +theme_override_fonts/italics_font = ExtResource("2_ebuk0") +theme_override_fonts/bold_italics_font = ExtResource("3_102to") +theme_override_fonts/mono_font = ExtResource("2_ebuk0") +theme_override_font_sizes/normal_font_size = 18 +theme_override_font_sizes/bold_font_size = 18 +theme_override_font_sizes/italics_font_size = 18 +theme_override_font_sizes/bold_italics_font_size = 18 +theme_override_font_sizes/mono_font_size = 18 +bbcode_enabled = true +scroll_following = true + +[node name="Input" type="TextEdit" parent="Shell"] +layout_mode = 0 +offset_top = 361.0 +offset_right = 762.0 +offset_bottom = 396.0 +theme_override_colors/background_color = Color(0, 0, 0, 1) +theme_override_colors/font_color = Color(1, 1, 1, 1) +theme_override_fonts/font = ExtResource("4_jqfsc") +theme_override_font_sizes/font_size = 18 +placeholder_text = "Enter a command here" + +[connection signal="pressed" from="Bar/CloseButton" to="." method="close_console"] +[connection signal="text_changed" from="Shell/Input" to="." method="input_changed"] diff --git a/docs/docs/reference/console.md b/docs/docs/reference/console.md new file mode 100644 index 0000000..d5e3b7c --- /dev/null +++ b/docs/docs/reference/console.md @@ -0,0 +1,60 @@ +--- +sidebar_position: 3 +--- + +# Debugging console +Presencode includes a debugging console that you can use to debug your presentation code, Presencode itself... or to have fun destroying Presencode from the inside. + +## Accessing the debug console +To make the debug console visible, press the `^` key on your keyboard. +To get started, type `help` into the input box. + +## Commands +You can use these commands inside the debugging console: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CommandDescriptionArguments
clearClears the console output
exitStarts a fresh console session
shutdownShuts Presencode down[exitcode: int = 0]
helpDisplays information about certain topics[topic: String = "INDEX"]
configReads and writes to the in-memory configuration[get <key: String>|set <key: String> <value: Variant>|list]
pmanaControls the Presentation Manager[register <version: int> <slides: int> <animations: bool> <quit_last_slide: bool> <controller: NodePath<String>>|unregister|change_slide <slide: int> [no_animations: bool = false]|clear_viewport|hide_log|show_log]
preaderControls the Presentation Reader<get <topic|authors|ratio|resolution>>
arbitraryExecute arbitrary GDScript expressions<gdscript expression>
\ No newline at end of file diff --git a/example/test.gd b/example/test.gd index d676101..e5970f3 100644 --- a/example/test.gd +++ b/example/test.gd @@ -78,6 +78,7 @@ func presentation_start(viewport_: Control) -> void: ## without calling this function. func presentation_end() -> void: logger.diag("presentation_end() called") + queue_free() # Change the current slide to another one func change_slide(new_slide: int) -> void: diff --git a/project.godot b/project.godot index 1766963..1b80571 100644 --- a/project.godot +++ b/project.godot @@ -70,6 +70,12 @@ content_scale_switch={ , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":true,"meta_pressed":false,"pressed":false,"keycode":71,"physical_keycode":0,"key_label":0,"unicode":103,"echo":false,"script":null) ] } +console={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":0,"key_label":96,"unicode":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":96,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null) +] +} [rendering] diff --git a/src/clickoverlay.gd b/src/clickoverlay.gd index 0ac901d..47d6ba4 100644 --- a/src/clickoverlay.gd +++ b/src/clickoverlay.gd @@ -46,7 +46,5 @@ func clicked() -> void: pmana.change_slide(pmana.current_slide+1, true) func _process(_delta: float) -> void: - # Move button to top - get_tree().root.move_child(self, get_tree().root.get_child_count(true)) # Scale clickoverlay to window size size = DisplayServer.window_get_size() diff --git a/src/console.gd b/src/console.gd new file mode 100644 index 0000000..b21f435 --- /dev/null +++ b/src/console.gd @@ -0,0 +1,457 @@ +############################################################################## +### PRESENCODE SOURCE FILE ### +### Copyright (c) 2024 JeremyStarTM & Contributors ### +### Licensed under the GNU General Public License v3 ### +### ### +### This program is free software: you can redistribute it and/or modify ### +### it under the terms of the GNU General Public License as published by ### +### the Free Software Foundation, either version 3 of the License, or ### +### (at your option) any later version. ### +### ### +### This program is distributed in the hope that it will be useful, ### +### but WITHOUT ANY WARRANTY; without even the implied warranty of ### +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ### +### GNU General Public License for more details. ### +### ### +### You should have received a copy of the GNU General Public License ### +### along with this program. If not, see . ### +############################################################################## +### src/console.gd (Debug console) ### +### ### +### This source file controls the debug console, which can be enabled by ### +### pressing the ^ key. ### +############################################################################## +extends ColorRect + +# Enums +enum BooleanState { + TRUE, + FALSE, + INVALID +} + +# Nodes +@onready var input: TextEdit = get_node("Shell/Input") +@onready var output: RichTextLabel = get_node("Shell/Output") +var expressionscript: Node = Node.new() + +# Console information +@onready var info: ConsoleInfo = ConsoleInfo.new() + +# States +var dragging: bool = false +var drag_area: Vector2 = Vector2(0, 0) +var cursor_origin: Vector2 = Vector2(0, 0) + +func _ready() -> void: + logger.info("Initializing debug console") + position = Vector2(30, 30) + calculate_drag_area() + # Setup window dragging + $Bar/DragButton.connect("button_down", func(): + dragging = true + cursor_origin = get_tree().root.get_viewport().get_mouse_position() + ) + $Bar/DragButton.connect("button_up", func(): + dragging = false + cursor_origin = Vector2(0,0) + ) + expressionscript.name = "ExpressionScript" + expressionscript.set_script(ResourceLoader.load("res://src/console_expressionscript.gd")) + expressionscript.logger = get_node("/root/logger") + expressionscript.misc = get_node("/root/misc") + expressionscript.preader = get_node("/root/preader") + expressionscript.pmana = get_node("/root/pmana") + expressionscript.console = self + expressionscript.loader = get_node("/root/Presencode") + add_child(expressionscript) + await process_command(PackedStringArray(["exit"])) + logger.info("Debug console initialized") + +func _process(_delta: float) -> void: + # Move to top + get_tree().root.move_child(self, get_tree().root.get_child_count(true)) + # Visibility toggle key + if Input.is_action_just_pressed("console"): + logger.info("Toggling console visibility") + visible = !visible + if dragging: + # Get new cursor position + var cursor_position = get_tree().root.get_viewport().get_mouse_position() + # If nothing changed, don't do anything + if cursor_origin != cursor_position: + # Calculate cursor offset + var cursor_offset: Vector2 = Vector2(0, 0) + cursor_offset.x = cursor_origin.x-cursor_position.x + cursor_offset.y = cursor_origin.y-cursor_position.y + # Change console position + position.x = position.x-cursor_offset.x + position.y = position.y-cursor_offset.y + # Prevent console from going out of scope + var position_new: Vector2 = check_violation() + if position != position_new: + position = position_new + # Set new cursor_origin + cursor_origin = cursor_position + +# Helper function, prevents console from going out of scope +func check_violation() -> Vector2: + var new_position: Vector2 = position + if position.x <= 0: + new_position.x = 0 + elif position.x >= drag_area.x: + new_position.x = drag_area.x + if position.y <= 0: + new_position.y = 0 + elif position.y >= drag_area.y: + new_position.y = drag_area.y + return new_position + +# Calculates a new drag_area +func calculate_drag_area(area: Vector2i = DisplayServer.window_get_size()) -> void: + drag_area.x = area.x-size.x + drag_area.y = area.y-size.y + +# Close console on close button press +func close_console() -> void: + logger.info("Closing console") + visible = false + +# Input changed +func input_changed() -> void: + if input.text.contains("\n"): + input.text = input.text.replace("\n", "") + var input2: String = input.text + input.text = "" + input.editable = false + await process_command(input2.split(" ", false)) + input.editable = true + +# Process commands +func process_command(command: PackedStringArray) -> void: + logger.diag("Processing command [" + " ".join(command) + "]") + await get_tree().process_frame + append_output("[color=gray]$ " + " ".join(command) + "[color=white]") + if command.size() == 0: return + match(command[0]): + "clear": + match(command.size()): + _: append_output(info.get_error_string(info.ConsoleError.TOO_MANY_ARGUMENTS)) + output.text = "" + "exit": + match(command.size()): + _: append_output(info.get_error_string(info.ConsoleError.TOO_MANY_ARGUMENTS)) + await process_command(PackedStringArray(["clear"])) + append_output("""[color=#d60532]Welcome to the Presencode debug console! + +To get started, enter \"help\". To close the console, press the X button.[color=white]""") + "shutdown": + append_output("Shutting down...") + var exitcode: int = 0 + match(command.size()): + 1: exitcode = 0 + 2: exitcode = int(command[1]) + _: append_output(info.get_error_string(info.ConsoleError.TOO_MANY_ARGUMENTS)) + pmana.shutdown(exitcode) + "help": + match(command.size()): + 1: append_output(info.get_help_topic(info.HelpTopic.INDEX)) + 2: + match(command[1]): + "index": append_output(info.get_help_topic(info.HelpTopic.INDEX)) + "clear": append_output(info.get_help_topic(info.HelpTopic.CLEAR)) + "exit": append_output(info.get_help_topic(info.HelpTopic.EXIT)) + "shutdown": append_output(info.get_help_topic(info.HelpTopic.SHUTDOWN)) + "help": append_output(info.get_help_topic(info.HelpTopic.HELP)) + "config": append_output(info.get_help_topic(info.HelpTopic.CONFIG)) + "pmana": append_output(info.get_help_topic(info.HelpTopic.PMANA)) + "preader": append_output(info.get_help_topic(info.HelpTopic.PREADER)) + "arbitrary": append_output(info.get_help_topic(info.HelpTopic.ARBITRARY)) + _: append_output(info.get_error_string(info.ConsoleError.INVALID_HELP_TOPIC)) + _: append_output(info.get_error_string(info.ConsoleError.TOO_MANY_ARGUMENTS)) + "config": + match(command.size()): + 1: append_output(info.get_error_string(ConsoleInfo.ConsoleError.TOO_FEW_ARGUMENTS)) + _: + match(command[1]): + "list": + match(command.size()): + 2: + append_output("""These configuration keys can be used: +Key | Type | Description +LOGGER_ENABLED | bool | Toggles the logger +LOGGER_DIAGNOSTIC | bool | Toggles diagnostic log output +LOGGER_COLORED | bool | Toggles colored log output +LOGGER_HARDFAIL | bool | Toggles hardfailing errors (don't set this to false) +LOGGER_LOGSTRING | String | The logging template, variables: runtime, time, file, function, line, color, type, message +PMANA_ALLOW_FULLSCREEN | bool | Toggles window mode switching +MISC_SHUTDOWN_INVISIBLE | bool | Toggles if the window should be made invisible on shutdown, displays white texture if disabled""") + _: append_output(info.get_error_string(ConsoleInfo.ConsoleError.TOO_MANY_ARGUMENTS)) + "set": + match(command.size()): + 2: append_output(info.get_error_string(ConsoleInfo.ConsoleError.TOO_FEW_ARGUMENTS)) + 3: append_output(info.get_error_string(ConsoleInfo.ConsoleError.TOO_FEW_ARGUMENTS)) + _: + match(command[2]): + "LOGGER_ENABLED": + match(get_boolean(" ".join(get_last(command, 2)))): + BooleanState.TRUE: + logger.config_enabled = true + append_output("Set \"" + str(command[2]) + "\" to [b]true[/b].") + BooleanState.FALSE: + logger.config_enabled = false + append_output("Set \"" + str(command[2]) + "\" to [b]false[/b]") + _: append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_TYPE, {"expected_type": "bool"})) + "LOGGER_DIAGNOSTIC": + match(get_boolean(" ".join(get_last(command, 2)))): + BooleanState.TRUE: + logger.config_diagnostic = true + append_output("Set \"" + str(command[2]) + "\" to [b]true[/b]") + BooleanState.FALSE: + logger.config_diagnostic = false + append_output("Set \"" + str(command[2]) + "\" to [b]false[/b]") + _: append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_TYPE, {"expected_type": "bool"})) + "LOGGER_COLORED": + match(get_boolean(" ".join(get_last(command, 2)))): + BooleanState.TRUE: + logger.config_colored = true + append_output("Set \"" + str(command[2]) + "\" to [b]true[/b]") + BooleanState.FALSE: + logger.config_colored = false + append_output("Set \"" + str(command[2]) + "\" to [b]false[/b]") + _: append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_TYPE, {"expected_type": "bool"})) + "LOGGER_HARDFAIL": + match(get_boolean(" ".join(get_last(command, 2)))): + BooleanState.TRUE: + logger.config_hardfail = true + append_output("Set \"" + str(command[2]) + "\" to [b]true[/b]") + BooleanState.FALSE: + logger.config_hardfail = false + append_output("Set \"" + str(command[2]) + "\" to [b]false[/b]") + _: append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_TYPE, {"expected_type": "bool"})) + "LOGGER_LOGSTRING": + logger.config_logstring = " ".join(PackedStringArray(get_last(command, 2))) + append_output("Set \"" + str(command[2]) + "\" to [b]\"" + logger.config_logstring + "\"[/b]") + "PMANA_ALLOW_FULLSCREEN": + match(get_boolean(" ".join(get_last(command, 2)))): + BooleanState.TRUE: + pmana.config_allow_fullscreen = true + append_output("Set \"" + str(command[2]) + "\" to [b]true[/b]") + BooleanState.FALSE: + pmana.config_allow_fullscreen = false + append_output("Set \"" + str(command[2]) + "\" to [b]false[/b]") + _: append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_TYPE, {"expected_type": "bool"})) + "MISC_SHUTDOWN_INVISIBLE": + match(get_boolean(" ".join(get_last(command, 2)))): + BooleanState.TRUE: + misc.config_shutdown_invisible = true + append_output("Set \"" + str(command[2]) + "\" to [b]true[/b]") + BooleanState.FALSE: + misc.config_shutdown_invisible = false + append_output("Set \"" + str(command[2]) + "\" to [b]false[/b]") + _: append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_TYPE, {"expected_type": "bool"})) + _: append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_CONFIG_KEY, {"key": command[2]})) + "get": + match(command.size()): + 2: append_output(info.get_error_string(ConsoleInfo.ConsoleError.TOO_FEW_ARGUMENTS)) + 3: + match(command[2]): + "LOGGER_ENABLED": append_output("[b]" + command[2] + "[/b] is set to [b]" + str(logger.config_enabled) + "[/b]") + "LOGGER_DIAGNOSTIC": append_output("[b]" + command[2] + "[/b] is set to [b]" + str(logger.config_diagnostic) + "[/b]") + "LOGGER_COLORED": append_output("[b]" + command[2] + "[/b] is set to [b]" + str(logger.config_colored) + "[/b]") + "LOGGER_HARDFAIL": append_output("[b]" + command[2] + "[/b] is set to [b]" + str(logger.config_hardfail) + "[/b]") + "LOGGER_LOGSTRING": append_output("\"" + command[2] + "[/b] is set to [b]\"" + logger.config_logstring + "\"[/b]") + "PMANA_ALLOW_FULLSCREEN": append_output("[b]" + command[2] + "[/b] is set to [b]" + str(pmana.config_allow_fullscreen) + "[/b]") + "MISC_SHUTDOWN_INVISIBLE": append_output("[b]" + command[2] + "[/b] is set to [b]" + str(misc.config_shutdown_invisible) + "[/b]") + _: append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_CONFIG_KEY, {"key": command[2]})) + _: append_output(info.get_error_string(ConsoleInfo.ConsoleError.TOO_MANY_ARGUMENTS)) + _: append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_ARGUMENT)) + "pmana": + match(command.size()): + 1: append_output(info.get_error_string(ConsoleInfo.ConsoleError.TOO_FEW_ARGUMENTS)) + _: + match(command[1]): + "register": + match(command.size()): + 1: append_output(info.get_error_string(ConsoleInfo.ConsoleError.TOO_FEW_ARGUMENTS)) + 2: append_output(info.get_error_string(ConsoleInfo.ConsoleError.TOO_FEW_ARGUMENTS)) + 3: append_output(info.get_error_string(ConsoleInfo.ConsoleError.TOO_FEW_ARGUMENTS)) + 4: append_output(info.get_error_string(ConsoleInfo.ConsoleError.TOO_FEW_ARGUMENTS)) + 5: append_output(info.get_error_string(ConsoleInfo.ConsoleError.TOO_FEW_ARGUMENTS)) + 6: append_output(info.get_error_string(ConsoleInfo.ConsoleError.TOO_FEW_ARGUMENTS)) + 7: + var version: Vector2 = get_int_direct(command[2]) + var slides: Vector2 = get_int_direct(command[3]) + var animations: BooleanState = get_boolean(command[4]) + var animations_bool: bool = false + var quit_last_slide: BooleanState = get_boolean(command[5]) + var quit_last_slide_bool: bool = false + var controller: NodePath = NodePath(command[6]) + if version.x != 0: + append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_TYPE, {"expected_type": "int"})) + return + if slides.x != 0: + append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_TYPE, {"expected_type": "int"})) + return + match(animations): + BooleanState.TRUE: animations_bool = true + BooleanState.FALSE: animations_bool = false + _: + append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_TYPE, {"expected_type": "bool"})) + return + match(quit_last_slide): + BooleanState.TRUE: quit_last_slide_bool = true + BooleanState.FALSE: quit_last_slide_bool = false + _: + append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_TYPE, {"expected_type": "bool"})) + return + if get_node(controller) == null or typeof(get_node(controller)) != TYPE_OBJECT: + append_output(info.get_error_string(ConsoleInfo.ConsoleError.NOT_AN_OBJECT)) + return + if get_node(controller).get_script() == null: + append_output(info.get_error_string(ConsoleInfo.ConsoleError.NO_SCRIPT_ATTACHED)) + return + @warning_ignore("narrowing_conversion") + pmana.register(version.y, slides.y, animations_bool, quit_last_slide_bool, controller) + append_output("A new presentation controller has been registered. Look into the Presencode log output if nothing happens.") + _: append_output(info.get_error_string(ConsoleInfo.ConsoleError.TOO_MANY_ARGUMENTS)) + "unregister": + if !pmana.registered: append_output(info.get_error_string(ConsoleInfo.ConsoleError.NO_CONTROLLER_REGISTERED)) + else: + await pmana.unregister() + append_output("The current presentation controller has been unregistered.") + "change_slide": + match(command.size()): + 3: + if !pmana.registered: append_output(info.get_error_string(ConsoleInfo.ConsoleError.NO_CONTROLLER_REGISTERED)) + else: + var slide: Vector2i = get_int_direct(command[2]) + match(slide.x): + 0: + pmana.change_slide(slide.y) + append_output("Switched to slide [b]" + str(slide.y) + "[/b]") + 1: append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_TYPE, {"expected_type": "int"})) + _: append_output(info.generate_internal_error("Invalid get_int() status number")) + 4: + if !pmana.registered: append_output(info.get_error_string(ConsoleInfo.ConsoleError.NO_CONTROLLER_REGISTERED)) + else: + var slide: Vector2i = get_int_direct(command[2]) + match(slide.x): + 0: + var no_animations: BooleanState = get_boolean(command[3]) + match(no_animations): + BooleanState.TRUE: + pmana.change_slide(slide.y, true) + append_output("Switched to slide [b]" + str(slide.y) + "[/b] without animations") + BooleanState.FALSE: + pmana.change_slide(slide.y, false) + append_output("Switched to slide [b]" + str(slide.y) + "[/b]") + BooleanState.INVALID: + append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_TYPE, {"expected_type": "bool"})) + 1: append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_TYPE, {"expected_type": "int"})) + _: append_output(info.generate_internal_error("Invalid get_int() status number")) + _: append_output(info.get_error_string(ConsoleInfo.ConsoleError.TOO_MANY_ARGUMENTS)) + "clear_viewport": + if !pmana.registered: append_output(info.get_error_string(ConsoleInfo.ConsoleError.NO_CONTROLLER_REGISTERED)) + else: + pmana.clear_viewport() + append_output("The viewport has been cleared.") + "hide_log": + if !pmana.registered: append_output(info.get_error_string(ConsoleInfo.ConsoleError.NO_CONTROLLER_REGISTERED)) + else: + pmana.hide_log() + append_output("The log output is now [b]invisible[/b].") + "show_log": + if !pmana.registered: append_output(info.get_error_string(ConsoleInfo.ConsoleError.NO_CONTROLLER_REGISTERED)) + else: + pmana.show_log() + append_output("The log output is now [b]visible[/b].") + _: append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_ARGUMENT)) + "preader": + match(command.size()): + 1: append_output(info.get_error_string(ConsoleInfo.ConsoleError.TOO_FEW_ARGUMENTS)) + _: + match(command[1]): + "get": + match(command.size()): + 2: append_output(info.get_error_string(ConsoleInfo.ConsoleError.TOO_FEW_ARGUMENTS)) + 3: + match(command[2]): + "topic": + if preader.get_topic() == "": append_output(info.get_error_string(ConsoleInfo.ConsoleError.NO_PRESENTATION_OPEN)) + else: append_output("The presentation topic is [b]" + preader.get_topic() + "[/b]") + "authors": + if preader.get_authors() == "": append_output(info.get_error_string(ConsoleInfo.ConsoleError.NO_PRESENTATION_OPEN)) + else: append_output("The presentation author is/authors are: [b]" + preader.get_authors() + "[/b]") + "ratio": + if preader.get_ratio() == "": append_output(info.get_error_string(ConsoleInfo.ConsoleError.NO_PRESENTATION_OPEN)) + else: append_output("The presentation ratio is [b]" + preader.get_ratio() + "[/b]") + "resolution": + if preader.get_ratio_resolution() == Vector2i(0, 0): append_output(info.get_error_string(ConsoleInfo.ConsoleError.NO_PRESENTATION_OPEN)) + else: + var resolution: Vector2i = preader.get_ratio_resolution() + append_output("The presentation resolution is [b]" + str(resolution.x) + "x" + str(resolution.y) + "[/b].") + _: append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_ARGUMENT)) + _: append_output(info.get_error_string(ConsoleInfo.ConsoleError.TOO_MANY_ARGUMENTS)) + _: append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_ARGUMENT)) + "arbitrary": + match(command.size()): + 1: append_output(info.get_error_string(ConsoleInfo.ConsoleError.TOO_FEW_ARGUMENTS)) + _: + var expression_raw: String = " ".join(PackedStringArray(get_last(command, 0))) + var expression: Expression = Expression.new() + if expression.parse(expression_raw) != Error.OK: + append_output(info.get_error_string(ConsoleInfo.ConsoleError.EXPRESSION_PARSING_FAILED, {"error": expression.get_error_text()})) + else: + var returned = expression.execute([], expressionscript) + if expression.has_execute_failed(): + append_output(info.get_error_string(ConsoleInfo.ConsoleError.EXPRESSION_EXECUTION_FAILED, {"error": expression.get_error_text(), "returned": returned})) + else: + append_output("""Executed arbitrary expression successfully. +Returned: """ + str(returned)) + _: append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_COMMAND)) + +# Helper function. I'm bad at explaining and naming things, just look at the code... please. +func get_last(array: Array, n: int) -> Array: + # Makes it easier + n = n+1 + # Create new array + var new_array: Array = [] + if n >= array.size(): + logger.error("n is bigger than array.size()") + return [] + for value in array: + if n == 0: # Append to new_array + new_array.append(value) + else: # Count down + n = n-1 + return new_array + +# Helper function. Tries to get a bool from an Array. +func get_boolean(string: String) -> BooleanState: + match(string): + "true": return BooleanState.TRUE + "false": return BooleanState.FALSE + _: return BooleanState.INVALID + +# Helper function. Tries to get a int from an Array. +## Returns Vector2i(0, NUMBER) if successful +## Returns Vector2i(1, 0) if not successful +func get_int(array: Array) -> Vector2i: + for value in array: + if str(value).is_valid_int(): return Vector2i(0, int(value)) + return Vector2i(1, 0) + +# Helper function. Tries to get a int from a String. +## Returns Vector2i(0, NUMBER) if successful +## Returns Vector2i(1, 0) if not successful +func get_int_direct(string: String) -> Vector2i: + if string.is_valid_int(): return Vector2i(0, int(string)) + else: return Vector2i(1, 0) + +# Helper function. Appends text to output.text +func append_output(text: String, newline: bool = true) -> void: + if newline: + output.text = output.text + text + "\n" + else: + output.text = output.text + text diff --git a/src/console_expressionscript.gd b/src/console_expressionscript.gd new file mode 100644 index 0000000..cb640b8 --- /dev/null +++ b/src/console_expressionscript.gd @@ -0,0 +1,33 @@ +############################################################################## +### PRESENCODE SOURCE FILE ### +### Copyright (c) 2024 JeremyStarTM & Contributors ### +### Licensed under the GNU General Public License v3 ### +### ### +### This program is free software: you can redistribute it and/or modify ### +### it under the terms of the GNU General Public License as published by ### +### the Free Software Foundation, either version 3 of the License, or ### +### (at your option) any later version. ### +### ### +### This program is distributed in the hope that it will be useful, ### +### but WITHOUT ANY WARRANTY; without even the implied warranty of ### +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ### +### GNU General Public License for more details. ### +### ### +### You should have received a copy of the GNU General Public License ### +### along with this program. If not, see . ### +############################################################################## +### src/console_expressionscript.gd (Debug console expression playground) ### +### ### +### This file will be used as a script for executing arbitrary expressions ### +### by the debugging console. ### +############################################################################## +extends Node + +# @GlobalScope +var logger: Node = null +var misc: Node = null +var preader: Node = null +var pmana: Node = null +# SceneTree +var console: Node = null +var loader: Node = null diff --git a/src/console_info.gd b/src/console_info.gd new file mode 100644 index 0000000..9c15e15 --- /dev/null +++ b/src/console_info.gd @@ -0,0 +1,156 @@ +############################################################################## +### PRESENCODE SOURCE FILE ### +### Copyright (c) 2024 JeremyStarTM & Contributors ### +### Licensed under the GNU General Public License v3 ### +### ### +### This program is free software: you can redistribute it and/or modify ### +### it under the terms of the GNU General Public License as published by ### +### the Free Software Foundation, either version 3 of the License, or ### +### (at your option) any later version. ### +### ### +### This program is distributed in the hope that it will be useful, ### +### but WITHOUT ANY WARRANTY; without even the implied warranty of ### +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ### +### GNU General Public License for more details. ### +### ### +### You should have received a copy of the GNU General Public License ### +### along with this program. If not, see . ### +############################################################################## +### src/console_info.gd (Debug console information) ### +### ### +### This source file holds console information such as help topics or ### +### console errors. ### +############################################################################## +extends Node +class_name ConsoleInfo + +enum ConsoleError { + OK, + UNIMPLEMENTED, + INVALID_COMMAND, + TOO_MANY_ARGUMENTS, + TOO_FEW_ARGUMENTS, + INVALID_ARGUMENT, + INVALID_TYPE, + NOT_AN_OBJECT, + NO_SCRIPT_ATTACHED, + INVALID_HELP_TOPIC, + INVALID_CONFIG_KEY, + NO_PRESENTATION_OPEN, + CONTROLLER_ALREADY_REGISTERED, + NO_CONTROLLER_REGISTERED, + EXPRESSION_PARSING_FAILED, + EXPRESSION_EXECUTION_FAILED +} +enum HelpTopic { + INDEX, + CLEAR, + EXIT, + SHUTDOWN, + HELP, + CONFIG, + PMANA, + PREADER, + ARBITRARY +} + +var error_color: String = "[color=red]" +var internal_error_color: String = "[b][color=red]" + +func _init() -> void: + logger.diag("ConsoleInfo initialized") + +func generate_internal_error(error: String) -> String: + var origin = misc.get_origin() + logger.warn("Debug console (" + origin["file"] + ":" + origin["function"] + ":" + str(origin["line"]) + ") experienced an internal error: " + error) + return internal_error_color + "INTERNAL ERROR (" + error + ") [" + origin["file"] + ":" + origin["function"] + ":" + str(origin["line"]) + "]" + +func get_error_string(error: ConsoleError, context: Dictionary = {}) -> String: + match(error): + ConsoleError.OK: + return generate_internal_error("ConsoleError.OK is not a valid error") + ConsoleError.UNIMPLEMENTED: + return error_color + "Function unimplemented, aborting." + ConsoleError.INVALID_COMMAND: + return error_color + "Invalid command. Execute \"help\" for a list of all available commands." + ConsoleError.TOO_MANY_ARGUMENTS: + return error_color + "Too many arguments." + ConsoleError.TOO_FEW_ARGUMENTS: + return error_color + "Too few arguments." + ConsoleError.INVALID_ARGUMENT: + return error_color + "Invalid argument." + ConsoleError.INVALID_TYPE: + return error_color + "Invalid type. Command expected type " + str(context["expected_type"]) + "." + ConsoleError.NOT_AN_OBJECT: + return error_color + "Invalid type. Command expected a valid NodePath." + ConsoleError.NO_SCRIPT_ATTACHED: + return error_color + "Invalid object. Command expected a script-attached object." + ConsoleError.INVALID_HELP_TOPIC: + return error_color + "Invalid help topic." + ConsoleError.INVALID_CONFIG_KEY: + return error_color + "Invalid config key \"" + str(context["key"]) + "\". Execute \"config list\" for a list of all config keys." + ConsoleError.NO_PRESENTATION_OPEN: + return error_color + "No presentation is currently open." + ConsoleError.CONTROLLER_ALREADY_REGISTERED: + return error_color + "A presentation controller has been registered already." + ConsoleError.NO_CONTROLLER_REGISTERED: + return error_color + "No presentation controller has been registered yet." + ConsoleError.EXPRESSION_PARSING_FAILED: + return error_color + """Could not parse arbitrary expression successfully. +Error: """ + str(context["error"]) + ConsoleError.EXPRESSION_EXECUTION_FAILED: + return error_color + """Could not execute arbitrary expression successfully. +Error: """ + str(context["error"]) + """ +Returned: """ + str(context["returned"]) + _: + return generate_internal_error("Invalid ConsoleError \"" + str(error) + "\", context: " + str(context)) + +func get_help_topic(topic: HelpTopic) -> String: + match(topic): + HelpTopic.INDEX: + return """Command | Description +clear | Clears the console output +exit | Starts a fresh session +shutdown | Shuts Presencode down +help | Displays information about certain topics +config | Reads and writes to the in-memory configuration +pmana | Controls the Presentation Manager +preader | Controls the Presentation Reader +arbitrary | Execute arbitrary GDScript expressions + +To view more information about one topic, type \"help [topic]\".""" + HelpTopic.CLEAR: + return """clear + +Clears the console output.""" + HelpTopic.EXIT: + return """exit + +Clears the console output and starts a fresh session.""" + HelpTopic.SHUTDOWN: + return """shutdown [exitcode: int = 0] + +Shuts Presencode down, accepts a exitcode ranging from 0-255.""" + HelpTopic.HELP: + return """help [topic: String = "INDEX"] + +Displays useful information about commands.""" + HelpTopic.CONFIG: + return """config [get |set |list] + +Returns, lists or overwrites Presencode's configuration. Lives in memory and cannot be saved to persistent storage.""" + HelpTopic.PMANA: + return """pmana [register >|unregister|change_slide [no_animations: bool = false]|clear_viewport|hide_log|show_log] + +Calls functions belonging to the Presentation Manager.""" + HelpTopic.PREADER: + return """preader > + +Calls functions belonging to the Presentation Reader""" + HelpTopic.ARBITRARY: + return """arbitrary + +Executes arbitrary GDScript expressions. +-> EXPERIMENTAL""" + _: + return generate_internal_error("Invalid HelpTopic \"" + str(topic) + "\"") diff --git a/src/loader.gd b/src/loader.gd index 9c53531..9900fc2 100644 --- a/src/loader.gd +++ b/src/loader.gd @@ -124,6 +124,9 @@ ONLY VIEW PRESENCODE PRESENTATIONS IF YOU TRUST THE ################################################### """) await get_tree().create_timer(5).timeout + logger.info("Injecting console") + var console: Control = ResourceLoader.load("res://Console.tscn").instantiate() + get_tree().root.add_child.call_deferred(console) if config_slow_init: await get_tree().create_timer(randf_range(0.1, 0.15)).timeout # Open presentation archive/directory var path: String = "".join(OS.get_cmdline_user_args()) @@ -142,6 +145,7 @@ ONLY VIEW PRESENCODE PRESENTATIONS IF YOU TRUST THE preader.read_manifest() preader.read_entrypoint() # Update window properties + console.calculate_drag_area(preader.get_ratio_resolution()) DisplayServer.window_set_title("Presencode ยป Presenting \"" + preader.get_topic() + "\" by \"" + preader.get_authors() + "\"") DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED) DisplayServer.window_set_size(preader.get_ratio_resolution()) diff --git a/src/misc.gd b/src/misc.gd index ac8e15f..a4550f6 100644 --- a/src/misc.gd +++ b/src/misc.gd @@ -53,7 +53,7 @@ func get_origin(n: int = 0) -> Dictionary: # Shutdown Presencode safely func shutdown(exitcode: int = 0) -> void: - logger.info("Shutting down") + logger.info("Shutting down (code " + str(exitcode) + ")") get_tree().paused = true # Display white texture var npr: NinePatchRect = NinePatchRect.new() diff --git a/src/pmana.gd b/src/pmana.gd index 7131d29..0dd92ca 100644 --- a/src/pmana.gd +++ b/src/pmana.gd @@ -51,6 +51,9 @@ func register(version: int, slides_: int, animations_: bool, quit_last_slide_: b # Check entrypoint version if version != entrypoint_version: await logger.error("Presentation entrypoint version does not match Presencode's entrypoint version") + # Check controller nodepath + if get_node(controller_) == null: + await logger.error("The presentation controller could not be located.") # Set controller information slides = slides_ animations = animations_ @@ -75,17 +78,18 @@ func register(version: int, slides_: int, animations_: bool, quit_last_slide_: b for function in lacking_functions: logger.warn("- " + function) logger.warn("Consult the documentation or take a look at the example presentation if you need help.") - # Create clickoverlay - clickoverlay = Button.new() - clickoverlay.name = "ClickOverlay" - clickoverlay.set_script(ResourceLoader.load("res://src/clickoverlay.gd")) - get_tree().root.add_child(clickoverlay) + await misc.shutdown(1) # Create presentation viewport viewport = Control.new() viewport.name = "Viewport" viewport.size = preader.get_ratio_resolution() viewport.position = Vector2(0, 0) get_node("/root/Presencode").add_child(viewport) + # Create clickoverlay + clickoverlay = Button.new() + clickoverlay.name = "ClickOverlay" + clickoverlay.set_script(ResourceLoader.load("res://src/clickoverlay.gd")) + get_node("/root/Presencode").add_child(clickoverlay) # Did you know that every big galaxy contains a massive black hole in it's center? registered = true # Invoke presentation_start() @@ -103,10 +107,11 @@ func unregister() -> void: animation_active = false config_allow_fullscreen = true # Remove clickoverlay - get_tree().root.remove_child(clickoverlay) + get_node("/root/Presencode/").remove_child(clickoverlay) clickoverlay = null get_node("/root/Presencode/").remove_child(viewport) viewport = null + await get_tree().process_frame # Change slide func change_slide(slide: int, no_animations: bool = false) -> void: @@ -126,8 +131,7 @@ func change_slide(slide: int, no_animations: bool = false) -> void: # Quit after last slide? if quit_last_slide: logger.info("Ending presentation") - await controller.presentation_end() - await misc.shutdown(0) + await shutdown() return else: logger.warn("Over max slides by " + str(slide-slides)) @@ -138,8 +142,7 @@ func change_slide(slide: int, no_animations: bool = false) -> void: return else: # End presentation (slide is over max slides by two or more) logger.info("Ending presentation") - await controller.presentation_end() - await misc.shutdown(0) + await shutdown() return # Slide is not over max slides, continue slide change # Play switch away animation @@ -186,6 +189,12 @@ func show_log() -> void: return get_node("/root/Presencode/Log").modulate = Color8(255, 255, 255, 255) +# Helper function. Shutdown Presencode +func shutdown(exitcode: int = 0) -> void: + logger.diag("Shutting down Presencode from Presentation Manager") + if registered: await controller.presentation_end() + await misc.shutdown(exitcode) + # Runs every frame (duh) func _process(_delta: float) -> void: # Fullscreen key combo diff --git a/src/preader.gd b/src/preader.gd index e485ae6..ebdfa5d 100644 --- a/src/preader.gd +++ b/src/preader.gd @@ -131,6 +131,7 @@ func read_entrypoint() -> misc.Error: # Check if manifest is loaded in memory if manifest == {}: logger.error("Manifest not loaded in memory, please call read_manifest() first") + return misc.Error.PREADER_NO_MANIFEST # Create empty script variable var script: Script if directorypath == "": @@ -169,21 +170,50 @@ func get_entrypoint() -> Node: # Return presentation topic func get_topic() -> String: + if !is_open: + logger.error("No presentation is currently opened") + return "" + # Check if manifest is loaded in memory + if manifest == {}: + logger.error("Manifest not loaded in memory, please call read_manifest() first") + return "" return manifest["topic"] # Return presentation authors func get_authors() -> String: + if !is_open: + logger.error("No presentation is currently opened") + return "" + # Check if manifest is loaded in memory + if manifest == {}: + logger.error("Manifest not loaded in memory, please call read_manifest() first") + return "" var authors: String = "" for author in manifest["authors"]: - authors = authors + ", " + author + if authors == "": authors = author + else: authors = authors + ", " + author return authors # Return display ratio func get_ratio() -> String: + if !is_open: + logger.error("No presentation is currently opened") + return "" + # Check if manifest is loaded in memory + if manifest == {}: + logger.error("Manifest not loaded in memory, please call read_manifest() first") + return "" return manifest["ratio"] # Return display resolution from display ratio func get_ratio_resolution() -> Vector2i: + if !is_open: + logger.error("No presentation is currently opened") + return Vector2i(0, 0) + # Check if manifest is loaded in memory + if manifest == {}: + logger.error("Manifest not loaded in memory, please call read_manifest() first") + return Vector2i(0, 0) match(manifest["ratio"]): "16:9": return Vector2i(1920, 1080) "4:3": return Vector2i(1920, 1440)