From c237837b0b71e747bc6e19f687edfa5590eab74e Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Sat, 4 Feb 2023 09:24:15 -0500 Subject: [PATCH 01/49] Remove GUT and custom Docs plugin --- addons/gut/GutScene.gd | 432 ----- addons/gut/GutScene.tscn | 471 ----- addons/gut/LICENSE.md | 22 - addons/gut/UserFileViewer.gd | 55 - addons/gut/UserFileViewer.tscn | 127 -- addons/gut/autofree.gd | 59 - addons/gut/comparator.gd | 130 -- addons/gut/compare_result.gd | 47 - addons/gut/diff_formatter.gd | 64 - addons/gut/diff_tool.gd | 162 -- .../double_templates/function_template.txt | 6 - .../gut/double_templates/script_template.txt | 41 - addons/gut/doubler.gd | 556 ------ addons/gut/fonts/AnonymousPro-Bold.ttf | Bin 107624 -> 0 bytes addons/gut/fonts/AnonymousPro-BoldItalic.ttf | Bin 95872 -> 0 bytes addons/gut/fonts/AnonymousPro-Italic.ttf | Bin 98424 -> 0 bytes addons/gut/fonts/AnonymousPro-Regular.ttf | Bin 112072 -> 0 bytes addons/gut/fonts/CourierPrime-Bold.ttf | Bin 69768 -> 0 bytes addons/gut/fonts/CourierPrime-BoldItalic.ttf | Bin 77384 -> 0 bytes addons/gut/fonts/CourierPrime-Italic.ttf | Bin 76496 -> 0 bytes addons/gut/fonts/CourierPrime-Regular.ttf | Bin 68128 -> 0 bytes addons/gut/fonts/LobsterTwo-Bold.ttf | Bin 222040 -> 0 bytes addons/gut/fonts/LobsterTwo-BoldItalic.ttf | Bin 216036 -> 0 bytes addons/gut/fonts/LobsterTwo-Italic.ttf | Bin 229404 -> 0 bytes addons/gut/fonts/LobsterTwo-Regular.ttf | Bin 234956 -> 0 bytes addons/gut/fonts/OFL.txt | 94 - addons/gut/gut.gd | 1566 ----------------- addons/gut/gut_cmdln.gd | 402 ----- addons/gut/gut_plugin.gd | 12 - addons/gut/hook_script.gd | 35 - addons/gut/icon.png | Bin 129 -> 0 bytes addons/gut/logger.gd | 348 ---- addons/gut/method_maker.gd | 213 --- addons/gut/one_to_many.gd | 38 - addons/gut/optparse.gd | 248 --- addons/gut/orphan_counter.gd | 55 - addons/gut/parameter_factory.gd | 75 - addons/gut/parameter_handler.gd | 37 - addons/gut/plugin.cfg | 7 - addons/gut/plugin_control.gd | 247 --- addons/gut/printers.gd | 157 -- addons/gut/signal_watcher.gd | 166 -- addons/gut/source_code_pro.fnt | Bin 26499 -> 0 bytes addons/gut/spy.gd | 108 -- addons/gut/strutils.gd | 166 -- addons/gut/stub_params.gd | 43 - addons/gut/stubber.gd | 163 -- addons/gut/summary.gd | 171 -- addons/gut/test.gd | 1566 ----------------- addons/gut/test_collector.gd | 286 --- addons/gut/thing_counter.gd | 43 - addons/gut/utils.gd | 344 ---- .../class_doc_generator.gd | 342 ---- .../doc_exporter/doc_exporter.gd | 13 - .../doc_exporter/editor_help_doc_exporter.gd | 1046 ----------- .../doc_item/argument_doc_item.gd | 27 - .../doc_item/class_doc_item.gd | 77 - .../doc_item/constant_doc_item.gd | 27 - .../doc_item/doc_item.gd | 13 - .../doc_item/method_doc_item.gd | 33 - .../doc_item/property_doc_item.gd | 39 - .../doc_item/signal_doc_item.gd | 19 - addons/silicon.util.custom_docs/plugin.cfg | 7 - addons/silicon.util.custom_docs/plugin.gd | 630 ------- 64 files changed, 11035 deletions(-) delete mode 100644 addons/gut/GutScene.gd delete mode 100644 addons/gut/GutScene.tscn delete mode 100644 addons/gut/LICENSE.md delete mode 100644 addons/gut/UserFileViewer.gd delete mode 100644 addons/gut/UserFileViewer.tscn delete mode 100644 addons/gut/autofree.gd delete mode 100644 addons/gut/comparator.gd delete mode 100644 addons/gut/compare_result.gd delete mode 100644 addons/gut/diff_formatter.gd delete mode 100644 addons/gut/diff_tool.gd delete mode 100644 addons/gut/double_templates/function_template.txt delete mode 100644 addons/gut/double_templates/script_template.txt delete mode 100644 addons/gut/doubler.gd delete mode 100644 addons/gut/fonts/AnonymousPro-Bold.ttf delete mode 100644 addons/gut/fonts/AnonymousPro-BoldItalic.ttf delete mode 100644 addons/gut/fonts/AnonymousPro-Italic.ttf delete mode 100644 addons/gut/fonts/AnonymousPro-Regular.ttf delete mode 100644 addons/gut/fonts/CourierPrime-Bold.ttf delete mode 100644 addons/gut/fonts/CourierPrime-BoldItalic.ttf delete mode 100644 addons/gut/fonts/CourierPrime-Italic.ttf delete mode 100644 addons/gut/fonts/CourierPrime-Regular.ttf delete mode 100644 addons/gut/fonts/LobsterTwo-Bold.ttf delete mode 100644 addons/gut/fonts/LobsterTwo-BoldItalic.ttf delete mode 100644 addons/gut/fonts/LobsterTwo-Italic.ttf delete mode 100644 addons/gut/fonts/LobsterTwo-Regular.ttf delete mode 100644 addons/gut/fonts/OFL.txt delete mode 100644 addons/gut/gut.gd delete mode 100644 addons/gut/gut_cmdln.gd delete mode 100644 addons/gut/gut_plugin.gd delete mode 100644 addons/gut/hook_script.gd delete mode 100644 addons/gut/icon.png delete mode 100644 addons/gut/logger.gd delete mode 100644 addons/gut/method_maker.gd delete mode 100644 addons/gut/one_to_many.gd delete mode 100644 addons/gut/optparse.gd delete mode 100644 addons/gut/orphan_counter.gd delete mode 100644 addons/gut/parameter_factory.gd delete mode 100644 addons/gut/parameter_handler.gd delete mode 100644 addons/gut/plugin.cfg delete mode 100644 addons/gut/plugin_control.gd delete mode 100644 addons/gut/printers.gd delete mode 100644 addons/gut/signal_watcher.gd delete mode 100644 addons/gut/source_code_pro.fnt delete mode 100644 addons/gut/spy.gd delete mode 100644 addons/gut/strutils.gd delete mode 100644 addons/gut/stub_params.gd delete mode 100644 addons/gut/stubber.gd delete mode 100644 addons/gut/summary.gd delete mode 100644 addons/gut/test.gd delete mode 100644 addons/gut/test_collector.gd delete mode 100644 addons/gut/thing_counter.gd delete mode 100644 addons/gut/utils.gd delete mode 100644 addons/silicon.util.custom_docs/class_doc_generator.gd delete mode 100644 addons/silicon.util.custom_docs/doc_exporter/doc_exporter.gd delete mode 100644 addons/silicon.util.custom_docs/doc_exporter/editor_help_doc_exporter.gd delete mode 100644 addons/silicon.util.custom_docs/doc_item/argument_doc_item.gd delete mode 100644 addons/silicon.util.custom_docs/doc_item/class_doc_item.gd delete mode 100644 addons/silicon.util.custom_docs/doc_item/constant_doc_item.gd delete mode 100644 addons/silicon.util.custom_docs/doc_item/doc_item.gd delete mode 100644 addons/silicon.util.custom_docs/doc_item/method_doc_item.gd delete mode 100644 addons/silicon.util.custom_docs/doc_item/property_doc_item.gd delete mode 100644 addons/silicon.util.custom_docs/doc_item/signal_doc_item.gd delete mode 100644 addons/silicon.util.custom_docs/plugin.cfg delete mode 100644 addons/silicon.util.custom_docs/plugin.gd diff --git a/addons/gut/GutScene.gd b/addons/gut/GutScene.gd deleted file mode 100644 index 9894c9c..0000000 --- a/addons/gut/GutScene.gd +++ /dev/null @@ -1,432 +0,0 @@ -extends Panel - -onready var _script_list = $ScriptsList -onready var _nav = { - prev = $Navigation/Previous, - next = $Navigation/Next, - run = $Navigation/Run, - current_script = $Navigation/CurrentScript, - run_single = $Navigation/RunSingleScript -} -onready var _progress = { - script = $ScriptProgress, - script_xy = $ScriptProgress/xy, - test = $TestProgress, - test_xy = $TestProgress/xy -} -onready var _summary = { - failing = $Summary/Failing, - passing = $Summary/Passing, - fail_count = 0, - pass_count = 0 -} - -onready var _extras = $ExtraOptions -onready var _ignore_pauses = $ExtraOptions/IgnorePause -onready var _continue_button = $Continue/Continue -onready var _text_box = $TextDisplay/RichTextLabel - -onready var _titlebar = { - bar = $TitleBar, - time = $TitleBar/Time, - label = $TitleBar/Title -} - -onready var _user_files = $UserFileViewer - -var _mouse = { - down = false, - in_title = false, - down_pos = null, - in_handle = false -} -var _is_running = false -var _start_time = 0.0 -var _time = 0.0 - -const DEFAULT_TITLE = 'Gut: The Godot Unit Testing tool.' -var _pre_maximize_rect = null -var _font_size = 20 - -signal end_pause -signal ignore_pause -signal log_level_changed -signal run_script -signal run_single_script - -func _ready(): - - if(Engine.editor_hint): - return - - _pre_maximize_rect = get_rect() - _hide_scripts() - _update_controls() - _nav.current_script.set_text("No scripts available") - set_title() - clear_summary() - _titlebar.time.set_text("Time 0.0") - - _extras.visible = false - update() - - set_font_size(_font_size) - set_font('CourierPrime') - - _user_files.set_position(Vector2(10, 30)) - -func elapsed_time_as_str(): - return str("%.1f" % (_time / 1000.0), 's') - -func _process(_delta): - if(_is_running): - _time = OS.get_ticks_msec() - _start_time - _titlebar.time.set_text(str('Time: ', elapsed_time_as_str())) - -func _draw(): # needs get_size() - # Draw the lines in the corner to show where you can - # drag to resize the dialog - var grab_margin = 3 - var line_space = 3 - var grab_line_color = Color(.4, .4, .4) - for i in range(1, 10): - var x = rect_size - Vector2(i * line_space, grab_margin) - var y = rect_size - Vector2(grab_margin, i * line_space) - draw_line(x, y, grab_line_color, 1, true) - -func _on_Maximize_draw(): - # draw the maximize square thing. - var btn = $TitleBar/Maximize - btn.set_text('') - var w = btn.get_size().x - var h = btn.get_size().y - btn.draw_rect(Rect2(0, 0, w, h), Color(0, 0, 0, 1)) - btn.draw_rect(Rect2(2, 4, w - 4, h - 6), Color(1,1,1,1)) - -func _on_ShowExtras_draw(): - var btn = $Continue/ShowExtras - btn.set_text('') - var start_x = 20 - var start_y = 15 - var pad = 5 - var color = Color(.1, .1, .1, 1) - var width = 2 - for i in range(3): - var y = start_y + pad * i - btn.draw_line(Vector2(start_x, y), Vector2(btn.get_size().x - start_x, y), color, width, true) - -# #################### -# GUI Events -# #################### -func _on_Run_pressed(): - _run_mode() - emit_signal('run_script', get_selected_index()) - -func _on_CurrentScript_pressed(): - _toggle_scripts() - -func _on_Previous_pressed(): - _select_script(get_selected_index() - 1) - -func _on_Next_pressed(): - _select_script(get_selected_index() + 1) - -func _on_LogLevelSlider_value_changed(_value): - emit_signal('log_level_changed', $LogLevelSlider.value) - -func _on_Continue_pressed(): - _continue_button.disabled = true - emit_signal('end_pause') - -func _on_IgnorePause_pressed(): - var checked = _ignore_pauses.is_pressed() - emit_signal('ignore_pause', checked) - if(checked): - emit_signal('end_pause') - _continue_button.disabled = true - -func _on_RunSingleScript_pressed(): - _run_mode() - emit_signal('run_single_script', get_selected_index()) - -func _on_ScriptsList_item_selected(index): - var tmr = $ScriptsList/DoubleClickTimer - if(!tmr.is_stopped()): - _run_mode() - emit_signal('run_single_script', get_selected_index()) - tmr.stop() - else: - tmr.start() - - _select_script(index) - -func _on_TitleBar_mouse_entered(): - _mouse.in_title = true - -func _on_TitleBar_mouse_exited(): - _mouse.in_title = false - -func _input(event): - if(event is InputEventMouseButton): - if(event.button_index == 1): - _mouse.down = event.pressed - if(_mouse.down): - _mouse.down_pos = event.position - - if(_mouse.in_title): - if(event is InputEventMouseMotion and _mouse.down): - set_position(get_position() + (event.position - _mouse.down_pos)) - _mouse.down_pos = event.position - _pre_maximize_rect = get_rect() - - if(_mouse.in_handle): - if(event is InputEventMouseMotion and _mouse.down): - var new_size = rect_size + event.position - _mouse.down_pos - var new_mouse_down_pos = event.position - rect_size = new_size - _mouse.down_pos = new_mouse_down_pos - _pre_maximize_rect = get_rect() - -func _on_ResizeHandle_mouse_entered(): - _mouse.in_handle = true - -func _on_ResizeHandle_mouse_exited(): - _mouse.in_handle = false - -func _on_RichTextLabel_gui_input(ev): - pass - # leaving this b/c it is wired up and might have to send - # more signals through - -func _on_Copy_pressed(): - OS.clipboard = _text_box.text - -func _on_ShowExtras_toggled(button_pressed): - _extras.visible = button_pressed - -func _on_Maximize_pressed(): - if(get_rect() == _pre_maximize_rect): - maximize() - else: - rect_size = _pre_maximize_rect.size - rect_position = _pre_maximize_rect.position -# #################### -# Private -# #################### -func _run_mode(is_running=true): - if(is_running): - _start_time = OS.get_ticks_msec() - _time = 0.0 - clear_summary() - _is_running = is_running - - _hide_scripts() - var ctrls = $Navigation.get_children() - for i in range(ctrls.size()): - ctrls[i].disabled = is_running - -func _select_script(index): - var text = _script_list.get_item_text(index) - var max_len = 50 - if(text.length() > max_len): - text = '...' + text.right(text.length() - (max_len - 5)) - $Navigation/CurrentScript.set_text(text) - _script_list.select(index) - _update_controls() - -func _toggle_scripts(): - if(_script_list.visible): - _hide_scripts() - else: - _show_scripts() - -func _show_scripts(): - _script_list.show() - -func _hide_scripts(): - _script_list.hide() - -func _update_controls(): - var is_empty = _script_list.get_selected_items().size() == 0 - if(is_empty): - _nav.next.disabled = true - _nav.prev.disabled = true - else: - var index = get_selected_index() - _nav.prev.disabled = index <= 0 - _nav.next.disabled = index >= _script_list.get_item_count() - 1 - - _nav.run.disabled = is_empty - _nav.current_script.disabled = is_empty - _nav.run_single.disabled = is_empty - -func _update_summary(): - if(!_summary): - return - - var total = _summary.fail_count + _summary.pass_count - $Summary.visible = !total == 0 - $Summary/AssertCount.text = str('Failures ', _summary.fail_count, '/', total) -# #################### -# Public -# #################### -func run_mode(is_running=true): - _run_mode(is_running) - -func set_scripts(scripts): - _script_list.clear() - for i in range(scripts.size()): - _script_list.add_item(scripts[i]) - _select_script(0) - _update_controls() - -func select_script(index): - _select_script(index) - -func get_selected_index(): - return _script_list.get_selected_items()[0] - -func get_log_level(): - return $LogLevelSlider.value - -func set_log_level(value): - var new_value = value - if(new_value == null): - new_value = 0 - $LogLevelSlider.value = new_value - -func set_ignore_pause(should): - _ignore_pauses.pressed = should - -func get_ignore_pause(): - return _ignore_pauses.pressed - -func get_text_box(): - # due to some timing issue, this cannot return _text_box but can return - # this. - return $TextDisplay/RichTextLabel - -func end_run(): - _run_mode(false) - _update_controls() - -func set_progress_script_max(value): - var max_val = max(value, 1) - _progress.script.set_max(max_val) - _progress.script_xy.set_text(str('0/', max_val)) - -func set_progress_script_value(value): - _progress.script.set_value(value) - var txt = str(value, '/', _progress.test.get_max()) - _progress.script_xy.set_text(txt) - -func set_progress_test_max(value): - var max_val = max(value, 1) - _progress.test.set_max(max_val) - _progress.test_xy.set_text(str('0/', max_val)) - -func set_progress_test_value(value): - _progress.test.set_value(value) - var txt = str(value, '/', _progress.test.get_max()) - _progress.test_xy.set_text(txt) - -func clear_progress(): - _progress.test.set_value(0) - _progress.script.set_value(0) - -func pause(): - _continue_button.disabled = false - -func set_title(title=null): - if(title == null): - $TitleBar/Title.set_text(DEFAULT_TITLE) - else: - $TitleBar/Title.set_text(title) - -func add_passing(amount=1): - if(!_summary): - return - _summary.pass_count += amount - _update_summary() - -func add_failing(amount=1): - if(!_summary): - return - _summary.fail_count += amount - _update_summary() - -func clear_summary(): - _summary.fail_count = 0 - _summary.pass_count = 0 - _update_summary() - -func maximize(): - if(is_inside_tree()): - var vp_size_offset = get_viewport().size - rect_size = vp_size_offset / get_scale() - set_position(Vector2(0, 0)) - -func clear_text(): - _text_box.bbcode_text = '' - -func scroll_to_bottom(): - pass - #_text_box.cursor_set_line(_gui.get_text_box().get_line_count()) - -func _set_font_size_for_rtl(rtl, new_size): - if(rtl.get('custom_fonts/normal_font') != null): - rtl.get('custom_fonts/bold_italics_font').size = new_size - rtl.get('custom_fonts/bold_font').size = new_size - rtl.get('custom_fonts/italics_font').size = new_size - rtl.get('custom_fonts/normal_font').size = new_size - - -func _set_fonts_for_rtl(rtl, base_font_name): - pass - - -func set_font_size(new_size): - _font_size = new_size - _set_font_size_for_rtl(_text_box, new_size) - _set_font_size_for_rtl(_user_files.get_rich_text_label(), new_size) - - -func _set_font(rtl, font_name, custom_name): - if(font_name == null): - rtl.set('custom_fonts/' + custom_name, null) - else: - var dyn_font = DynamicFont.new() - var font_data = DynamicFontData.new() - font_data.font_path = 'res://addons/gut/fonts/' + font_name + '.ttf' - font_data.antialiased = true - dyn_font.font_data = font_data - rtl.set('custom_fonts/' + custom_name, dyn_font) - -func _set_all_fonts_in_ftl(ftl, base_name): - if(base_name == 'Default'): - _set_font(ftl, null, 'normal_font') - _set_font(ftl, null, 'bold_font') - _set_font(ftl, null, 'italics_font') - _set_font(ftl, null, 'bold_italics_font') - else: - _set_font(ftl, base_name + '-Regular', 'normal_font') - _set_font(ftl, base_name + '-Bold', 'bold_font') - _set_font(ftl, base_name + '-Italic', 'italics_font') - _set_font(ftl, base_name + '-BoldItalic', 'bold_italics_font') - set_font_size(_font_size) - -func set_font(base_name): - _set_all_fonts_in_ftl(_text_box, base_name) - _set_all_fonts_in_ftl(_user_files.get_rich_text_label(), base_name) - -func set_default_font_color(color): - _text_box.set('custom_colors/default_color', color) - -func set_background_color(color): - $TextDisplay.color = color - -func _on_UserFiles_pressed(): - _user_files.show_open() - -func get_waiting_label(): - return $TextDisplay/WaitingLabel diff --git a/addons/gut/GutScene.tscn b/addons/gut/GutScene.tscn deleted file mode 100644 index 3819a6e..0000000 --- a/addons/gut/GutScene.tscn +++ /dev/null @@ -1,471 +0,0 @@ -[gd_scene load_steps=15 format=2] - -[ext_resource path="res://addons/gut/GutScene.gd" type="Script" id=1] -[ext_resource path="res://addons/gut/fonts/AnonymousPro-Italic.ttf" type="DynamicFontData" id=2] -[ext_resource path="res://addons/gut/fonts/AnonymousPro-Regular.ttf" type="DynamicFontData" id=3] -[ext_resource path="res://addons/gut/fonts/AnonymousPro-BoldItalic.ttf" type="DynamicFontData" id=4] -[ext_resource path="res://addons/gut/fonts/AnonymousPro-Bold.ttf" type="DynamicFontData" id=5] -[ext_resource path="res://addons/gut/UserFileViewer.tscn" type="PackedScene" id=6] - -[sub_resource type="StyleBoxFlat" id=1] -bg_color = Color( 0.192157, 0.192157, 0.227451, 1 ) -corner_radius_top_left = 10 -corner_radius_top_right = 10 - -[sub_resource type="StyleBoxFlat" id=2] -bg_color = Color( 1, 1, 1, 1 ) -border_color = Color( 0, 0, 0, 1 ) -corner_radius_top_left = 5 -corner_radius_top_right = 5 - -[sub_resource type="Theme" id=3] -resource_local_to_scene = true -Panel/styles/panel = SubResource( 2 ) -Panel/styles/panelf = null -Panel/styles/panelnc = null - -[sub_resource type="DynamicFont" id=4] -font_data = ExtResource( 4 ) - -[sub_resource type="DynamicFont" id=5] -font_data = ExtResource( 2 ) - -[sub_resource type="DynamicFont" id=6] -font_data = ExtResource( 5 ) - -[sub_resource type="DynamicFont" id=7] -font_data = ExtResource( 3 ) - -[sub_resource type="StyleBoxFlat" id=8] -bg_color = Color( 0.192157, 0.192157, 0.227451, 1 ) -corner_radius_top_left = 20 -corner_radius_top_right = 20 - -[node name="Gut" type="Panel"] -margin_right = 880.0 -margin_bottom = 360.0 -rect_min_size = Vector2( 740, 250 ) -custom_styles/panel = SubResource( 1 ) -script = ExtResource( 1 ) -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="UserFileViewer" parent="." instance=ExtResource( 6 )] -margin_top = 388.0 -margin_bottom = 818.0 - -[node name="TitleBar" type="Panel" parent="."] -anchor_top = -0.000491047 -anchor_right = 1.0 -anchor_bottom = -0.000491047 -margin_left = 1.0 -margin_top = 1.17678 -margin_right = -1.0 -margin_bottom = 40.1768 -theme = SubResource( 3 ) -__meta__ = { -"_edit_group_": true, -"_edit_use_anchors_": false -} - -[node name="Title" type="Label" parent="TitleBar"] -anchor_right = 1.0 -margin_bottom = 40.0 -custom_colors/font_color = Color( 0, 0, 0, 1 ) -text = "Gut" -align = 1 -valign = 1 - -[node name="Time" type="Label" parent="TitleBar"] -anchor_left = 1.0 -anchor_right = 1.0 -margin_left = -105.0 -margin_right = -53.0 -margin_bottom = 40.0 -custom_colors/font_color = Color( 0, 0, 0, 1 ) -text = "9999.99" -valign = 1 - -[node name="Maximize" type="Button" parent="TitleBar"] -anchor_left = 1.0 -anchor_right = 1.0 -margin_left = -30.0 -margin_top = 10.0 -margin_right = -6.0 -margin_bottom = 30.0 -custom_colors/font_color = Color( 0, 0, 0, 1 ) -text = "M" -flat = true - -[node name="ScriptProgress" type="ProgressBar" parent="."] -anchor_top = 1.0 -anchor_bottom = 1.0 -margin_left = 75.0 -margin_top = -70.0 -margin_right = 185.0 -margin_bottom = -40.0 -hint_tooltip = "Overall progress of executing tests." -step = 1.0 -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="Label" type="Label" parent="ScriptProgress"] -margin_left = -70.0 -margin_right = -5.0 -margin_bottom = 30.0 -text = "Scripts" -valign = 1 -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="xy" type="Label" parent="ScriptProgress"] -visible = false -margin_right = 110.0 -margin_bottom = 30.0 -text = "0/0" -align = 1 -valign = 1 -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="TestProgress" type="ProgressBar" parent="."] -anchor_top = 1.0 -anchor_bottom = 1.0 -margin_left = 75.0 -margin_top = -105.0 -margin_right = 185.0 -margin_bottom = -75.0 -hint_tooltip = "Test progress for the current script." -step = 1.0 -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="Label" type="Label" parent="TestProgress"] -margin_left = -70.0 -margin_right = -5.0 -margin_bottom = 30.0 -text = "Tests" -valign = 1 -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="xy" type="Label" parent="TestProgress"] -visible = false -margin_right = 110.0 -margin_bottom = 30.0 -text = "0/0" -align = 1 -valign = 1 -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="TextDisplay" type="ColorRect" parent="."] -anchor_right = 1.0 -anchor_bottom = 1.0 -margin_top = 40.0 -margin_bottom = -110.0 -color = Color( 0, 0, 0, 1 ) -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="RichTextLabel" type="RichTextLabel" parent="TextDisplay"] -anchor_right = 1.0 -anchor_bottom = 1.0 -margin_left = 10.0 -focus_mode = 2 -custom_fonts/bold_italics_font = SubResource( 4 ) -custom_fonts/italics_font = SubResource( 5 ) -custom_fonts/bold_font = SubResource( 6 ) -custom_fonts/normal_font = SubResource( 7 ) -bbcode_enabled = true -scroll_following = true -selection_enabled = true -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="WaitingLabel" type="RichTextLabel" parent="TextDisplay"] -anchor_top = 1.0 -anchor_right = 1.0 -anchor_bottom = 1.0 -margin_top = -25.0 -bbcode_enabled = true -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="Navigation" type="Panel" parent="."] -self_modulate = Color( 1, 1, 1, 0 ) -anchor_top = 1.0 -anchor_bottom = 1.0 -margin_left = 220.0 -margin_top = -99.0 -margin_right = 580.0 -margin_bottom = 1.0 -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="Previous" type="Button" parent="Navigation"] -margin_left = -20.0 -margin_top = 44.0 -margin_right = 65.0 -margin_bottom = 84.0 -hint_tooltip = "Previous script in the list." -text = "|<" -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="Next" type="Button" parent="Navigation"] -margin_left = 250.0 -margin_top = 44.0 -margin_right = 335.0 -margin_bottom = 84.0 -hint_tooltip = "Next script in the list. -" -text = ">|" -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="Run" type="Button" parent="Navigation"] -margin_left = 70.0 -margin_top = 44.0 -margin_right = 155.0 -margin_bottom = 84.0 -hint_tooltip = "Run the currently selected item and all after it." -text = ">" -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="CurrentScript" type="Button" parent="Navigation"] -anchor_top = -0.01 -anchor_bottom = -0.01 -margin_left = -20.0 -margin_top = -5.0 -margin_right = 335.0 -margin_bottom = 35.0 -hint_tooltip = "Select a script to run. You can run just this script, or this script and all scripts after using the run buttons." -text = "res://test/unit/test_gut.gd" -clip_text = true -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="RunSingleScript" type="Button" parent="Navigation"] -margin_left = 160.0 -margin_top = 44.0 -margin_right = 245.0 -margin_bottom = 84.0 -hint_tooltip = "Run the currently selected item. - -If the selected item has Inner Test Classes -then they will all be run. If the selected item -is an Inner Test Class then only it will be run." -text = "> (1)" -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="LogLevelSlider" type="HSlider" parent="."] -anchor_top = 1.0 -anchor_bottom = 1.0 -margin_left = 80.0 -margin_top = -40.0 -margin_right = 130.0 -margin_bottom = -20.0 -rect_scale = Vector2( 2, 2 ) -max_value = 2.0 -tick_count = 3 -ticks_on_borders = true -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="Label" type="Label" parent="LogLevelSlider"] -margin_left = -37.0 -margin_right = 28.0 -margin_bottom = 40.0 -rect_scale = Vector2( 0.5, 0.5 ) -text = "Log Level" -valign = 1 -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="ScriptsList" type="ItemList" parent="."] -anchor_bottom = 1.0 -margin_left = 179.0 -margin_top = 40.0 -margin_right = 619.0 -margin_bottom = -110.0 -allow_reselect = true -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="DoubleClickTimer" type="Timer" parent="ScriptsList"] -wait_time = 0.3 -one_shot = true - -[node name="ExtraOptions" type="Panel" parent="."] -anchor_left = 1.0 -anchor_top = 1.0 -anchor_right = 1.0 -anchor_bottom = 1.0 -margin_left = -212.0 -margin_top = -260.0 -margin_right = -2.0 -margin_bottom = -106.0 -custom_styles/panel = SubResource( 8 ) -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="IgnorePause" type="CheckBox" parent="ExtraOptions"] -margin_left = 18.0 -margin_right = 136.0 -margin_bottom = 24.0 -rect_scale = Vector2( 1.5, 1.5 ) -hint_tooltip = "Ignore all calls to pause_before_teardown." -text = "Ignore Pauses" -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="Copy" type="Button" parent="ExtraOptions"] -margin_left = 15.0 -margin_top = 40.0 -margin_right = 195.0 -margin_bottom = 80.0 -hint_tooltip = "Copy all output to the clipboard." -text = "Copy to Clipboard" -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="UserFiles" type="Button" parent="ExtraOptions"] -margin_left = 15.0 -margin_top = 90.0 -margin_right = 195.0 -margin_bottom = 130.0 -hint_tooltip = "Copy all output to the clipboard." -text = "View User Files" -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="ResizeHandle" type="Control" parent="."] -anchor_left = 1.0 -anchor_top = 1.0 -anchor_right = 1.0 -anchor_bottom = 1.0 -margin_left = -40.0 -margin_top = -40.0 - -[node name="Continue" type="Panel" parent="."] -self_modulate = Color( 1, 1, 1, 0 ) -anchor_left = 1.0 -anchor_top = 1.0 -anchor_right = 1.0 -anchor_bottom = 1.0 -margin_left = -150.0 -margin_top = -100.0 -margin_right = -30.0 -margin_bottom = -10.0 - -[node name="Continue" type="Button" parent="Continue"] -margin_left = -2.0 -margin_top = 45.0 -margin_right = 117.0 -margin_bottom = 85.0 -hint_tooltip = "When a pause_before_teardown is encountered this button will be enabled and must be pressed to continue running tests." -disabled = true -text = "Continue" -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="ShowExtras" type="Button" parent="Continue"] -anchor_left = -0.0166667 -anchor_right = -0.0166667 -margin_left = 50.0 -margin_top = -5.0 -margin_right = 120.0 -margin_bottom = 35.0 -rect_pivot_offset = Vector2( 35, 20 ) -hint_tooltip = "Show/hide additional options." -toggle_mode = true -text = "_" -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="Summary" type="Node2D" parent="."] -position = Vector2( 0, 3 ) - -[node name="Passing" type="Label" parent="Summary"] -visible = false -margin_left = 5.0 -margin_top = 7.0 -margin_right = 45.0 -margin_bottom = 21.0 -custom_colors/font_color = Color( 0, 0, 0, 1 ) -text = "0" -align = 1 -valign = 1 -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="Failing" type="Label" parent="Summary"] -visible = false -margin_left = 100.0 -margin_top = 7.0 -margin_right = 140.0 -margin_bottom = 21.0 -custom_colors/font_color = Color( 0, 0, 0, 1 ) -text = "0" -align = 1 -valign = 1 - -[node name="AssertCount" type="Label" parent="Summary"] -margin_left = 5.0 -margin_top = 7.0 -margin_right = 165.0 -margin_bottom = 21.0 -custom_colors/font_color = Color( 0, 0, 0, 1 ) -text = "Assert count" -__meta__ = { -"_edit_use_anchors_": false -} -[connection signal="mouse_entered" from="TitleBar" to="." method="_on_TitleBar_mouse_entered"] -[connection signal="mouse_exited" from="TitleBar" to="." method="_on_TitleBar_mouse_exited"] -[connection signal="draw" from="TitleBar/Maximize" to="." method="_on_Maximize_draw"] -[connection signal="pressed" from="TitleBar/Maximize" to="." method="_on_Maximize_pressed"] -[connection signal="gui_input" from="TextDisplay/RichTextLabel" to="." method="_on_RichTextLabel_gui_input"] -[connection signal="pressed" from="Navigation/Previous" to="." method="_on_Previous_pressed"] -[connection signal="pressed" from="Navigation/Next" to="." method="_on_Next_pressed"] -[connection signal="pressed" from="Navigation/Run" to="." method="_on_Run_pressed"] -[connection signal="pressed" from="Navigation/CurrentScript" to="." method="_on_CurrentScript_pressed"] -[connection signal="pressed" from="Navigation/RunSingleScript" to="." method="_on_RunSingleScript_pressed"] -[connection signal="value_changed" from="LogLevelSlider" to="." method="_on_LogLevelSlider_value_changed"] -[connection signal="item_selected" from="ScriptsList" to="." method="_on_ScriptsList_item_selected"] -[connection signal="pressed" from="ExtraOptions/IgnorePause" to="." method="_on_IgnorePause_pressed"] -[connection signal="pressed" from="ExtraOptions/Copy" to="." method="_on_Copy_pressed"] -[connection signal="pressed" from="ExtraOptions/UserFiles" to="." method="_on_UserFiles_pressed"] -[connection signal="mouse_entered" from="ResizeHandle" to="." method="_on_ResizeHandle_mouse_entered"] -[connection signal="mouse_exited" from="ResizeHandle" to="." method="_on_ResizeHandle_mouse_exited"] -[connection signal="pressed" from="Continue/Continue" to="." method="_on_Continue_pressed"] -[connection signal="draw" from="Continue/ShowExtras" to="." method="_on_ShowExtras_draw"] -[connection signal="toggled" from="Continue/ShowExtras" to="." method="_on_ShowExtras_toggled"] diff --git a/addons/gut/LICENSE.md b/addons/gut/LICENSE.md deleted file mode 100644 index a38ac23..0000000 --- a/addons/gut/LICENSE.md +++ /dev/null @@ -1,22 +0,0 @@ -The MIT License (MIT) -===================== - -Copyright (c) 2018 Tom "Butch" Wesley - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/addons/gut/UserFileViewer.gd b/addons/gut/UserFileViewer.gd deleted file mode 100644 index 9713a94..0000000 --- a/addons/gut/UserFileViewer.gd +++ /dev/null @@ -1,55 +0,0 @@ -extends WindowDialog - -onready var rtl = $TextDisplay/RichTextLabel -var _has_opened_file = false - -func _get_file_as_text(path): - var to_return = null - var f = File.new() - var result = f.open(path, f.READ) - if(result == OK): - to_return = f.get_as_text() - f.close() - else: - to_return = str('ERROR: Could not open file. Error code ', result) - return to_return - -func _ready(): - rtl.clear() - -func _on_OpenFile_pressed(): - $FileDialog.popup_centered() - -func _on_FileDialog_file_selected(path): - show_file(path) - -func _on_Close_pressed(): - self.hide() - -func show_file(path): - var text = _get_file_as_text(path) - if(text == ''): - text = '' - rtl.set_text(text) - self.window_title = path - -func show_open(): - self.popup_centered() - $FileDialog.popup_centered() - -func _on_FileDialog_popup_hide(): - if(rtl.text.length() == 0): - self.hide() - -func get_rich_text_label(): - return $TextDisplay/RichTextLabel - -func _on_Home_pressed(): - rtl.scroll_to_line(0) - -func _on_End_pressed(): - rtl.scroll_to_line(rtl.get_line_count() -1) - - -func _on_Copy_pressed(): - OS.clipboard = rtl.text diff --git a/addons/gut/UserFileViewer.tscn b/addons/gut/UserFileViewer.tscn deleted file mode 100644 index 1236ebb..0000000 --- a/addons/gut/UserFileViewer.tscn +++ /dev/null @@ -1,127 +0,0 @@ -[gd_scene load_steps=2 format=2] - -[ext_resource path="res://addons/gut/UserFileViewer.gd" type="Script" id=1] - -[node name="UserFileViewer" type="WindowDialog"] -margin_top = 20.0 -margin_right = 800.0 -margin_bottom = 450.0 -rect_min_size = Vector2( 800, 180 ) -popup_exclusive = true -window_title = "View File" -resizable = true -script = ExtResource( 1 ) -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="FileDialog" type="FileDialog" parent="."] -margin_right = 416.0 -margin_bottom = 184.0 -rect_min_size = Vector2( 400, 140 ) -rect_scale = Vector2( 2, 2 ) -popup_exclusive = true -window_title = "Open a File" -resizable = true -mode = 0 -access = 1 -show_hidden_files = true -current_dir = "user://" -current_path = "user://" -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="TextDisplay" type="ColorRect" parent="."] -anchor_right = 1.0 -anchor_bottom = 1.0 -margin_left = 8.0 -margin_right = -10.0 -margin_bottom = -65.0 -color = Color( 0.2, 0.188235, 0.188235, 1 ) -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="RichTextLabel" type="RichTextLabel" parent="TextDisplay"] -anchor_right = 1.0 -anchor_bottom = 1.0 -focus_mode = 2 -text = "In publishing and graphic design, Lorem ipsum is a placeholder text commonly used to demonstrate the visual form of a document or a typeface without relying on meaningful content. Lorem ipsum may be used before final copy is available, but it may also be used to temporarily replace copy in a process called greeking, which allows designers to consider form without the meaning of the text influencing the design. - -Lorem ipsum is typically a corrupted version of De finibus bonorum et malorum, a first-century BCE text by the Roman statesman and philosopher Cicero, with words altered, added, and removed to make it nonsensical, improper Latin. - -Versions of the Lorem ipsum text have been used in typesetting at least since the 1960s, when it was popularized by advertisements for Letraset transfer sheets. Lorem ipsum was introduced to the digital world in the mid-1980s when Aldus employed it in graphic and word-processing templates for its desktop publishing program PageMaker. Other popular word processors including Pages and Microsoft Word have since adopted Lorem ipsum as well." -selection_enabled = true -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="OpenFile" type="Button" parent="."] -anchor_left = 1.0 -anchor_top = 1.0 -anchor_right = 1.0 -anchor_bottom = 1.0 -margin_left = -158.0 -margin_top = -50.0 -margin_right = -84.0 -margin_bottom = -30.0 -rect_scale = Vector2( 2, 2 ) -text = "Open File" - -[node name="Home" type="Button" parent="."] -anchor_left = 1.0 -anchor_top = 1.0 -anchor_right = 1.0 -anchor_bottom = 1.0 -margin_left = -478.0 -margin_top = -50.0 -margin_right = -404.0 -margin_bottom = -30.0 -rect_scale = Vector2( 2, 2 ) -text = "Home" -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="Copy" type="Button" parent="."] -anchor_top = 1.0 -anchor_bottom = 1.0 -margin_left = 160.0 -margin_top = -50.0 -margin_right = 234.0 -margin_bottom = -30.0 -rect_scale = Vector2( 2, 2 ) -text = "Copy" -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="End" type="Button" parent="."] -anchor_left = 1.0 -anchor_top = 1.0 -anchor_right = 1.0 -anchor_bottom = 1.0 -margin_left = -318.0 -margin_top = -50.0 -margin_right = -244.0 -margin_bottom = -30.0 -rect_scale = Vector2( 2, 2 ) -text = "End" - -[node name="Close" type="Button" parent="."] -anchor_top = 1.0 -anchor_bottom = 1.0 -margin_left = 10.0 -margin_top = -50.0 -margin_right = 80.0 -margin_bottom = -30.0 -rect_scale = Vector2( 2, 2 ) -text = "Close" -[connection signal="file_selected" from="FileDialog" to="." method="_on_FileDialog_file_selected"] -[connection signal="popup_hide" from="FileDialog" to="." method="_on_FileDialog_popup_hide"] -[connection signal="pressed" from="OpenFile" to="." method="_on_OpenFile_pressed"] -[connection signal="pressed" from="Home" to="." method="_on_Home_pressed"] -[connection signal="pressed" from="Copy" to="." method="_on_Copy_pressed"] -[connection signal="pressed" from="End" to="." method="_on_End_pressed"] -[connection signal="pressed" from="Close" to="." method="_on_Close_pressed"] diff --git a/addons/gut/autofree.gd b/addons/gut/autofree.gd deleted file mode 100644 index 80b4e89..0000000 --- a/addons/gut/autofree.gd +++ /dev/null @@ -1,59 +0,0 @@ -# ############################################################################## -#(G)odot (U)nit (T)est class -# -# ############################################################################## -# The MIT License (MIT) -# ===================== -# -# Copyright (c) 2020 Tom "Butch" Wesley -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ############################################################################## -# Class used to keep track of objects to be freed and utilities to free them. -# ############################################################################## -var _to_free = [] -var _to_queue_free = [] - -func add_free(thing): - if(typeof(thing) == TYPE_OBJECT): - if(!thing is Reference): - _to_free.append(thing) - -func add_queue_free(thing): - _to_queue_free.append(thing) - -func get_queue_free_count(): - return _to_queue_free.size() - -func get_free_count(): - return _to_free.size() - -func free_all(): - for i in range(_to_free.size()): - if(is_instance_valid(_to_free[i])): - _to_free[i].free() - _to_free.clear() - - for i in range(_to_queue_free.size()): - if(is_instance_valid(_to_queue_free[i])): - _to_queue_free[i].queue_free() - _to_queue_free.clear() - - diff --git a/addons/gut/comparator.gd b/addons/gut/comparator.gd deleted file mode 100644 index ff03af8..0000000 --- a/addons/gut/comparator.gd +++ /dev/null @@ -1,130 +0,0 @@ -var _utils = load('res://addons/gut/utils.gd').get_instance() -var _strutils = _utils.Strutils.new() -var _max_length = 100 -var _should_compare_int_to_float = true - -const MISSING = '|__missing__gut__compare__value__|' -const DICTIONARY_DISCLAIMER = 'Dictionaries are compared-by-ref. See assert_eq in wiki.' - -func _cannot_comapre_text(v1, v2): - return str('Cannot compare ', _strutils.types[typeof(v1)], ' with ', - _strutils.types[typeof(v2)], '.') - -func _make_missing_string(text): - return '' - -func _create_missing_result(v1, v2, text): - var to_return = null - var v1_str = format_value(v1) - var v2_str = format_value(v2) - - if(typeof(v1) == TYPE_STRING and v1 == MISSING): - v1_str = _make_missing_string(text) - to_return = _utils.CompareResult.new() - elif(typeof(v2) == TYPE_STRING and v2 == MISSING): - v2_str = _make_missing_string(text) - to_return = _utils.CompareResult.new() - - if(to_return != null): - to_return.summary = str(v1_str, ' != ', v2_str) - to_return.are_equal = false - - return to_return - - -func simple(v1, v2, missing_string=''): - var missing_result = _create_missing_result(v1, v2, missing_string) - if(missing_result != null): - return missing_result - - var result = _utils.CompareResult.new() - var cmp_str = null - var extra = '' - - if(_should_compare_int_to_float and [2, 3].has(typeof(v1)) and [2, 3].has(typeof(v2))): - result.are_equal = v1 == v2 - - elif(_utils.are_datatypes_same(v1, v2)): - result.are_equal = v1 == v2 - if(typeof(v1) == TYPE_DICTIONARY): - if(result.are_equal): - extra = '. Same dictionary ref. ' - else: - extra = '. Different dictionary refs. ' - extra += DICTIONARY_DISCLAIMER - - if(typeof(v1) == TYPE_ARRAY): - var array_result = _utils.DiffTool.new(v1, v2, _utils.DIFF.SHALLOW) - result.summary = array_result.get_short_summary() - if(!array_result.are_equal()): - extra = ".\n" + array_result.get_short_summary() - - else: - cmp_str = '!=' - result.are_equal = false - extra = str('. ', _cannot_comapre_text(v1, v2)) - - cmp_str = get_compare_symbol(result.are_equal) - if(typeof(v1) != TYPE_ARRAY): - result.summary = str(format_value(v1), ' ', cmp_str, ' ', format_value(v2), extra) - - return result - - -func shallow(v1, v2): - var result = null - - if(_utils.are_datatypes_same(v1, v2)): - if(typeof(v1) in [TYPE_ARRAY, TYPE_DICTIONARY]): - result = _utils.DiffTool.new(v1, v2, _utils.DIFF.SHALLOW) - else: - result = simple(v1, v2) - else: - result = simple(v1, v2) - - return result - - -func deep(v1, v2): - var result = null - - if(_utils.are_datatypes_same(v1, v2)): - if(typeof(v1) in [TYPE_ARRAY, TYPE_DICTIONARY]): - result = _utils.DiffTool.new(v1, v2, _utils.DIFF.DEEP) - else: - result = simple(v1, v2) - else: - result = simple(v1, v2) - - return result - - -func format_value(val, max_val_length=_max_length): - return _strutils.truncate_string(_strutils.type2str(val), max_val_length) - - -func compare(v1, v2, diff_type=_utils.DIFF.SIMPLE): - var result = null - if(diff_type == _utils.DIFF.SIMPLE): - result = simple(v1, v2) - elif(diff_type == _utils.DIFF.SHALLOW): - result = shallow(v1, v2) - elif(diff_type == _utils.DIFF.DEEP): - result = deep(v1, v2) - - return result - - -func get_should_compare_int_to_float(): - return _should_compare_int_to_float - - -func set_should_compare_int_to_float(should_compare_int_float): - _should_compare_int_to_float = should_compare_int_float - - -func get_compare_symbol(is_equal): - if(is_equal): - return '==' - else: - return '!=' diff --git a/addons/gut/compare_result.gd b/addons/gut/compare_result.gd deleted file mode 100644 index be6aebd..0000000 --- a/addons/gut/compare_result.gd +++ /dev/null @@ -1,47 +0,0 @@ -var are_equal = null setget set_are_equal, get_are_equal -var summary = null setget set_summary, get_summary -var max_differences = 30 setget set_max_differences, get_max_differences -var differences = {} setget set_differences, get_differences - -func _block_set(which, val): - push_error(str('cannot set ', which, ', value [', val, '] ignored.')) - -func _to_string(): - return str(get_summary()) # could be null, gotta str it. - -func get_are_equal(): - return are_equal - -func set_are_equal(r_eq): - are_equal = r_eq - -func get_summary(): - return summary - -func set_summary(smry): - summary = smry - -func get_total_count(): - pass - -func get_different_count(): - pass - -func get_short_summary(): - return summary - -func get_max_differences(): - return max_differences - -func set_max_differences(max_diff): - max_differences = max_diff - -func get_differences(): - return differences - -func set_differences(diffs): - _block_set('differences', diffs) - -func get_brackets(): - return null - diff --git a/addons/gut/diff_formatter.gd b/addons/gut/diff_formatter.gd deleted file mode 100644 index fd954af..0000000 --- a/addons/gut/diff_formatter.gd +++ /dev/null @@ -1,64 +0,0 @@ -var _utils = load('res://addons/gut/utils.gd').get_instance() -var _strutils = _utils.Strutils.new() -const INDENT = ' ' -var _max_to_display = 30 -const ABSOLUTE_MAX_DISPLAYED = 10000 -const UNLIMITED = -1 - - -func _single_diff(diff, depth=0): - var to_return = "" - var brackets = diff.get_brackets() - - if(brackets != null and !diff.are_equal): - to_return = '' - to_return += str(brackets.open, "\n", - _strutils.indent_text(differences_to_s(diff.differences, depth), depth+1, INDENT), "\n", - brackets.close) - else: - to_return = str(diff) - - return to_return - - -func make_it(diff): - var to_return = '' - if(diff.are_equal): - to_return = diff.summary - else: - if(_max_to_display == ABSOLUTE_MAX_DISPLAYED): - to_return = str(diff.get_value_1(), ' != ', diff.get_value_2()) - else: - to_return = diff.get_short_summary() - to_return += str("\n", _strutils.indent_text(_single_diff(diff, 0), 1, ' ')) - return to_return - - -func differences_to_s(differences, depth=0): - var to_return = '' - var keys = differences.keys() - keys.sort() - var limit = min(_max_to_display, differences.size()) - - for i in range(limit): - var key = keys[i] - to_return += str(key, ": ", _single_diff(differences[key], depth)) - - if(i != limit -1): - to_return += "\n" - - if(differences.size() > _max_to_display): - to_return += str("\n\n... ", differences.size() - _max_to_display, " more.") - - return to_return - - -func get_max_to_display(): - return _max_to_display - - -func set_max_to_display(max_to_display): - _max_to_display = max_to_display - if(_max_to_display == UNLIMITED): - _max_to_display = ABSOLUTE_MAX_DISPLAYED - diff --git a/addons/gut/diff_tool.gd b/addons/gut/diff_tool.gd deleted file mode 100644 index 9dbbd1c..0000000 --- a/addons/gut/diff_tool.gd +++ /dev/null @@ -1,162 +0,0 @@ -extends 'res://addons/gut/compare_result.gd' -const INDENT = ' ' -enum { - DEEP, - SHALLOW, - SIMPLE -} - -var _utils = load('res://addons/gut/utils.gd').get_instance() -var _strutils = _utils.Strutils.new() -var _compare = _utils.Comparator.new() -var DiffTool = load('res://addons/gut/diff_tool.gd') - -var _value_1 = null -var _value_2 = null -var _total_count = 0 -var _diff_type = null -var _brackets = null -var _valid = true -var _desc_things = 'somethings' - -# -------- comapre_result.gd "interface" --------------------- -func set_are_equal(val): - _block_set('are_equal', val) - -func get_are_equal(): - return are_equal() - -func set_summary(val): - _block_set('summary', val) - -func get_summary(): - return summarize() - -func get_different_count(): - return differences.size() - -func get_total_count(): - return _total_count - -func get_short_summary(): - var text = str(_strutils.truncate_string(str(_value_1), 50), - ' ', _compare.get_compare_symbol(are_equal()), ' ', - _strutils.truncate_string(str(_value_2), 50)) - if(!are_equal()): - text += str(' ', get_different_count(), ' of ', get_total_count(), - ' ', _desc_things, ' do not match.') - return text - -func get_brackets(): - return _brackets -# -------- comapre_result.gd "interface" --------------------- - - -func _invalidate(): - _valid = false - differences = null - - -func _init(v1, v2, diff_type=DEEP): - _value_1 = v1 - _value_2 = v2 - _diff_type = diff_type - _compare.set_should_compare_int_to_float(false) - _find_differences(_value_1, _value_2) - - -func _find_differences(v1, v2): - if(_utils.are_datatypes_same(v1, v2)): - if(typeof(v1) == TYPE_ARRAY): - _brackets = {'open':'[', 'close':']'} - _desc_things = 'indexes' - _diff_array(v1, v2) - elif(typeof(v2) == TYPE_DICTIONARY): - _brackets = {'open':'{', 'close':'}'} - _desc_things = 'keys' - _diff_dictionary(v1, v2) - else: - _invalidate() - _utils.get_logger().error('Only Arrays and Dictionaries are supported.') - else: - _invalidate() - _utils.get_logger().error('Only Arrays and Dictionaries are supported.') - - -func _diff_array(a1, a2): - _total_count = max(a1.size(), a2.size()) - for i in range(a1.size()): - var result = null - if(i < a2.size()): - if(_diff_type == DEEP): - result = _compare.deep(a1[i], a2[i]) - else: - result = _compare.simple(a1[i], a2[i]) - else: - result = _compare.simple(a1[i], _compare.MISSING, 'index') - - if(!result.are_equal): - differences[i] = result - - if(a1.size() < a2.size()): - for i in range(a1.size(), a2.size()): - differences[i] = _compare.simple(_compare.MISSING, a2[i], 'index') - - -func _diff_dictionary(d1, d2): - var d1_keys = d1.keys() - var d2_keys = d2.keys() - - # Process all the keys in d1 - _total_count += d1_keys.size() - for key in d1_keys: - if(!d2.has(key)): - differences[key] = _compare.simple(d1[key], _compare.MISSING, 'key') - else: - d2_keys.remove(d2_keys.find(key)) - - var result = null - if(_diff_type == DEEP): - result = _compare.deep(d1[key], d2[key]) - else: - result = _compare.simple(d1[key], d2[key]) - - if(!result.are_equal): - differences[key] = result - - # Process all the keys in d2 that didn't exist in d1 - _total_count += d2_keys.size() - for i in range(d2_keys.size()): - differences[d2_keys[i]] = _compare.simple(_compare.MISSING, d2[d2_keys[i]], 'key') - - -func summarize(): - var summary = '' - - if(are_equal()): - summary = get_short_summary() - else: - var formatter = load('res://addons/gut/diff_formatter.gd').new() - formatter.set_max_to_display(max_differences) - summary = formatter.make_it(self) - - return summary - - -func are_equal(): - if(!_valid): - return null - else: - return differences.size() == 0 - - -func get_diff_type(): - return _diff_type - - -func get_value_1(): - return _value_1 - - -func get_value_2(): - return _value_2 diff --git a/addons/gut/double_templates/function_template.txt b/addons/gut/double_templates/function_template.txt deleted file mode 100644 index 666952e..0000000 --- a/addons/gut/double_templates/function_template.txt +++ /dev/null @@ -1,6 +0,0 @@ -{func_decleration} - __gut_spy('{method_name}', {param_array}) - if(__gut_should_call_super('{method_name}', {param_array})): - return {super_call} - else: - return __gut_get_stubbed_return('{method_name}', {param_array}) diff --git a/addons/gut/double_templates/script_template.txt b/addons/gut/double_templates/script_template.txt deleted file mode 100644 index 6fc7165..0000000 --- a/addons/gut/double_templates/script_template.txt +++ /dev/null @@ -1,41 +0,0 @@ -{extends} - -var __gut_metadata_ = { - path = '{path}', - subpath = '{subpath}', - stubber = __gut_instance_from_id({stubber_id}), - spy = __gut_instance_from_id({spy_id}), - gut = __gut_instance_from_id({gut_id}), -} - -func __gut_instance_from_id(inst_id): - if(inst_id == -1): - return null - else: - return instance_from_id(inst_id) - -func __gut_should_call_super(method_name, called_with): - if(__gut_metadata_.stubber != null): - return __gut_metadata_.stubber.should_call_super(self, method_name, called_with) - else: - return false - -var __gut_utils_ = load('res://addons/gut/utils.gd').get_instance() - -func __gut_spy(method_name, called_with): - if(__gut_metadata_.spy != null): - __gut_metadata_.spy.add_call(self, method_name, called_with) - -func __gut_get_stubbed_return(method_name, called_with): - if(__gut_metadata_.stubber != null): - return __gut_metadata_.stubber.get_return(self, method_name, called_with) - else: - return null - -func _init(): - if(__gut_metadata_.gut != null): - __gut_metadata_.gut.get_autofree().add_free(self) - -# ------------------------------------------------------------------------------ -# Methods start here -# ------------------------------------------------------------------------------ diff --git a/addons/gut/doubler.gd b/addons/gut/doubler.gd deleted file mode 100644 index c5e9e0e..0000000 --- a/addons/gut/doubler.gd +++ /dev/null @@ -1,556 +0,0 @@ -# ------------------------------------------------------------------------------ -# Utility class to hold the local and built in methods separately. Add all local -# methods FIRST, then add built ins. -# ------------------------------------------------------------------------------ -class ScriptMethods: - # List of methods that should not be overloaded when they are not defined - # in the class being doubled. These either break things if they are - # overloaded or do not have a "super" equivalent so we can't just pass - # through. - var _blacklist = [ - 'has_method', - 'get_script', - 'get', - '_notification', - 'get_path', - '_enter_tree', - '_exit_tree', - '_process', - '_draw', - '_physics_process', - '_input', - '_unhandled_input', - '_unhandled_key_input', - '_set', - '_get', # probably - 'emit_signal', # can't handle extra parameters to be sent with signal. - 'draw_mesh', # issue with one parameter, value is `Null((..), (..), (..))`` - '_to_string', # nonexistant function ._to_string - '_get_minimum_size', # Nonexistent function _get_minimum_size - ] - - # These methods should not be included in the double. - var _skip = [ - # There is an init in the template. There is also no real reason - # to include this method since it will always be called, it has no - # return value, and you cannot prevent super from being called. - '_init' - ] - - var built_ins = [] - var local_methods = [] - var _method_names = [] - - func is_blacklisted(method_meta): - return _blacklist.find(method_meta.name) != -1 - - func _add_name_if_does_not_have(method_name): - if(_skip.has(method_name)): - return false - var should_add = _method_names.find(method_name) == -1 - if(should_add): - _method_names.append(method_name) - return should_add - - func add_built_in_method(method_meta): - var did_add = _add_name_if_does_not_have(method_meta.name) - if(did_add and !is_blacklisted(method_meta)): - built_ins.append(method_meta) - - func add_local_method(method_meta): - var did_add = _add_name_if_does_not_have(method_meta.name) - if(did_add): - local_methods.append(method_meta) - - func to_s(): - var text = "Locals\n" - for i in range(local_methods.size()): - text += str(" ", local_methods[i].name, "\n") - text += "Built-Ins\n" - for i in range(built_ins.size()): - text += str(" ", built_ins[i].name, "\n") - return text - -# ------------------------------------------------------------------------------ -# Helper class to deal with objects and inner classes. -# ------------------------------------------------------------------------------ -class ObjectInfo: - var _path = null - var _subpaths = [] - var _utils = load('res://addons/gut/utils.gd').get_instance() - var _method_strategy = null - var make_partial_double = false - var scene_path = null - var _native_class = null - var _native_class_name = null - - func _init(path, subpath=null): - _path = path - if(subpath != null): - _subpaths = _utils.split_string(subpath, '/') - - # Returns an instance of the class/inner class - func instantiate(): - var to_return = null - if(is_native()): - to_return = _native_class.new() - else: - to_return = get_loaded_class().new() - return to_return - - # Can't call it get_class because that is reserved so it gets this ugly name. - # Loads up the class and then any inner classes to give back a reference to - # the desired Inner class (if there is any) - func get_loaded_class(): - var LoadedClass = load(_path) - for i in range(_subpaths.size()): - LoadedClass = LoadedClass.get(_subpaths[i]) - return LoadedClass - - func to_s(): - return str(_path, '[', get_subpath(), ']') - - func get_path(): - return _path - - func get_subpath(): - return _utils.join_array(_subpaths, '/') - - func has_subpath(): - return _subpaths.size() != 0 - - func get_extends_text(): - var extend = null - if(is_native()): - var native = get_native_class_name() - if(native.begins_with('_')): - native = native.substr(1) - extend = str("extends ", native) - else: - extend = str("extends '", get_path(), "'") - - if(has_subpath()): - extend += str('.', get_subpath().replace('/', '.')) - - return extend - - func get_method_strategy(): - return _method_strategy - - func set_method_strategy(method_strategy): - _method_strategy = method_strategy - - func is_native(): - return _native_class != null - - func set_native_class(native_class): - _native_class = native_class - var inst = native_class.new() - _native_class_name = inst.get_class() - _path = _native_class_name - if(!inst is Reference): - inst.free() - - func get_native_class_name(): - return _native_class_name - -# ------------------------------------------------------------------------------ -# Allows for interacting with a file but only creating a string. This was done -# to ease the transition from files being created for doubles to loading -# doubles from a string. This allows the files to be created for debugging -# purposes since reading a file is easier than reading a dumped out string. -# ------------------------------------------------------------------------------ -class FileOrString: - extends File - - var _do_file = false - var _contents = '' - var _path = null - - func open(path, mode): - _path = path - if(_do_file): - return .open(path, mode) - else: - return OK - - func close(): - if(_do_file): - return .close() - - func store_string(s): - if(_do_file): - .store_string(s) - _contents += s - - func get_contents(): - return _contents - - func get_path(): - return _path - - func load_it(): - if(_contents != ''): - var script = GDScript.new() - script.set_source_code(get_contents()) - script.reload() - return script - else: - return load(_path) - -# ------------------------------------------------------------------------------ -# A stroke of genius if I do say so. This allows for doubling a scene without -# having to write any files. By overloading instance we can make whatever -# we want. -# ------------------------------------------------------------------------------ -class PackedSceneDouble: - extends PackedScene - var _script = null - var _scene = null - - func set_script_obj(obj): - _script = obj - - func instance(edit_state=0): - var inst = _scene.instance(edit_state) - if(_script != null): - inst.set_script(_script) - return inst - - func load_scene(path): - _scene = load(path) - - - - -# ------------------------------------------------------------------------------ -# START Doubler -# ------------------------------------------------------------------------------ -var _utils = load('res://addons/gut/utils.gd').get_instance() - -var _ignored_methods = _utils.OneToMany.new() -var _stubber = _utils.Stubber.new() -var _lgr = _utils.get_logger() -var _method_maker = _utils.MethodMaker.new() - -var _output_dir = 'user://gut_temp_directory' -var _double_count = 0 # used in making files names unique -var _spy = null -var _gut = null -var _strategy = null -var _base_script_text = _utils.get_file_as_text('res://addons/gut/double_templates/script_template.txt') -var _make_files = false - -# These methods all call super implicitly. Stubbing them to call super causes -# super to be called twice. -var _non_super_methods = [ - "_init", - "_ready", - "_notification", - "_enter_world", - "_exit_world", - "_process", - "_physics_process", - "_exit_tree", - "_gui_input ", -] - -func _init(strategy=_utils.DOUBLE_STRATEGY.PARTIAL): - set_logger(_utils.get_logger()) - _strategy = strategy - -# ############### -# Private -# ############### -func _get_indented_line(indents, text): - var to_return = '' - for _i in range(indents): - to_return += "\t" - return str(to_return, text, "\n") - - -func _stub_to_call_super(obj_info, method_name): - if(_non_super_methods.has(method_name)): - return - var path = obj_info.get_path() - if(obj_info.scene_path != null): - path = obj_info.scene_path - var params = _utils.StubParams.new(path, method_name, obj_info.get_subpath()) - params.to_call_super() - _stubber.add_stub(params) - -func _get_base_script_text(obj_info, override_path): - var path = obj_info.get_path() - if(override_path != null): - path = override_path - - var stubber_id = -1 - if(_stubber != null): - stubber_id = _stubber.get_instance_id() - - var spy_id = -1 - if(_spy != null): - spy_id = _spy.get_instance_id() - - var gut_id = -1 - if(_gut != null): - gut_id = _gut.get_instance_id() - - var values = { - "path":path, - "subpath":obj_info.get_subpath(), - "stubber_id":stubber_id, - "spy_id":spy_id, - "extends":obj_info.get_extends_text(), - "gut_id":gut_id - } - - return _base_script_text.format(values) - -func _write_file(obj_info, dest_path, override_path=null): - var base_script = _get_base_script_text(obj_info, override_path) - var script_methods = _get_methods(obj_info) - - var f = FileOrString.new() - f._do_file = _make_files - var f_result = f.open(dest_path, f.WRITE) - - if(f_result != OK): - _lgr.error(str('Error creating file ', dest_path)) - _lgr.error(str('Could not create double for :', obj_info.to_s())) - return - - f.store_string(base_script) - - for i in range(script_methods.local_methods.size()): - if(obj_info.make_partial_double): - _stub_to_call_super(obj_info, script_methods.local_methods[i].name) - f.store_string(_get_func_text(script_methods.local_methods[i])) - - for i in range(script_methods.built_ins.size()): - _stub_to_call_super(obj_info, script_methods.built_ins[i].name) - f.store_string(_get_func_text(script_methods.built_ins[i])) - - f.close() - return f - -func _double_scene_and_script(scene_info): - var to_return = PackedSceneDouble.new() - to_return.load_scene(scene_info.get_path()) - - var inst = load(scene_info.get_path()).instance() - var script_path = null - if(inst.get_script()): - script_path = inst.get_script().get_path() - inst.free() - - if(script_path): - var oi = ObjectInfo.new(script_path) - oi.set_method_strategy(scene_info.get_method_strategy()) - oi.make_partial_double = scene_info.make_partial_double - oi.scene_path = scene_info.get_path() - to_return.set_script_obj(_double(oi, scene_info.get_path()).load_it()) - - return to_return - -func _get_methods(object_info): - var obj = object_info.instantiate() - # any method in the script or super script - var script_methods = ScriptMethods.new() - var methods = obj.get_method_list() - if(!(obj is Reference)): - obj.free() - - # first pass is for local methods only - for i in range(methods.size()): - # 65 is a magic number for methods in script, though documentation - # says 64. This picks up local overloads of base class methods too. - if(methods[i].flags == 65 and !_ignored_methods.has(object_info.get_path(), methods[i]['name'])): - script_methods.add_local_method(methods[i]) - - - if(object_info.get_method_strategy() == _utils.DOUBLE_STRATEGY.FULL): - # second pass is for anything not local - for i in range(methods.size()): - # 65 is a magic number for methods in script, though documentation - # says 64. This picks up local overloads of base class methods too. - if(methods[i].flags != 65 and !_ignored_methods.has(object_info.get_path(), methods[i]['name'])): - script_methods.add_built_in_method(methods[i]) - - return script_methods - -func _get_inst_id_ref_str(inst): - var ref_str = 'null' - if(inst): - ref_str = str('instance_from_id(', inst.get_instance_id(),')') - return ref_str - -func _get_func_text(method_hash): - return _method_maker.get_function_text(method_hash) + "\n" - -# returns the path to write the double file to -func _get_temp_path(object_info): - var file_name = null - var extension = null - if(object_info.is_native()): - file_name = object_info.get_native_class_name() - extension = 'gd' - else: - file_name = object_info.get_path().get_file().get_basename() - extension = object_info.get_path().get_extension() - - if(object_info.has_subpath()): - file_name += '__' + object_info.get_subpath().replace('/', '__') - - file_name += str('__dbl', _double_count, '__.', extension) - - var to_return = _output_dir.plus_file(file_name) - return to_return - -func _load_double(fileOrString): - return fileOrString.load_it() - -func _double(obj_info, override_path=null): - var temp_path = _get_temp_path(obj_info) - var result = _write_file(obj_info, temp_path, override_path) - _double_count += 1 - return result - -func _double_script(path, make_partial, strategy): - var oi = ObjectInfo.new(path) - oi.make_partial_double = make_partial - oi.set_method_strategy(strategy) - return _double(oi).load_it() - -func _double_inner(path, subpath, make_partial, strategy): - var oi = ObjectInfo.new(path, subpath) - oi.set_method_strategy(strategy) - oi.make_partial_double = make_partial - return _double(oi).load_it() - -func _double_scene(path, make_partial, strategy): - var oi = ObjectInfo.new(path) - oi.set_method_strategy(strategy) - oi.make_partial_double = make_partial - return _double_scene_and_script(oi) - -func _double_gdnative(native_class, make_partial, strategy): - var oi = ObjectInfo.new(null) - oi.set_native_class(native_class) - oi.set_method_strategy(strategy) - oi.make_partial_double = make_partial - return _double(oi).load_it() - -# ############### -# Public -# ############### -func get_output_dir(): - return _output_dir - -func set_output_dir(output_dir): - if(output_dir != null): - _output_dir = output_dir - if(_make_files): - var d = Directory.new() - d.make_dir_recursive(output_dir) - -func get_spy(): - return _spy - -func set_spy(spy): - _spy = spy - -func get_stubber(): - return _stubber - -func set_stubber(stubber): - _stubber = stubber - -func get_logger(): - return _lgr - -func set_logger(logger): - _lgr = logger - _method_maker.set_logger(logger) - -func get_strategy(): - return _strategy - -func set_strategy(strategy): - _strategy = strategy - -func get_gut(): - return _gut - -func set_gut(gut): - _gut = gut - -func partial_double_scene(path, strategy=_strategy): - return _double_scene(path, true, strategy) - -# double a scene -func double_scene(path, strategy=_strategy): - return _double_scene(path, false, strategy) - -# double a script/object -func double(path, strategy=_strategy): - return _double_script(path, false, strategy) - -func partial_double(path, strategy=_strategy): - return _double_script(path, true, strategy) - -func partial_double_inner(path, subpath, strategy=_strategy): - return _double_inner(path, subpath, true, strategy) - -# double an inner class in a script -func double_inner(path, subpath, strategy=_strategy): - return _double_inner(path, subpath, false, strategy) - -# must always use FULL strategy since this is a native class and you won't get -# any methods if you don't use FULL -func double_gdnative(native_class): - return _double_gdnative(native_class, false, _utils.DOUBLE_STRATEGY.FULL) - -# must always use FULL strategy since this is a native class and you won't get -# any methods if you don't use FULL -func partial_double_gdnative(native_class): - return _double_gdnative(native_class, true, _utils.DOUBLE_STRATEGY.FULL) - -func clear_output_directory(): - if(!_make_files): - return false - - var did = false - if(_output_dir.find('user://') == 0): - var d = Directory.new() - var result = d.open(_output_dir) - # BIG GOTCHA HERE. If it cannot open the dir w/ erro 31, then the - # directory becomes res:// and things go on normally and gut clears out - # out res:// which is SUPER BAD. - if(result == OK): - d.list_dir_begin(true) - var f = d.get_next() - while(f != ''): - d.remove(f) - f = d.get_next() - did = true - return did - -func delete_output_directory(): - var did = clear_output_directory() - if(did): - var d = Directory.new() - d.remove(_output_dir) - -func add_ignored_method(path, method_name): - _ignored_methods.add(path, method_name) - -func get_ignored_methods(): - return _ignored_methods - -func get_make_files(): - return _make_files - -func set_make_files(make_files): - _make_files = make_files - set_output_dir(_output_dir) diff --git a/addons/gut/fonts/AnonymousPro-Bold.ttf b/addons/gut/fonts/AnonymousPro-Bold.ttf deleted file mode 100644 index 1d4bf2b5265bfc9dcb92eeabd5d3691035361a9a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 107624 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 PH|o@)X;BICzq{yv!e16k diff --git a/addons/gut/fonts/AnonymousPro-Italic.ttf b/addons/gut/fonts/AnonymousPro-Italic.ttf deleted file mode 100644 index f6870b7c4e5af796982cc026e159e844c5ad8233..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 98424 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≺`{ReJ=xB61ni;WZBNY2Q_gY=S5D_ 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 S6E|)CSBUoGWXG@WbN>SdkO*x6 diff --git a/addons/gut/fonts/AnonymousPro-Regular.ttf b/addons/gut/fonts/AnonymousPro-Regular.ttf deleted file mode 100644 index 57aa893826db03a74c98480093e5c9942798391b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 112072 zcmc${3tUrYx;MHmNeK6FNkTv_A&`(p2oOSoD1smaij-kEF)~OgRW82X5BAfj!ja}nL0?%M6P{d$?*Z+5qKaJRF&yZvT&x3WC{_gz_N zI?f*F_dDlzIwQ$iEY^D8=Y8Ji`9Igiunfbf@I%Hhk9^l!Sl98|_ZhaS9B13zRVzH} ze*ENO9KXyk^8fYthQ~HP8?SwqVLLnw!~fIc+qY%N4n(}fu-)~zKCg3g*M?^+KKPnp zI~Owy`&idwTQ@UmM#->0ox<-`UF-WhFKqqnb^I=hVXMFBUbA6G|C$vO4D0zH3={r8 zyVpLp=8olsUoz|pT+S4C<3vOk_jh>y0vu;`Z`ih@Xr28Ee(%J9|7rcE#~&M)cbsHc z7k)4Mrwxzo*eu)6KgO^%w{d;O#>Y0Sm36gtV|@ROVM^C+-n4bwjnl>NG3?*`j$z{d zZu66CH|PJyKj8OWFXQ_Ei{Y38%xkM$$M8%Tv)re&`ogU~<|Cg{XwgD zjmwp{7$39TT1R=2c&Y)u*@mxR1v9=v%%Fl`rgyFU&fwz(JVG7l{Lv zFZPO0q4LFD@$o8O!WBHlr=~x3^vnYK+q5g<;8YloDj_(yL>wea2XxpNDIK-~zfs^I zkis%DOCXkwl36fGpX>_xsl4J#QTZaS_+nMQc=}Uw#TTXWCDHSEAik$w$uGz@s10^K z{u%7HIQ+BQ)O!50sSR;9Oh1kv`AzQ~ruR4aua~8t!kIhFKa%5(ta8IYEu0Q8@ruu^@?lU}Dj$Zkz&i24 z!kY`Z)#WMS3v~<43riN(E?l*+XJP+B@518?Pc1yR@bbdw)xL#f*uGRN`Qr?F3+t8g z6opM=Gun!6)i$@S)3)8V&vw){YMZoOv_-F;xMsU<<5rU=7t^OL`H1nUt-gn>e(OW( zxcCx(G5yFdkmvCxheo4~%j4}uWt;ZS_@kCG#_|)SF2NTD7w3SagZdd^E{gV zna49vWuD8toEg2kJhroNd*QyqqlKe|lZ6)x!&a9^D+)D*#=_#l>Oyy+0{1PvUdXMU z_`rG7N$=*=InB-zXRULUv&Y%*^g53_PdU#yFFT`GkC%nt@+GN!%dsA@R-YDI7F$yd zwpL3#o{EE1!s8~c6ce+vact!s#@*1fyq?a$|se3Jh!yYoBqx99K6Kbk+9Kbe0qpKw9`^?Y!FCMKV7fyO#+Q{I~Rp!8-bUAt0U zskyYIw6=6rX-{c?skiia>8a9lrI$;iSBsKLhvm^pVpNU;xwLTMI0(nVcup9hevUTB zl;g;$$yu4xmD86qm~$-WWX{=~nH)m>oEtgVo;uZdnGCC1Hd{tlwJfPDr_5RAD)W?e zm+dIqUp8DeRyI|3sVthTltK}v4Kpbe(sDVBMte~)Sv0y&+2UdcTZn(S1Z%}(x0Yc~ zW9ecz{z80X>0h_~b=^cy{otC#Dc17rvZ}!IRehdmNh%uch@iNX|7CdINtEWq`7{5b;HKys%+uRvcuJa zwzPn4OwH|ex9{ekwDvumwy2`f-q2zwZD3fom#^ZiR97W1&`oz$5D1Bx@dlo=S~|^g zonT?FF)VXIILN&r{}>v%ETD?U2EdxCG%BO2SXHfZt2$NNRr^#&Rimm&)kPIqQ`L19 zhXsnb=hkub)_ELQJ07Ib#_;OIG)~L)KQX#_@#4*+Pq-d^zRPa!I`Wusu=(1Tta5OW z4g0ri%`Npm`|$Vu1E0VDi+YUYI!5wYa3oB0q|7q$LFCOy$|{k%NONRKWNqZC$ezgl zNN?ov$WxK$A}>c0R*4fwLK#FHNg{^@G?69A<4le;E}r{r@zx)cQS{jDJv6!=b3d4Rwe3w|C%fm(wpXXx-(vR(PrXIP#W2x820kW|aRj(%9^wibAAkTFKOFj< z6G@@OV3inbAW{)jiqP7TDxU%x8K}b$r?>0lY(O6)LiaYIn;qW94znXd4|}YaJtp+v zIbR4vTn94^oeM5v#JjS%D@zHS%^DK84&nFh)6;YBa8@!dc7z|{2to>H0z7#td6FGZ zxZTQ+up_;MGXZU^(mjJCmB0}Y5zql=ot~b?-OP7XyjNaF??!l-x8iP)mb`b4>)-^r z_CNo#NTDFI>Ek=I{0wB15-XL>eC0D*eI~2VNI^-wb)1QUcz6Is{Jr4Ac zPkv6XC8AGDVc*32idhAfK8hqoj>4&MDLjg9#SX=O#js*bF{QYqfKJh?#v>I(szque zO_7esn#h%rU6Fl}gOSG~Pez`NoB`8Myc>A~JHsDGZ=YX~kf>#i5I0X&0`| z?ewu0R_5AWD%{?4>%x{T7jEs@b8C9bmg!p}4>H^=BuqTBC=jO%@?dNbUB+VOu@V`i z)g9*)xBN0DmXHDsIDE5lvvcoUjc-`Dt7^#)wl^I3`(A#)yn02^ z)Aier)_v0oo`}G5_hF?om?wR5Yal672PG7)jK>xTSNRx#KEQI-co<+Yjx(Y3iMn5a z;Vh7b6M=&WqBQ`)eG63MY6TR6dR9%BM4hD0Q9IQxwMX5p-l5*F9#)U3r_`6!R3Q*_ zhMj{wV`qb+UZFOKDuLgcP|&zUDB(=Kw|(qwQ;jp@hrJ_XyFUB4`T4EO_?cq2r(>yL zV;NznpOcvy8r{|%1Cy`(=Iesiy@Z}A;NfA+teE+SKvrZnre%#v!?dib@r3YOkmX(eg3X{*wD()!c9X~)w}rJYN=oCdZm%%-Tzs`5F5WOxWO ze@G&OB)tMUMG_t(SB=L|ev8q@m|`3;H8Cq=x?=ib24jxJoQydeGXqYacsJ$-wuA2> z)wrGzO+TxrMAIkfbM#KVOYhNl>v!n)>xcDY`YHV-JsCxk4(pAL8{|NOeQjj5A(X{s z7qO0Eo>LWJX*@q%-Fv+G(ZAW*pxhfZeW0|vA=lP^aCOzLHD%9kd)Bvm+4j-3&4)KE z**j3(U7gK#yAQ5$7OffXh(9)Lat}RLI)!IE(8}}X`={&Ki6f|~kI+74k`n`>! zWySC#;N2L=i|+)YqX_p#Q*#D#RN~<SYC#UVdjCEiP-}?Lq?YcvN!sQ zFGgZ&a0vLB7&vSk%s8<1+!SXxbM@*~VMM5fmD9)P-oDy*hhh7;p*aXe*jM0y(-_r5 z%&5;|^(}O1dQeQo235yYCsk)v zGeq6LtGWSo?@Ol(?>ETPzqO`m{QR0`D#D;y)Oy%21S#>ST|0fZ;x*tNqcPHFdjM?Um)+4|;{} zMN2FCUct)U`3f|84Z9>0`{d(*E|S_KQ3i@vbQ+yeSFEenxpkeo?Ye!sqq-7<2}+p~O&YSY_xj^c%c}NGBlY#(w6ghfct zzgDyB%-W#~TW!k^ygl^u-7PJLdVchTJ*J_vZ*k`<1IxX+%>ygyceNDcH1BlR4YU-n z7uLVhzYMZ`&)!?FZX9gvXgqXzhpnZ;w0z)|uDacA#f2>c57+Hkv#6j2J|F7)7zS^u zn1>0{insdYAB`&@lj2n1d0&imJUTk)_J{_vMN1gzzVsuMgL1kgEQa*6Juj)*n&V= z6yb!lD6GrG2RS!^^c z%_4evk*>&GR8mx1w5q75sK3ZtbiC+P(Yd0_MX(|PA^fosWJdu>lxfP0WyNLHW$v=h zvh8L2%8r(emQ9vjgw-%{t?W8X3x8rn8M%F;)o-AGHJ%q0B+q@#nLvS}L}u~FmK5Nx z{090#j1>#vFUzb28Px!Tv^#w*;hGOd93ne<=M)aO2S)}H!8o)TMp3^F>lvE{N7`KCVZRI&eZKb)3N52 z-F2qgo}~_VslIyqi_Oh1Zm-stdYt9mb>^uT2M1q#?$DvU5DHA z^4bq~+1IaLqEswyY_oXwdUA8|!_wBcSfO09dOc^}xNFzOuBV<7!fVzpO-)_8wx+tX zO0Tc##QvYfxpqwXL>l?>k;df!x03JwM!FSJij+FM=4TJ|Kw!^LIrtz_)IArN3g! zg)bonE^ON0SXcl#&~Iy5W~v@|r4w?%Uf|hT4>?c*w-dvO=$#({CJ$q*0Y=G!0-`&R zRr%yW^hZ62k_3=J9LPW}?DnAdkK+4jZub1`)@h=Rsg)7D=R@K>MYR=jAG!vxJ78vU zAfkA?1Y&}~T(A)<1b3X%1TB%7JO9Wh^8d)lVTnk#Lv|R;YFR#Ug@A{y5E^qI;qawV zXr#W@8CWiVp!14l0$~bh`2f$)TQI~{60I0w$#A>+=H8(?`_P?#`Vh z5ts(nCp0#&|0pnzu;Z++k<4}s2=*X2m}m6NGl57Y9I64CG68givy?iL@nxJR#z(@< z1j?lex&(7VNkVPHs)U||{seEr@q|+e=MpX_fJ*^B3_&gp(ee^tAv#+_m`yXQq0FX9 z(&T8I8kfeS>DKJf?AHuy#xzr!OB%v##QXwMlL6G0$%3woFcZ<_8bW5at;F5wUUazB z-QzA@-?jA73X@_W{ujQ{>1n>Ip|```x?y?4Q%|(DK3?KkV_x3AC}Hg9r?G1#`|J^> zoh=U(hF4+Eo+!*25*rpiJc>rlAEs%_mB~}abH>X?9-Gv7(})pA*BVzDdyM@?ukpB1 zvAR4w+^92}jU`6e>hd^uVW_WAxw<^^uCGvz4NpCV%HtFsjmPLI_EdY^o=(qp z54n7w=cs4YGwHeLA(wludx%?u7!41DaUYg8{Sq83A)EetJaKy^vERjjV+hOpBbPi( zgm%hzlgQrwp30}cLOk^k14VG4d+uy5rF(8tZceT<*OlwZ?atkiyFYh0cPw`*_fjqv zafxc&!4Vna&^k;Ghoi=^($VGUa|}9;IZisxI%XV1hB$6GLPWRyu4fACqlNuM9EKEH z%kE+WIAVwc3#(ZsBO*SVScn8Th}ZHnn&Otq)Z&M9nzpiK9=j&9YHg*VA!$#Nc0o>3 zWSpsNRd3yv;ng`gtB-GP`akWZ|Kpdfhc`V^s7)$-Wb-rMBc`LUTf5X~bY#Vq6lE`d zNW;BS^@A<0M58TZ!NYBf+Mg<~8|+$Q$hOb@d&Yv|@2;^f_YU-|u(Y4}hu*DMPqZ&8 zsM>97{qIM|R<1gA^uK@4wrTQ)tiY7}@S_DAHs;oTFBjbNE9{t0fIY%s{pE{mL&Fl% z+RU>$Sv5)A1e82S+z)yKujdV{ErI9xPmT=C-R?ia`lok(_#xXX49Ru3uW>(`dlV0# z2u~=3;4Zwi+jQ7(cJXne+ETv_kNBq(0)JFi_08j~k4l%I^22>aj zA)zkEFbz+1F*z*b5}=~Vw_^Upmmdr5thec z|Ag&5KP}g#)a_aKW?en|*m93GPFNmS;GVlRI4~#dW>|RpXZcqkm9m*ng5Hz(kAmhj z+$f0iAlwzN8jpirjhKp3HJ(O&DruTDV_I=qb(%Y^Gi`g?zO?^)(tGIA+m~@=9a1<#@2NF5_Dn6QQq-fQ+j>s+DEP(r@QkSI5(ebOx)lQvD z=h1cRcIfu&hIM1QDcvPq#A-q?169KYt-)k)7-|eF4PAyl!=T}q;iTcLVFt*4;$6cH zz<+`}$(9JZ;OWjWgrJ-R{uSHreBRgk?e_lHy#243dwcCI16AvWnhl2Lp>?;~S@+nl z2zDlZ5w~YpXkI?hVq0_K;^Q4NKU~Y2h@2NYT1zb96~`ZS<<>p6LE)Z}jo#Q_<(5FGqus6sqxf2|tFIrIJO0=-)W3 zID#zVMYaK05cwudC4P(N-*)uBJ}7wDvAwVNmw4ZJMffe7{>mHN(4Nl&-E!R%6QAv2 z8CLhrJFJFSqf)dpl35anh$4DE;#(SCS~+R|L2D{;8G@T6#)bTnZ_hR#@g1WZqUD*~x_ zfR|q;8^HtSh@~%b_ zG-ER3Vg_O5jO!U-}*z?LQbkDTJw0W>ctA zNJ&b`NpYsQQamZ$DLYd3rwpfzrA(z{XKS-f*^ca*?3LMF*?rlA*~hX^ zW}nTT$tE0~eIq+WDNwX{AC@k55CrocVSzu%*Dp=R2_Lh5&b#-^=}jT*T{GaBXYX6T zp~&Yhf3*9etiEXKtzbbOz!>=zV00P}54+-HMW-|o&LLu(`r#znNjyX~@4O5TR|I7^ zV#M&I;HQn>dSLDyua~nP;9AMb!mHu}w-O*pypfb!i8u1U&=b^kgDpk46p{@d9QbY` z;8>GFy&iv-9?k^>OO)B~wPH;B)n(R67pk|ZPi51k&N zjW9(xB5ER5Ms!8=MGQt9i#Qo^Hex1%EMLTp2yyvjfbN^xn%2%w4=kKFAppo(}mSQi5XkZ$FBVno0r^ zZ9uMrmu!CO&y~0+irGL|C}y6E;En_DkPHVX-H5QezN`>o5Q+U4`R%7s9+Jd_pJSAm zm|re_M9>d1%{aus$V3wo2v2?5al`BX{TF`O(z!>uAAR!%ChF!+OH?K+9?o=-Xjdc| zArXa$rO2$~yqNP5Ci#m(EX9zahLBxi5sZ#lg#Ix4L8(v1#1aNLJ@;?kH`xC;AZz@_ z2WovHOT7n4C&2UZPre*vDA4ibPzEN!IKT&DIwantKB@3TU7|U$B(XMeRbo$Kf1)?> zc;czVbBUMXlOh%~(c)UgeYH3o3q+B_S{4<#xV-n3am1f=YfLOLH83w&3b+F&=c!^(3}DNCI&62vL-={-ETj;LekZ1 ze>fA=)dc%RAkOt9oHitL1FH-{Pj&UUTwLWyqOTty7zxpRe=oA6t^;uj;&O)4hRk5$iJ(;8P(lOK z@COOn7>O1&&JqJLMQKbj<`|tum(gSFHtsO)Hx3)ej8n!-MobW(HIJn$kNVPjnT`^H*lm)7^x>UOn^5YuwPz zPbd4L_H}YxFUOtvY0Ostr@N(C+2a>atRZYB+!o$r%|R~1PG(}6B<5+K&gx6H`XVHh zt-DuiVrQzz3aW0x>kdbzN~bcbN>sI~RjM9UzsjpRt~#YUr@9Q+Jdyc{_mlY{@@8J6 zYFJ{=Aa21y>sbk~OrWCKz#1TUz<@Nf;jVGvJ0t9QlXBsrHA79G|M(f92KXHGsQXL zYT{PLb;b3?4aOadI~jL2ZYGX!RNRd?xa4xgKgm{&7Z8>zm@S|zRghGWQ{XIc6?h7| z3w9LjFBmQuE0`*{R3OCx2?vqr&OHved5-!c_@#wQbx+lk&oyqGD&DrOtbJ$2qK-O? zWA(PhHBWD@uG;wQibsEx+uvW%yneC0-DR=4x0KdByRD9GZra;k)YLe{9<1Edn5$pn zaV~8s&(-K0&HXKnJKebrjr)b6(ym%dlD&D6&E?3|CYQrahn2v8G{P0jJmFITon0Z? zHT)xgc{sb;$KsP^;nb#4(s3D<2G&*-j^N6R3oZvub&!ZIX;Zj7{wwqZCIUnwxqTF{ z!DH4d{O<1&`x1WFi-d)Cw(6UAdI6?{3+y?nTZ3py2MY zqICG#*WT(SeI_w}dH^iRs3#=YMN zdOf0;Rrk5~AHzFovq%r3ow2?_is|8ipQ;LEdzr}hkf ze~OHS+c~!f4v$yj@`Weli#jqH_Jxm4hhS8d)e2xEf{tp z``Ntx)rS#BGyeyH%~0MzEHOdE0@K-#{2TH(h%9os_HposU=#n0{V9whb~)%roN2JXuEIx`8&HH3ZvhK}#K4gi z)J^m;00IG?C1yLGqM&Ly=o#QsbMVJj&P2u1+$p53PWs|@SkKB zTNK@!a957A@}r=xf=R_KNbb>HeFH`a;&$v}1Kr2`=&SFfXHQn!)fAu)hvZ5N>!37 zN99zxR325gYKLmSYFIU??xh!Vt1Q~5b+&r9!4ttH=DO^qk5j)MbQbX_CBjSm8T^{WU5cHT zEorTAXsxI#{%9KZB@3V?NbW&0IFPWkAjq&GsxBl||Gw~NJcH6TLz`jBaAeeEtjy@j z=*t+)IF@lT<7~zZabdliaRV+aAfyb!9V!G%$?7G~CR4bRoRplC>`Zngdy>18cO>sm z9!?%ho=U!yjMa-)CF4(sl|llLBI*eycZk3_42kPB2VrE{z?t>+LyNuM#Y6S$&kQUZ zGB@vRdZ~A@x1njE*}}f_#Kdk_vAr2tr*>OO?e3qhYaCp&Xr~ZU;as$4knWdR&u#VHd_i- z?`i5jVez+aaP9VQSohQ2wTb2u)1WX^)>&r;4I3K=I~Fxn5zA2WRFU-j-;s1kI16AI z5)U2H9HE!sEr5tb*#>?>$HL}Fr_@Z>q#M(V)2q|n>7D7@)AywxO&?94Ouv{;L`C}b zbV*cD(@vu1pVs}7h8)kLJesA=GG#fkYO+>lb!GKs4Q3t7I+=AgYlcMU-_5##=)9kk zXAvG%Q`>}?hY!lC#DDUwpcsUNo|j|;NtJQH*;9J3Zj-OSqN3losqSFuG+T9`bf}>T zW?F2Xi&h{M*$NyIRGKh;9roIDcKwdUXxr* zjaE%apism95VjIDb@l4ZWov~tb*W{~9`+z_ovQ{4obLVEn+jR4OtJSc-7i8fzZI*R z&g8MI&uaDMQ@L)XF+8<(Jcn+M98Hcfr#PoN$DPxevpr{D&e5FFoXMPvq{iS{&UI2_ zfZ>zEFx7Z0wi?QaVpXUi64@jTIy6n9L^2MNrGt4CM%?6dXGwk%$!QXZk`Wg#{253J z0ykN-7L&zcsj;lIbXoc=gO+2Kla{lV8RDCH*Kz~C8NVvtLN-z|4UJ8_Z>*-(F|^Z>RRHdd2mbhP1MX+-FWk2MKxa3B8&nZA4B^^qbIJdpaM zfv6Y-YztOFEdkUFif)l{_>W1zizM$81_*JYhwLS3`_SZNNEUd`$RC0*(J+l*N!SvI z^$n?q_yx(MC*04j{(D*5`L91EJb(Kgj7cW+(r5{f;;k|<gOH zLr^skLE~=VL*PI0k?h^HB>Ln;1vfiAEo}WmM^9F-8-+#Mt8DA57{Y(H{;jLN$>i!> z##?)Dw@#qilExW{?=_P7&VxiT?z^rMRf16rf5iQQ87F=r>={EaBjJ}qE_>ny;VVS{ zpJ)FdG!w3qRw`z5RWuTjLPSi6@R5d3*)0T9QdLIwNZNz%5wabV7y=Xh3+TuE zZc&iUa?iF=Lq2PXj8^j_UZ=gG_CSffV39t;>$N=NEvWi$eVe$K8a*S=&3#Ee7qx6G zp8Jv?8T@On_u!#<=>XdO<B=6z^ozI_+=a8tw z|EKXBi8K;agUb5&a z@b~QQ+S=V`VMKH;TGUCguhZEvNIdPr5ZsM~OCHch!>)QT8h_hHlSmf=hUtEif*ul4 z|6&{MFIWVBbnLxV7APKuXPRs^DN$T;*G8*?jaEl&v|Z1_M62>z+&h~Z1{W_LgrauC zM%&;gHk#m&>)h*W_WX2RgLloMMHNDnXrrMj9keAliO}}Tp#2NumjQaCC0F4ejVmFl zBCq(Pm~c@z5ut?)i$qPLF|jzYI?#NVKL3Q6ld=R)XgZR`A688MCl+5fsWu%E-xZX1Fpu8QmE>GWKT-XN+Y`Wn6+W z><<&IV3ESWQaY{9q;u$MbSrgTx<1{Y?wIbR?yPQxR1UnWyMY8Ge^ijdanv0_?Y1}( zc#Ao>#f~@;6jeR-ll6yp0r?Eot^dhW5`9ZcCHk)W>7JVP?yo;(FWZX=@@{|$>_qu6 zML|LB@Dk{&W%fW5k#c)zBH{{QWg)Z1Y%~{}tIck6r+K@1pZTbH)I4dvXeKOVzHXLmQ|h^r_(j@*fjl`$_yME9 z(~MP*(ngu098oniThMIQ&uzuI= z)+Jq+o?z`Z_ogKcFYH=T)AzD_;O7mF#*ubQPsz#oH^YUp5t_ z$15Y#7O!bSPJBj*{RwAoY<6j`$x2TzT0`*#u}^`hQkcDggea1d1*XBWBGV}*$T}f- znWAEeSRsN;a*Gg!5DugXuL1R5`{46PqBKm>CYh2PNi|6;le&`nk_MBGC7nz3R| zXqa>(>E3KRU_+Ed(M&KJae?J64D$}mD1TA82Tahb%SO3E-pt5J2#U4>(qM|317zhkO*&?E*ct@IJ$b=~= zBx8$$0~`?R030IM%1v^IyhgrK-X-so56X|pPs-2AXJ8ggyeq!}+rb|jCx&wp=Cf}x zMT4C)WW*JCH}uy(<<32DphFPGPy_a}onB%duqSS}<&10K4P2u(E%F`GiK0q=8cU zO9xV5=Z_*tzYx|>3F}7K2DUz!qW(5tfO}sL-a9mcB;AgpS?PdkEa^ac5S6_1(E33l zNWXaX<>wE+{Q9$lhxb1}NXEjwGS`fWYq*=_Uog%xjrpc9ed5A|R8&(hk~EAEh$l(G(G`Ye@B_}H5m=L+`^J3` zLkLFuU>M#%^z%m){&S(9Ddc?J?uFlEFPBP$J1N5IovVf^BgS7oMf}CcR0j;TGHBjIytvIJG;!9e7aQ_veb2#v14PK$JjH(C%S1& zXv(ZBw<;95<+TREa~iR#gLkSqEk?{Uu~M}OsY6GsK@Ql&X%8HjRvi4+%=j48ml3fS zP1=bNZz<^~!^7?h&WyqpGP5)mnFs+RX_rGZcLaOOpTHPJWhzd?068d5jZ|J?qU0Do zB``Ja(MWM_sughf5 zjBT>ibuJc~#=wiWnLqGjoDZDHu!xF~77QfypExZ@BLqA}#tmiL^lIgx!cyH?zNERt zVlK>_KeJ}plIBv2*~<1>o3>WOw7jr+nYFrcxz*aVd0DLIueMZJmp4^gnLBsH$hYDL zoDn`y22sBs;E7vNmq&B;6+`eV=#Wd^9f%-VAQK;C+@zo^JVTdZ&M3*K%~+MulhL2y z%{ZQMD&t(nWdh2y8H5c)KB4+s3b;}cAxY?YcbJ?*)n`D@kV#dGG z$ntywsd6Z$g)E9MGs~JVlO=|wHugusN`N2Qma-Q<{II#v;bM~K99K>Fh#{-Br?*gB9IbIYHE?fkV?WTREqoz^Qr0F6FbX_xDCxI^XPs$HwTu^IWnl!cJr2~n%sC=1M%EVJd zBhy4-h0AaGbxIA{NE+36CIKy(v$WTaGBYVNC)1hf%JgJ*XYR<{pE;a4mN}Jq3B7gz zD#sVn)-?;Y3r!0h3u_jxT-dd+Z{gs=V+&6%JiBlP?T{wkU3ddMlOVj%PeYv;rXV}h zeiz&PQ49m=^SN&vYVsWU+g<*R$ej8UDfG|3ny5SCd9?SMTDk4qzwG)`x%7ALh`H#B z8E~7Ca@!E$w)yj0aUN0;T=<@NUaU8w+?juuJIVC~J<#*TA?F#K@0EWS_gMx5N5Kpc z7Dx1!oNb;zTY)pu*_HEW-@WHr-TYbX2kBbbo%yrx-E-~V&Y%5|Aq5)@IEuu+3cyk3 zdpIgJasfDMfy$RoIO@*>U|?3CEyz(cFHe&8A)cWJU^qVu z>RF=PE1nf+A)X~q6VHmX5YLiXh-c~3#QV~xiDyObrF)3p*ByKs;dJq=8+Q=TR=~F< zo%PJ0eV6VE@mlgU@mf5MXj@kFNk;sPtI4uIE>I3xK zU;;W0xs?H`m+}ilKLNCQFoXHqmAa*C2OH|WYf4Mkc>1?oA8ahiB zuW7Kz+UmV)OH0?1U$0rbc+Fr#>Dm<*%Zjz7i^QuiTP3id7na@vX#9h|EGu!xB!49J z3gH1zNFa(hrNyK={6xGLC;9)P1Q(Q%6%LQ!gUAIB_lY zIzo`-uchD>wGkwZlma(%3iJ+o3z6O{$Tp$Td4EO<@%dn58)kig0>1T&Z zL?{pgX_N)u3473g)zNjxb70qWNxS)AYwJOCd&%^!1Go3|_wPBaXPKY1o!Hr=`pKx` z3HP$HM#Y>?*|2n(d!6zGH?;TXdhYG?Ul6__d*UiI@j_;IAd%K7!6Jl*JcT753=-Ua zCkmsqhuGC)3q+wlpD*7%d5-X<(=i*%lDNZEgvnPEWcPz*1G(9Io5ifUldn- z-@&?nM6&UaJer{zcaaT&hEysqP}Q?7Ha%P!yvxChEr zJ~W)nK-J?FpG)N{xxxgB6c+4^NChxI!D_{{k(${4?jFyGjG#9S%|IsAdGpmUMR_u^ zd$h#>ayYF}R4sKaet7AI!%dCP4>U#X*1SBJ+g5K~_wv-sPdH0$<;%7_-{gL2PrG_o z>hw{^+FA?w%{p(}p;sEs12GZuXwF3kf>!nScWZ4;i)&k|m$e_> z>e_3!40sv`+w2yPw_QK)kA!O%=PexN)RQL$Lm4?Y`6AUTPs z6GsR*z)&s+W5cI|?C+Q>KDi1@g$;uF^r zuA`bA(`X%725$xPjRuRaG zA{~$HO6rYwSTd@ z+r4;MV@1=Nx;A@)SBKqE-}fC$MPo%Ns@)f}QA?g!k>|8`7g*gb&WD`^B@HE2^-JrP zRIe%R+~d0K=j-cFA8O03?=EZVs3^)zu~uE)_XP|b*)Xvj z=@MpT(W+7+vQ+#l1xP4CX(yd9ln+B@izU~JFb9H{1~rUGSzGgwxBAz=`C^-A_}9H# zF1^rFQNMSht8-%iiWPf*+P!vSZ^QZNGloNKl&#HW?r5_uD&4u$QO1vKdh>W|cx zQzx!DucO5(fe)46D$@uo;ZU{?0~~T95XHj;CS@Kr((>Se`kJtcyCv9A0#d(IhnRWt zr`Lz?Xn(75Z@b;rwzv7M_H~XQboOpp_X7vJuKD?UmgmjmEfo#Y^6ZbMh` zje*#3_#2SnOjd#+dCvX_RWJ<&)ih!nHpD+QuOtyd-ObX0ls$gmx>E%(ruk?r=ouwmL5~yHkT_pk) z&^o}BP;E-0+JGnZVFpscjO-S@!xn~Wa=`RNh;eSf8>qRbx2lc*g0 zn@XOh%g4yzwM3b_0evRsOWoNx9@%G#*-eMpAoDK-6SY}MSMzfUBBSy7gE`+ z+>yBr7~Gw!(1irWroh8d$fg7%Wr%)($TC2RDh!Q}<;tfkQmCSZke6XI^f|Aar$-rqZgvQN=7 z<$s9&mAG;t*q`Qz6P1WYi+M!3<;~J=O3J*Xyqr8|o-5Cj*PXW`Z-3r!-dNsL-X&Q4 z{^;;L0>_ay4&M~K1DV=PQ>G)cCUa$GS7u-4VCJ#RlbL5TXW*2YcsKI~JXih{Z6^N2 zpQa@yh5sS?fqd|J@Tk!`)bn`JXbNMCOstrRoDBDmTWq0#yJ*0ZHtK}fK@o4|t zlMU@C80}boU@14`Zbfk)=|skS#7}a+0%9|x7gXw%3?27N%T+J{3u&v@nPgMnQE^KR3%|N@( z#-hrD3nwM4@PLP*l8~mZiQVA?C@z}c$=Ji+?lvoSQW82h3!Ur>o7op=XV_7pg&p0( z9v8aitdLxgNc34^|E4Ggc5EP=MQj0rfHrZWO=agBIdSDAx|F+u6*#6rXqfBX^2c{T zv(VrjNDEokLGLguwP@m^$Q{tAiCck2P5y>&a0^L-Q9Zmdz|k&|biQa&LRwIfX-k2P zkbr7PD(t+R(~)jc(3A*y1mzrRJ{2en;%L7UKx@QGkYA!EYaVRRYd67S@ex#yTkBaKYMiU6|PxW*Z&>~1<-sL z5;avbFU@x%p*fYXTdCbX9!8SPs5ndrio=kQU`TcWwWFo{i~9;nKNm*!>;rcc^($l~ zqDQO>JEza2=_^t*BT|%ip#2Ur(Cn4sshF%aScg6(A=c$u);MujagHosnx8~^l17uUd?gMDa|>}WwgV>Dm`$U zoBN1Ks_7vn{qh7#4D|DX+|z6ICcQ&nqhG1-()Z~H^~dxl^=I`nD8`<6SAPQ)+oa`N zadDQzn1!a*XrWE3F31}JVsL=1UdAPEdy`eY*dTm0nmhRQtD9@~|E&M5|IJsZ;tt>P zb&MTueY(TOcJ!Qhs#&-5Olhy(`|4MJgPQk-H`q;%HC9!1$Ikj~&y;T)A)H}>@*&*~ zVwuW7SR_%w$nplwpmw2W@Lehgr3S48V&)eexfCQX0EB`?M3kVNWs!Oc_{`F(v8}g( zM}#Tf)jxNQT`rvGEd9O6LDBXKHEh+~?A-5Y^_YkhFp5ZKWw4hBdC!HI^8)cgx zB!zhn9fQg1AH+-*;(XV@$73B=Ntv-BDeY2hnvOz>PtgOqhcKjZo+oSn=L-P*ig4OR z49W4a2xsG|sLTk~JwmCG>UG-9i@1yqBql7qct2>Dvrltj}E}Y(i}zf9Ln0v>?xg<(>_6;wJ@FXe^9swTGnX zf}{_e`$(MO)R*|@?RQZe(&Puo40Osgg3@{g(9U0yTMDM5((iu8AQgnf8sLN_J%^i` zpI%p5y6)-bro%l;yyogwdof;)acGmHwc508PRqg3FKJ8KMsCH6&QA=N2 zqSa`P+G1_B)~zM4Ezw@r!k9)KiyXd1QIFi4xaVHv6SGG7cCimX9mc4GXrr1*{j9l&CxO8B$uWxeaQttX;;fvKz z_OgcLu7Sa&O=DiS+dI~@ba0?6S@=!wlWsQQ`7PH@bab3RAdA2AA*AUv;+b}4G4n7i zntH2mh1IwCBO*lo%wiP*Q1$a+1MzsV%4delS>;0 za$n`q%F)Wn%8R6(?X}A5XlE;OTS(5I0dzG;rlyntNM*7Ozp+6k2cqR@P2p$csbnN* zHzYc{k@1&E$owEpoioJ0wIPNsBl7*v$Iy|G35Pi8wor~GU8wR^6Wf-SvCdc5kZgHW zxsr4Z3FfQylI({pF$UiV6O1s3kigEq5p7x0P|&-xWc7}6O}?hO#@uFbCs$Wj=o&U_ z7kqzt>l3yZp}C>K>Wc3*?Ch-H#Wt4LENk>eMd0lj5lYlu)UBVo;#~eQsPmKvziS`zy-2;f4^ zQ^`8Dw9-WPEfW>PZM7uzeaqys=lM3v+-2*oW|Hkbx6H(qSa;J5_`bSrFOZz~hpYPg z>idtAH2AuzKKAW(&BEe>MaX<_d}0Or%L0-M5BujGtjHuV6^SK0hxh@i?gLTuJq=MR z9}B2&C82s0jYFS4XxxC}J_!VdWKl}eiu9VrPS9vICXGW=qgkox()4KtHODk3HD@(5 zK-ClPYHk2}L$)I{hWAs*>^9PH_Fe`jg-gPT5_gxo1g&P_)ZWm!YgdfeXcpa%Zoj&s zw_{~1UWPNU?$Oqc63^PBPsWe^f&|j>W)ZOu9O+NIG$1FlCel!}0-)sipmjsz9MVi^ z!lT4461D}JMOO@1T?Vee)?`B|6@U7+8;>VQ5dv-m&ytf2GU5ef03+sG)uG~aWXtWh z@WPYVHxsO-C0EPZ5UtgSl1hTD0kq?wE@?e-mK+Hz{!aKrxZ3^Ux=}WU-Nb)!Tg6?| z&yiZwAYub*%3zM6Lq)KQ4=hGlq*8=7Bxfk3&p4fwG{gByt|+RI0RdPDBQyj8eW*1^ z3|mM;cp;HW^x*ZVEYwU0I)qa&q#l!$W8OuH_ZDTMQOxZ(=8+=5wD)H-v@~<~VPe@a z&+t=xER8=fqT3iw4^uHwTqx*Gp!n+EGyK$(zpC>rud?lKT7z#b%c}~+Z{I%0y?rUy zk?Sq}mDQR1jIcsFaO6D$Hk$A5!Xu|bf6vNEY$I`s)ZGqEW=TJnZ~h4lX1@|n3wW0U z%!{ONeuv!{!yH9FY7;nyMD$2p9yOxaf?)uU)TI&2p*eLJX-+-ORr>gO* z>yNNCY+d8mUj;vjZ{#5EHOyyA_mb15bisSUxq%MCy!dlim}>ays|K{&Y5eM|M)o(t zxfNf3-5^YfpO6fckCB%`c1t88J5)hR)1n7bJdhqxu}Ul~QnU*PFl0OYv@p;BooVw# zEI9E5lYcO>&I?1>$M{G8BdJefcHm`Xeq`d&VTf%Ywlsg9g)Hm)EyQ%i> zZLGf+f2Dw?4?!c(zfr`XyycIi(J>vGMrS-`QaQPkA|d4C%7wp77YdfZ_|^~bJCsZu5?d&clwU> z{prK$W9d`rm(Ya(#3cyIOtZ|)TC>UQFxQw@n!C(>=0Wo@^GWkr^9-p7dDnac6(Lvw zNx7hZz5b`>(}Ggcko5VNmn4fUQ`8$@NnOpkhS3|>~$nNfz!2!KIxckfuv}X zVH6oHhURI19qc}=S2QxMr7mzGdijqX6_WI#yi5+z+?qslko!W4iNOMJ{D^@U(Gy9* zj5<@TwxM+mi9plukmWx!H}*BY`R3d^!p}|kcKI?l`@!;oR%h?W>F1>{L|!r6XZ!{r z5i*A-0&EN>8i63Gk4yDxXx|Oi>9iLO>xWQHe;Y znKPIJvw%n^^*hj^vo*>}QzL6mobonMF9u z2Q$y%=)n6aNASK<9$JCuSMqqNk_F=@?~NOw@5+>YMPGYK_bBuT$>O#%$Hno6!~o|7 zd@@bxk7G%uTO0+Gu*s1QGx2`(EN$Mcj82NqiFQW2qCL^w(L18|M-NAjMNdUvBF(#v zkZcqbr)$$q>5lZ8^p)vd>3!*g>BrJfrk_opA&phuO}_!TN0br#T!^;kA_A=P6%j7B ziI$*f?vco+B2k@~sKpoRgd<6HG+hd-V=S^46*&x}F^k*1EkoyR{R_U|y2guF_X@W- zj*Ys*=!Vv`et$u~?fg)Sx4kT8jado3gmJA6!a264VIHtx|G04VZzxW|)j%oa zY7w8{-5BQ^|KmUAAqkZdcuR8|@U0g9*VlvoS2*!R`w>zC)?f-e!r13XaMpY@o+SBV zLp;Kge4xuuS5rd0At4M2K8WKt z?TFhSHyk$>Hx+jY{UZrR)=>8>qz!;Gyrl5c;zhGki#oDfLeMWdGC<$Q$=bd-K^l@= zvM$psC`UsQ&PrYh)FgS{`*h`w1}ZD6^TAlKq`bPA%qBN z@fAV{_&$S#z~rC;fDK>wq1_!c=PKoN4OhnPT!NV8x|XvCp^t>WX?l>RNP0ZlR?DGIW0Kj=bojX7F`7f{tkaPZ_tQ8U&wOQbJ@^-JK@E-0(9TCa9$&ARs&%AI{ zL%FX2^<+bIj%6rQD%f02t%TJY`E5D2^Twk@yw*veC zLk^y^T2X;X*+O@fDM0`h^1_fnG>fb@ANo#)5!@U$8%9M|SW;L{m@~{3<_YT#+Yz=u zY&dKzY%1(h7@km|A{BOU&;0o}3i#4hJ_k~psVmlk>@R2{qC@2rFN@xqY%1Q6w0qYl zNrCpg=d=IB_1y5>rA}^%@EfbG0RP#n!vE~(?CR*~>g?b;thT~}fFh9j%vK>y64D|p6=(Kn@j%IiSBP5&)b`C&fA-)OSq?=h@3Y< z@P;wL^Fh0n)E|lVX=wcsse%))2ST~$KE%!y&j;;Xu>yqFF+D`@FWraMfGEgw#q-j5 zXcY*pR}#-l_ZQET`-|r#JDR>{iQZqdmxK2Y)5>rEeb5?a(Noq#f(sIL*4bSSV%jHX-AB|umZg*Oq+R~&_t5jOm!1Q~q zbw{_?$0WqWC93ug3|Tx4rC2p_c4XG#EF`MZdJ;P8;t;Be=gF+a^Ac6X^W;9_d3qm$ z!U02IZ9$&@nI1qAqzF--Bus;RFlbKr(894@5Db?ZG0pb?;wJmifCg@1~~)$hqsf(aWv4!vd&wd ziXx4g8IWSQM^IZLVGx}7rA*09$lt^a)A2^(hzQhCK2WfL321|L zH>qS`cPl|f4s}qG^sNd|Qs8NAD$8t<>ON(&`QIrs7|Oo$|FHKia8cfOzVLGyV8CIR z;Wmt11P2^O#E~F3??D4-EoE7XV2Gi{Dj1Eiiby%E>#>TX)g(>oB~24+S#O&+#Tlw- zlBTwAqn>Uyx}GG*G(GurQ%yD5Zca{4-@FMtdB4Bk|M?G$F$w9OJ-eUJyWI`LJU%?n z|ML6)F5lm6n#}*CRL}CdI@+uE(UF|;3u6^eNQuXy3tNGqF)1r0G=>~vnIH*GfYQ}A z_&6KQM>_5axW89Kbz;q9Xho?2I~K){2tQhlq+VoPJIVjE*u#%_w;5xXz;NbITD7h?(OioF)gjxl-# z9!}NUD0YB2w?5SKP>DD1jL)&W`z+SwtX>n2KSdTj z&I5>zxX-7bB`62aqW{xwSR222Am+3#yUKw>Q|KtEx`F35)qjc(8{`8iH zHsgP)@tDIWSCvQfmn_?X@s=pt5iHPOh8=mMztq$N2<@Cmp_UVGaz1VBME6AB#EOXv zCpJ%PpV&1qF!A8T;}g$Jyh!=9fU%}9b~Yt-%A_f!Q>vykPFXo+)07=k_Dwl5<Ck;$q2r6DPel(CmeG0>8+ZT?y5OtQxYfsitHS z1)wM0;Kqs8q_m{`r1GSiq^6{{q|He?lMW;uOBzhNfP5G%G7f4C64Hv1qn1-rCbGoQ z_z-d|RXvyEoaw%+@X_VQ{X_mT--eY}8_FszTgnPEOI&mP3;Sx{^ZPt`yZ+!S!nygx z(0;21qdcIXM?+b$1QTSLqaJuY@h1E0V-wwpzQl^eg^A6H?TKB9fy9G}#}m&aUL=1# zOnr8F2D1~XE3M!!oMH!ZN3@~Nr3b9q2LK38CesYZ+xI$@1(Mvim->ZS+V=gnQ((q6e(-*eafzqWVZkzF1ax1w@U z_v-TEmYT($5^zPxN_9I_O5O> z-dvj8x@f}u^4zscwHtS<6N?w!mr?qaX+{C%orDpcWaKwaQvTMgVmtATet`-gstr=0h?BpD#Q1~H|)ZW?gPhP7!}BqEbxgtPtLfLQo$sM#Rd< zid&%!%>|YXEZI z{O0I2GM8oO-f|qs)Or3zeYO8Q%{MVG@V2i=wT@mZbX7M@I?CaQvobYdq<(rLU^f5B ztXT4Pd~sBh=|;Zgv{K|+F$#dgTJob_)kz-~8S7a{U;3wBqJAeOmx85<#UPqs8Gk5r zE1eU0#`z1!tVq2(3_g}gih4hjYpCxt(AqMJjz5c$sF@7LesnQ{qP0(8xp4PH2egWv=xX0T0Sop9vDIie z9g&ZC3DCnyCk2!^Z!%aH>vTJP&I;#3XS1{2+2ss44?2%K&p0mIt zh9qto9fT4+K@B(|c2bF}kTU^Mt|&oGj1)R0qFcemC_PMcRwO7Z2VH|+p7re) zwi-=Gj;O~1FLae~e?`OK4vTww|0%RmTh^gfp~>P_G+A^R5V0;bjdZT%%36JaUMu{< z&7{66Lb;g^YcKaKAi zhJv2cT!-LPR0}NBY0585-UiO)0>4GSD*$|N1XWi8ovsZ2IGICaF_!LE06fQDR}Qu3 zM;pXJHJKaBW$?$zDGc#{aReAF6eO@f8oSC|RL_V597#E+h;L3}85QV#U)Zto z;o7dc-qtb^=*_|3zOL%i>YqkOeCwAFrFtA4g(VNwbr)~>!Ltu?$oC+Jfmc1?KV=+< z2>u402>%MzqgV<$q3Rb9>YOV;oi7rh&AdzXX(1kP@*qoruEzRLT5*o3xMM7(z0n~| zB4Qa!t7^!c@#X=HK&K;0fUzMZttxpmPi8V~mFdd#W|n1EXD-QHmD!ouo4G&pXy)n6 z^Hi_k)y(UtSAfcn9L@(>mqw^q+6`2o1CXDVmX@DZo>r6Al-8EEIc;a!fwW_3gJ~B~ zrY>l+n|^xU3Xyxju_fW|283?tM=Cd;d|+QsAkedKb$|DQ-39eq>YwQBd;%X_HFI0d zea4;9dyTov@7gl3z8+OGw-~mq6{~h*rA6FBDOOtGNB<26pX%3eg&E5)a9z}$`ETUl zGVksR-+guT-BD5DyRV{d45CyJHnWbA@l<6?$U(N@!0Y-=RFenZsJnHaUZF44oAq|R zOAqJ=_2c>({US=v2cua%B;72HU}!s{noPa9r;eZP> zQxsq-R@vG_6(-BNo`)y6YGm_eHCP1GbM9|`L+0nSuVjjhxR2f@D^yPq{g~0efGl1_ zJ{2nql>Zf~w2`i%$D#Q)DKbl^S$NX8gG-g&AZvV(|f>s%sc44fLc!!6f&Lp!*thl@AR_i)zgVu32W^lh5jlVrZlLp}6LHYFd}_zK9Me+drd0%1~ry z6wobIk$IYNxZDxBV^aB6u1~YT*0vIM=I1j%}A)Y`}I+0 zVk>;alRzcJ!*KmJ&wexFZmNDG?tYVQX1Vxs^vk%K$K$5E38TSCEIynp>Yy=MxjGU- zLzO0hi3{q38wXk>enK9>RW|k>8i{QBQ&$d9 z>i;>NHGHJj~od_mz~~_t4Ur9saZG{D6PWtl6!V9#3WK?Cuuz zV#7B-w|RzTE<-d}bW^|6q80e_!Nm-$bs^R|Lz$w+lI}be9yR0u;hY!BToj#c7W{-t z2ACD@R{RbF4`HknC!Bu!IAIDBrlZe6!b1Iv2#t&gUXqcSyuR!i0~5fxCnFXX=44*z z$*#%X$z_wPCoh@2YI5h~-pTtXADw)9^7+Yhh$mm43^8PhnM~3VZ3f0%lQ`K);lmK9 z6vP6@w~MSu%J>%UeR1v)H3sbyfJL#H-b#QwKv+xR*;~7soZWTScrK!^iGE|q6|p@B zI}oF{c64(S^E7OTCRrtX7pANdm9AJ;Rq;tysXJ*81p5t#>LIE~z^E35xa+W0uN3{pwKH1L8irIM(`0Y+4Vu7K(KQD0 zj3Rlx&|;s5Btt#Pf!E`1A|4TPI?f&Ei>ruR7}p%v9@iBYh&vc}Jnl@~MFcIPlN7-X zVWGvuD}`2soC+J}`O#UJ3QY2`rds#A9b*oShc7xpE&*E_$-i zb|n@RjBZ0S^c%B1%`#0kq|s6wyA#*y=lU9M6yFroyp%#IwJlq zsE+wNOPbf%yOz)V-IO%YFwpUZB~1-e^oXQ+jVW5(Eh$>uO%yHeW{NhSO%yHemJ}`S zCb=Z;Hsum(r=thGd3Ut(k9e91h%OP*jAM2v^I7=|M|l!&bXX{#8L+pXd86Ou z_xj8H)&3>^RsK$YuYbS)sQc5Vx_h53WpQeEJ5@Xe$f;m}U{0Yt?(x7EJ z{8&2EsOf5A>sj?s<(T@@Am$Ra89sjc3)|F0P#_#TBc5WDAHIYgUKqaK0r>_ej>XSf zx=T5TJQZsH3m*gW60k8**ce0(jo4SQ9902(39%~(YXQ88pTO&IC6$;O6b9D7B}Zc> zCci{%n)FK{0+kH$oJ-2K4 za^UP)^n%}o-z+ujhkRPgaG(C#`pcIu4-cYm<454xG*8YaPFH?1l0!>9#~{%ddF1Xr zHh$?i)|~J&^-|C^s+ms&C>z1-v#HjFzV_}O^gfp^(Y(pdC$`#H3 z5C1@hp)d-mT@Jd7cCh$_htQl$3Sx-+5Iqd5GnDvcW|K%f2bLqX(ZQ&IV7RPQ4S<@R zr*d++&=jOci4R#s!I}uQfqO5IffzY!Jeenc>wM*G$>ptwXN;~uG+_kO%1LvBCZ2QD zmS>?dnGqu-U&M|>j-bvm;x;IM8++@3#=MGB)d7)?L+1@xCI|@xt4Q{rOqV(H9EC60 zgG_?v3Gz`RHmw&U z6`E*+C!gquic&-~Uj+Iz$&8hg@dG@5-Uc}{Mku5l%Z51po*UJhOTY2y9$8SFVI84vG3h%6VW~n?Kw11vh$2aZjywfAGJK#42^Rpn zse+s(wib$J8wLk}3pLzkkQ{lUwnX;qQWuCw6rkpZj5Fs_U`Df*|{d?8aI zj@o?Dm`FM?LQ%-;$jEs3lu<;`SHstlm&7webqrjm`&nP3ySdjfU8TF_RepB(-WVVV zuNXJQvk7&V&sICt7QTMV{mMC<^|RuBsy8O@SI%+liTVk`YFM(yD_;xQWCch7_mHJA zrcMw6!OJB>p|NQ2xgfIG=))>LV)^5wO(*+52l20EIMqm3b>`?BhJJ>295}vbwBozA z+@-A=zEHKL%19xJLedIc$EgHD7CU*QpuGT#I~gOQaILJUK#zgn1R4g+bI5!?HNga( z6<1u6zI)CMkY`s4a|&Zt(c&R$92q^qX#OCdOe|un;(_v^t%vw02Y=uM!bHyDp_3cG z7*kRAP`|Z*;Kq+yP<^k{oIq!%`rKVxm^Q<7AnJxoIuLvEmsopIUs==vC;AptKzE1t zTl7H+-)mAmKbcOdc(O#E#FNP~5l<7KR;bw+P=p0p#Btij=h(f)57!6-;oDAPgq+2TVC;zF6oSX{o z5EE|%N56Afr7v#y&A>VJ|Jl_;47{~sy0@~a$kOp~Ke8L&vSiB9!m`a)e$<}>ue%?y z#oPlbht;ek|H(u!a;RFVN$GgG&|f3M@guU7q{_NkzNvM z+uZXfpkZO^=(di}4=!Eskv)Oh7LIgyG<&!WpBs2>JUFz7O(fJr#3oAAgez;a9*0me z(2hX(#ob-uyQPgO>Tr{dDee|FrnviN`0n454uE%xyEldJ{%sgz>1Q5SOf)6Df8ZBr<&W*TR+UVRpDvF*h?#6XcoEx?Bw;@WrM(GD8gfb4H z=y(dBDjs(Sc+wrhKaj$w1pdJi^1+{+3mPnT^e-~ih&!Mk+#$KobbOgEWes};C1u7=z9@asdR3_@f`5fEh%GGj7aaI-HRhU49>VP zL)Lg5X`%p+AF09&;z6^b^2VRpSAqIQZ7XZ(uA->I)!gSO+j(v`k9Nqw{el$u&AqLW}NJ{P*=TM-7|k(U184b zrt-N<=M|>9%Nn|t*7Y}q#)xdN_(uH&vVu8S@%!TZI9djxlw zgbkS_Hzd3VlN=`tX`u^m6!JnBrWNKFmKW9(HWjuNZZ6zec%blD;b7r~!rLzNSlV*U zT;5Yj%lqy3myWSD*MjAD=0%HzO^mv0Y(2Wdr(=ZP&9h{ljuCnnXNk@uw0)d{5$#Fr%eTN3^)5nmZMpzt-fd8{H7a;d0=TohhRx&oTf z^&@~IhP47{3y@^c6VQzwjGU|%1|f(glO%^S%}@lv5!BB1h7jpjj6cBk=`jBA4_d2{ z($S&5vmV)lmqrkDXm4RhkTx`C(t7^vM( z0^ke1)5y9)#8?j75CX;L`uL2d63EZf#{wWp9FfFV%+_cd!VS~RmT6bipS6kZXl~Io zt!=%kh_-2;2oBG-1K|;+Ob-%|;3)Vg1=kyKo$v^j2YfflF>$xVBlvF1FT;1^I!O<{ zo8*|dTjCMo+0o&b&5F^2Ld3Q8PJ@IT)>X~l({!X^9jNBrTkgcU0z5p)s(2gp44FH`4q0YH(+s1P!5I?Y^Yot%7~*lC!AyvMTuWDzP)MH*tUB z(Zth<=Mnch@M_|91jmNK645q~mLd!^2#m-ePs<$Yq9;p0j(A$QlO8I7j%uz)Q-^+Z z4xH^R=7i-;`L7Ck6MgYmr)Xlz$92-CaQ7hJJtN|75hX($dFjaAY)N)Tlf^ib?25J zH`T&ORel&!>=dPnm>fcs5t{`*3*cCG6_3F1A_$vRZn@hL1stS9bW}w}VEqJ+;Mss^ z02hrh+&VAJ)FZNDH5c1ySI%Gb-|5Pni+tD)ZXf#dUvmMN?T^sj&`t`Owl}=mow&|5 z#-bXrB2SvDjjl7ImNM_;LlJj#jViwTsgb*(OYSCJ63-yb)6{^W?d&FP2UmsOIU;e# zB!`iOum>41Rne5mDDp!&`s_IQlyN26(T0F{2Zyr6Qjhz%3_uiu|N{>;C~70 z9#*Q^O&AdhhI>$=N7wbY%3sSqO45!Tgj}UJlw-r03XQW- z#U@UnyxS**`Ama5o`-8sD(UM@u(r<{QeE&_ORSncX*;D)L4^Pcly5YdeLI_8+SGNoyJ|xqQ>D5db@{>tt3aWQ-mu=DU(Aw zRn!sd%u6c=ESHuT4HP=AK>;1DNZ&A=8i3=Qy4;WK*1x=Zk{A`?A>)+UsP=<;TYzRk z{9d{JK^g5BD0~u(bcKuV5Y9E?x|jZXuv)=?@wRy7>$mN&8PuaW_7UeW z3OXb@Q3q161S_4aQcX25#}2tqr+pOFLEP+4Vh&(4{9~=RWlaB`PnXD(Kk^1l!A5O$(+L2IKU}r`Dqjpns$>zFJsf(X}+|I zw1sKSY3*rUX@RtZX~)yfq+LYlCHZ3}B626`(<#}p_sol>v}{V+wjg?kQ?tc>N906H z#Ve%J6rkXbxn5a6s@KB-4GqQX@_JDq%roLwQw11=$^MMySOQ`C6?+csCcNMktHd1N%oCZ;EUOV4WK~?3~pZ#DPRhnP_ z+y~nbtov2elfQ5I`KP{#67xc?GuFy9Pj&k~hvNymN>p!Pfk4&`3@a|mB}^$)2__Rt z`emdWMo>G@`_Dn-B|S&Dk#0*!w@2ikJZ9AHB2xOPp=etkH_|~(+tE?>MW6~Sk?{6# z|6`^}`NoKsNL3^V>kAwbr&&c5JP`jnW1F%4Q50QMgO)uXgHL?)wt0$)XV{drR0sl; zX`UOnePf{SqNX(1cw7t}WwEpoK|&EJ0ukeo3h-?^c#xnO)wBbr=;Gc>|jWjTehBJdw^)?jQ9>=r+lzXofGkxsk_nGrG6h|x)Dg3(1` zX2BT#pka-myC;VK2P3+cx-g0hO~mBO z5E-)@Fh2qZ;G~n8UlB^CT&j?hy=%A)GX&ct7yy4MG#F@+!f#B?`AqMri2&rFp9Y5` zP+yHME!c&ULj}XNU3y>F@L4)-YT@9ZamCX2tUArOWc+uEw2&N%x)uGS*#Qu5&F2Gw zWyYEUNgq?IB60vy#bej)bAzY?Y1WJA>oo96)-`+xDvD67Wr!xhCvH-e-8b9}UAfcT z`R;Oejl0R+=HBey=|12-<{orkaDyvC^m~{wd0gm4ROYGnEb*-Jbb5L{`#ncJr#)|Xjlq;4u^`+$xIU9 zv%=tx5H@-x)ndnih{#n*Qod!2-4e=-VaE(u=B^cYK`3e2DcQ(uUX(Q<=8 zq2$hjq_&fyAk;08-W{niBMR2LuNYSY>NBss8#uAoxT<>hp3wWA9~{1nqM&Dcy9}j^ zPIUp!5uNIP9~Jczl>4cLA3SH^fw4gDV%2;4ReY*I@Dyk-kb)K{6R4+WV_ha(@INBJ zNFHR8*o@)bUe+L96{-V+cVb@|ri093$WCun7a*d86da&+SSw+3W37^vRj6r7nFJ22 z2zDg(B!ZPhNPW3$B=4!zKTZrOSYYd5OkgZ5`UEctI-)2yfgEOujJ2ue0AVeGxg;Sa zJx3p0ZCoTX)VR1>eQ?cNzgwd|Xz3e%$fyLm_%m&NM~9KDzT1I~c=maYcusj< zq|^S2=Ne8sJ$sT##S!Zvw}%K9t-ez{W_M<4znP^IOQ80f)D_2R*Y)RSulvEWUCR!x zn>lmcLHzQAb+ZHB>ig&Set)>~k>2_3)p^=O%a3iTi+lQ+xVoNW%kMk3^=t8m569JS zIdNZA&+>V;;k?xId-j}9)qZ2EZ0*55-oVnpuzgf-KYeko|- z1|7suaLVCE#y*1J2pi@oqF|!eT&s4h{q?Wf)sFSXdurO24)tlH0jNWUmV~WdrXEIf`+pk#{b(7T zu6!L{cZ#7hdoWt5_zt)V=n6nflDR7px^{$9Hln$=RJ>`wGzwinC3b-Ty6}Ng?WBJn z8`yQ~NWfWGKY!Pq^?t`L-TTrfN-C9wO;xp*)b|W~Nyp0C1pVHjA0^bT>?l#+hh+{l z2=QclVadZH2dV)3o-3>hbZf8X5m#Wz!xG2+8Z7M-Y?%`#qEWF4T;&`Lw!eRjtI_6F z8!!>}R;zzA>gw4~&~bN^tv>PuVZjIaUbf)H+u%b=LN|iHLv4NZ3`E#ICs>psWVcaS zscceqDEpKn$|>bVg$Q4{rbx*eQQ$oyE-;&deIyK-hc9{fLg9eW?xS2$-l(#u>Zm1A ztD-uidZYG79gR92bv}xOXw>y694mwlCXjGWxRJoZIUy|}KcPIKCZQ>zEn#!Q&V&OA z#}Wn;E+oK_oRQ}ywJOWa35OH;8_WX}ye*rmlUq548#Si7l8RCC0-Z4l)Zd*x^BePL zwNy_}PM%)fGHd=fW~K)UTfeuW`;@Qz(QVrvE%%-3Uh%!wLiNhLZELEkR&Sp-_dBcc z`JK7T`g`kt5K!%g-zZe=fhX#B^_zKm$6$$#SEj4+B9;bGekdO?@VfUVrzFLC-Cm!! z!n@Gh>}~gUc>~^q-s9dg-it77f>~a!z@OzGQ*&eLnG;Nk;`Z_&_o7Svspza>`g|f( zWhQOuFqMZrluNBPNMNV)o}|0dz3FA?)#*#pSEYBR_onYpKbn3z{XF_~4!oLv9nCxu zu0pW_!K9pA;@$vWrqB*exq-qNaP?0~o031Jd`iufrYUVxHc#0(<-n9_hZytYGmz9Zd~=-2V^#*Rlj z_O8jRyR#sB!OE)P!aK{M36OuT8_2s|nIj}ba9j+<;0PAUYa=n&h+rq?=2*huIK~;k zBz6$E_;#_xu zI?%CGb%v&LKY7JQZ7^eOsG&BdQx{Bj#$dPuujk*)r&W*5cjx=^EAkiSH|MwKcjX82 z59S}wKa+nkpMASiQ>RXvS~|6AYU9+EQ#VcBF?HY6BU4XJeUbXTzcTe2TEvn3sxXO$ z~2%JXXSn)2H6Hs|fkBY5CI-m$#F zybF1jdjN%;EXMsY#jax9k0#+|#nr`2idPkP7WWqKFFsm)y7+uC)b&@3uj2;dx>=|= zFBRufoLZS)BdTmtwN)e9)WeT$-+p+){B^ap4^&OCEbuL^U%utg%5VH&!<;!Ae(;S=e{69VmsR=-8s__Q ze9LoEXAZl*^?Y^p^BolrG?$b&JwTF%c`kV4H06a*h7J9aDaA~II^)M?Wu#_I$|%jK z%4p13nXxHjN5;O4BN?YMUPO1lfmbrFp#>lXShMd|P#0@RlDa05H=Phh(d!!8(pckW zzD)7b;MCLJf|M(?(?_KefiLe6FoKSTkcz?_04Us_O4h_Rn^e6$X)D9-A5;NZXfoB<)n%i$Jjk zUP-$Ks4HC4L58oBOaU_*iRqyJl_}8{L0E|9pmx{D-qf|6nN4L4&p)|x#WR1{X3W%X z4jNZqT3fT>(M8qYSw3@P$E>CW8}bU=HcPZ;#q)>Sem!uYq@$zg&~v|TS-+=l0v=b~ zw5ai*uPvvQX3tU@bd;$)V%AN7M-y5Kg^Qu`x8h{JkMe#o9Hb@4g;4`ZE*xehzp#oX z`MC@x3o0?BruPG#9y2AeYoBseK_y_$2)~M!zla>xRMkIWTifG@b~P?-+q0|CGchMU zr6RL>tsY&wa`5Lv3-!|*A3pTG;dixn3;YT9$Jyb$(-j~1+!pY;Bqa-5+IRa7M{XE6 zFy-OjMCJ>mJ!*A`Y2VgX4iU+VRX<+8;i-1Nzx}BV8-CR8 zN86S)3no~Gf3D>$SY2DYdO?n+&9mTlZTI&-4BmHN@Wbyv_Tj*a6~T{&=hgzzccge&|T#RJ1``5oSV4%CyVnBYx>-6FLY#x6#okDN9Dy9B7! zz*XxFE3p)-HO-oDEw|QKo2+ft&DNdP1J+~KLF)x8X(WtPdb#v1_pLy8Qa#f+W%M6b z4^<=;FUVNxPj78)-0M|8)cr%3^`Gr6ywj1q$us-DkJs}mL6bOu{mW6FHY+lq05a0K zf=X%? zYcI1`+n3l^**opM_Wkyw_S5$BcA^FQbvr53mxAg1SDYwF#mjKqN4xY%2#BK9leCVM z!17sbE_8OR^*7v^x7(Lll$oAVlvlIn$;It+Dhw2M8~(xSPG9YYYAt0rwb1WeYm09N z-Qa($Tg8Ebcz+_)sR-#OCm4*;3G2lHjMicZIA&$a6MS;Fv8_+_cWz14``$BhUO^S2 z-~CG6dKGUWNwORCF+=%fDAPuL&b>Bt-wrC}l#d3nkeQk}DYG=QDzhL9hv(w zk7S<8e32?rypnm1DpKg>T#-UY6fD7xQ*Uyw+}Nq^slKTdQx{Hcp4vXOYieNX!Kufm zo|$?P+ETw4oy?&oL2%KYNURra6N)i{)M&)p1r-uUSv7F2z^s^j49J%9Ss;+`gd)rLnGhouwAuZ~~t zse4T8_Z(@h-O^O#@vQcw^{iOGWuv!dSy^pMQPOBJ=mi+l8Nv z*^>NDz6ov%`JL=e_9a&&FHCMuZcpw?4kRB;KAwCg`65{IpO4u}Mi>T{{_QZ^=cpCb z+JUXy9BLgt1Gm=iQ(va`Pc>VDYpEmD!mX!Q*Y&QX&QEo9z01qd3(B+*`*>9+DSvO) zP6LJt>Ke&3Bs6@iD+{PMYqa2$3&sR0RRp1&c>{4-J*jFZ@{O{w^p-t zVC|llX0{d_ZmZvRcTt4KNoRW9xknn*%WHnRwR%lU<%0SdD|+je_0&@OcUa>TN7-7w zI(&{lf}Bc)F7}eCi%mA|5;A9^kjI=rZYkPr6y(B^jZYmvX?*GUs_~8ESB~E_e#iKI zxR2qB-j1`+@I#`^Ip(BIKftG|IIAV^{U<40^KMMM*4A~Rqvl&tS}O>5oLs?8bx#q8w^Jhk1AE@*qcrKI&Y-OFA$ z_zjXy(7t{KE|7yPu+t&84fQ=kDiM2;GL2cILJYsz)+#b@B~V9DxQpIc;+Oy3#xY42l3o(MjfnKkhyI^0r!Ey|UKGxkVaq`0cJ(j`#@gzU6J@1o z2y!cPnVrl_`IHhS#2ROr(r3bc^Jql=sxRYejOl$rodi6Cyu=>m5B<4*DhHZN`3T6LJDCk0s$Zhk3-H%k za;SHF(8(WWqB=;FIf6t_BhHe9Xn51Iv7KSRNLXY*U4aibXt9$y6m;?j`Ps${cOZfT z^)Lsnj=#aJT&?5N#^;YOA73-RX?)xG&Et2DKQR8-_`&fPsFkZm91m4y(IcB;IjEe1 zlLaa90!4s0J5T$eUAN*f43l-IKZ6%)^6ovBOBE5(~z|WsNf?xYx#7 z*Q*cq?5gV6UvDh4^wG}JTK7>*w)zsUHOFM*Y>{t6(I$#PYiv4TG*o?iX>r4@{<}Mx{%W zPz9LFh_*|d#u}9e0;P^jSSF{-iRL`cYUdK?Drcv&*SX($)Op%@o>c2so!6mS2lM%U z9GsIyvnu^+rHW%$II#6`1Xt3x)k<1b!jDn)$Mr(5tr1@opsN`d>8;koY2djz=m@kmq;0@@%lg9?|L$QMWYafZmX6loqC@W$xDMplvkhw1Cj7OnD2%JIkLojPR zF=(`Gqd0BMJ;#;f%_+;N&RLSPDyK82H)ns&(VWvc=W}T8IoES|?m)jmZ+Da;7KIA1 zN8_7Ea7A-PN}PJ)nwG_jThV41>Rj%G+=mk^<9+`%b2vsSU0anDWdoH$K@o-cFX013aD(ueQJA4jpNa_Q zk!lu-h7&V@rw{q9$xN+1sgo4Px)({;` zVdBuQBaVQKPGCX@FTwG}fp?L2w`7i%SVHv9l<$F6h=w9K1{F=SCaA;Db`RHgs~xVQ zx(fBVcRGsJKc{+rr&fO4Vk}nY8t0-?`;GT6KVD%$(xC8!kwx%-B*#VMM&fNrs2QC^ z5`n@dp~xXaI_{E?@DXbeAtNIKGb4C{R0Mbt*}F=LoLPa!EIl($nDtKn0M~GO5ylUz z9*an}J=OA2_mmkQJw$8teYI?tTK0Y7ArBcJm1)r{hWBZ|)5?dR(-!c57ltQjt5*zv zgcVE&*E;}Bb&m4;P+>eGC_-g6WK{K+;Z(*VYYW;c>|V&SaR_>vaFhE?$4+oh@J*_4oe;>G4IfL={X*& z=agzyAl7@}s^2YVgojYKLB|X{oov`o}z&I;kjMI@1GDQ zYCd|K5*?(4xFH((>MCV33b_a!m$~h~T@K9Kpj$fmNy=_B(p`OnD=$FXLMSmBlpvi~ zoXH3^C88^fWKz6DVayw*J_4grLYC`?GmQq?xFA$2vP90 zF(piyF-o_GcLa*|hyK9Nx{$#6vq0C=$jB{O+Pz@)?go#iVfSi6&W`=!0W)=zVY9os zmlliM%^!YK%_9zo|Cw6nzvt{5wh|$C5>eTL_as8PM8w90ZN0GqJV7Vt$3sg|X}tx` zQ(9KW-kWoV)w{Q`wKd(pG-E+=QiaiaSglhRL0Uh!e2%9x*;!e*M}J}Hl0NM}GH(9= zmvLj9vrJ$+=xc~o=h_Ptu^3XvQG&OMOjD@fRJ>dUMo1FW#C13!N2Ju=Ac_aAGih56Q9)56n0N8UnuIY3QKPcOo(i`LOfs8X+L|Va!BhRN?E+?zm@(bI7 z(78EL3*pg}SPo^I3}60F-Lp%z1cq7yszt2jM{m<7kJjAJ1ZAD5C=rUWii#T8Rr0pj zkWUOtz>cQ^cEPxisYpX(^;QmmO)U8(VyC73IVuAXCY}ugERv(cCm9A99F4NOTW&X{ zo6`0JFC~smRX=P%j5e8w5l|pZHBOYTRDsz!`jP$|Pk{~bHxX$72Y$Re-WOjHzc9Wz zzCFGxJ`jH}{&@VE_=^ZfxXt=XC=u0`(aoO1hQ5vavtc9)0+%Ruxt7^&PEueS0aA6ej;|7b;7<9e39nXtkERs_*{~2{!2_pnej~UfL#M+4d3RLHFvi%vi zC{E1Db~|k5S`^@n}C`rtQOV zg|Z7Q&)DFg;*S_TGDV=_J5n8!9HovbN26n|E-s*=W%l_i@>c9iTZIZ|?} zY(PbXT~c&u9Yor05anj_d_T@)YNns?xKqZ<4RnSLJK;t@Lg3?eOjM9r2y=z38LG_FeOVy2QrKa-hF1wKf=d-FlO= zEn=;1tIt|tU1)8#wp+Wb0qa5QaqAiDMZ`B!w}N!j;*3a=kLa~qj(sYpeVQ6ghr66{ zGH_DerF-=U28EgfXt*|THTMRGqgr#*a`SV` zb8B*&a@%q@=kClskb5k5F!ut&QBS66A5dmc&{s}*8aV4_WkXJXl6y8PCysYV;MAMTsG;mOv*IYosNd=izA}8yM{ml)s-m=+9nW+uf4ZkeMSCLC z+k}Wy>b5&cIgY9ZLPLYPI;Q@*!7L{w03jN5Y}v$AK+VnuBGp87nGA$ku{>2$fe*PU zbvS6xp|opaO^7-(4Sq0=QB~0>%lGzm(axVW9O~r-+uJIuR`#@g;%9coNEgtBVRsGUJR7;wBUukJK+V&exij0mbFst~Qz+ zwltK9r$9Di$0^3vtc`}MwL_P|r-WAuPx^DbQu0j_@V%{9N-sf8{IFAs^2(eZf8#uF z*e8`!vLHu2@=ixw(-Ut#a&G9d@JLxEZv6G34H?>5@X{fiVCq*!l`gu4E^^zr>d-{) zECFm8Nb}fE5F)WN=>%ooK!OaoVrE)qer9=QO=eSOTju7>otXzRk7W*KUO*NN7LdZk zMkS3>R1UluoK1y^1LRlnFfy1m#VvZ)wSbNd_TRW4sOx}jKy=p+VR?LCnH0LNNy zL5?=La>+Ql1PTkIZ)}IiRpHboEiqrXDal)d2t3HvEBlxW+i5F0C=pB0|LL zY{9-wRepsTWs4axBOQ1>`(`$qE!pmDUv@?I!tCbk_Ux|gK=#4x|3Jbw0c zd4)=UFwb(PikVemy$T!%Tzf4dw?_FnM`8k_e@uF1c-_X>G_ zExLdBP&l4cdqu7$k|4fp-8eZdJ{|Il#3p#(C5AG_x^dT*{~-{3?~g(4EhAHXYd8~m z8ROOKqeu9qy;pKr%&O#_4ySc)x!p=)*2hsJ&+Usk-H}fiAe9&|Bl?f5zaH# zdTXD%n_%Ta89{AAGO}`Hy){lY?jwza`hbXB!CZ8GM>skhLC6)%lMq-eU_;v_=}3lh zGv=(pSW0{m`a{IoVE=*~Hrv{tN+Cq=TqqRTu5531S$1{ylI&I4o!PzF`?HT`pUyrH zVKwk-_H{@!L<(nro_2{~F?^IdPkk(~^LLrm}wx833@pvs1GtWtV1GWjAK8%-)o}BYR)=k?d31 zFVehU$-ahpi&bSKM=oSUHiy)qgNb3c8tU&1Tur}$3Omrd($muO)63Iq(woxT(l@8? zOh1r*EPXKj0?P1^ij5L1SpGa$o;R;7uR3o@-m1LLyxzS1c}Me3=bfjf%CF{KMo=`ZNVH2RUIP%rC~nYaZwCXzYOV6ezk3>o*8h;RfU zM&wO-U~PNk@s0r5vhgpRp#rNK_R_#r?FP_F(59BAuDT% z_Z_2A{i*SlM&q4#hZ>E=TG6r-7cV}2FKC4-vz-O4q${0*Rv0alw8FLXQ%Qio_Knhx^3H^ROO*Am?!Ud zb*ZgtU3a%(;J<$35b2}j@xhG7p;O(MJa}?`ECu8j$-KVMgV$x$sB@vr;VcYAXmEd) z2T#5}LRLwV{Y(!YF}&M&@Sr5U))Tm^<3RoHp%#tWHFS^SVgK9wcXG@T#wnW_@?RvM zVPY_`QnDB${v0o!IZ)nqA_2lx zaF)mt`d9Jd$*GL;3w#ML-fs^-^UNXR0K9nZEpNWr(hkztz5S8z^}+(V`PByF*+vYV z$4>~84!;L^@shb;itcNVv(B3S$%83BH7igoO#s3GE4834w%z3C9!8 zBwR#o9&(OEc=6=;BUA)g)`q22*s_n}a?LR5krD{z2;x?y9}f)zWx9r*9taFSaOm+L z{pf)4N%Nv zaLuVMbvSw8F(^>2rixXw^^~mZ@c3ZDu%WrOpQv9@*|LT*x7P-@*4&SB4t1yYlHPC5 z(2kl7($AK{UgH?58AuM25kH&rCerNS^>(_QK4*nV!uX}lVB+&nwAptYq@k1FyhscurKnd%&2{^ zbEvJzMI$waHJ490(v}_xo0j*M-l48Ap6J-Fc38IuzSld{LLE@F-L(yB zRU>kT829D;=zj)3il&TZXekew9Z9{Ygp%(kGbZ2BkC+J6f6{`}8^bA}@oxmJDh2Qb zQ}}}`uAyA0hds5j=o+&esWV1E5xLTN#?6=^Y^hm&lmomWXw4=4tsf|J7M-!I8>qJq zEio%CD?h6|t0t={t1WAD*3PU0S;w*lvo2(T5`kY%f@-HzAA9O+|GBBh>kpfa-9Mds zd>H+ipY1R%Q}*$2I?$r5w6)gQ_1ZO=ilyI>*jyj=wN(F!*A3}tqinx02f00lp7rNo z=wr3zZjYhMB2Y*m-kmV``0d%pW2aKIX!Y>kQ*~e;Y@LOx!JOERj55)$5FGLjZ?{R$gU{4zq~LZZ)Ty_&zEPcAdH4HbYbODzw~=Wuk@fA&DG6u3`H_F^@||N zmq}}%eb^+_ce_sKm^0Azk-p}dA3f@+bqM+@kLp$~vxbRVC{VpkZoz%Yxp#>UE;$F; zh3ME5Oi(!oA%PPy#U@4!j=dycdBQ5DfMXWkG>}hsE7xwEJ&QK#%iD818qu!Tg*NKX zV_^Sp%Pnon!Lcc4xE~ywx;cFxS`_J0IaH}VlxoWWf61W;p@G*kZgS3kY=%3-mr;?i zFrzu6J)+gd|oGkS0jw>QQeR z$-==ncMf{+AFuJmO}rc zRY=4-JQYgh=rGeZYt7k1l_nWRI;VQps8ZcR>x9&TQcaOB>5y6({{BoqRmqKzTEH^GQtPB0 zX9okqyj03ez73H^h^q)KhI=H^PDe@$K~DC$#TH3ODN-N43(5K%sOQk9VJ!x9R#n*&q{?e=$|F_~sW zT`?|;{vzgkP!;G)yhqOX3*Tc|W?Tju5w1ycnCia(38aj(*P#?pFGo>BhOW<#ypH+5 z%=aUaVB~&Lg@Lc%HTt^nQ1W#YNEo?abcN*W)e+a(qsiB&MqI}i%2>d7YVm6{jnm98 z7o*4Dje0tJ3;6E87t;7k z>WW#;*YCtic7c+w;W@t?)y?xx#e%EnaP=+j&nfQaS}@_ep`|G(9~?dn;%*v?xSMKU zV3*8i)5yi$xQ?rY*bPS0NM=Ngbf zh5uQqz^7160Pz-?Y9f#pioj89=+ugQWkfuN(x~Ft!0Wb~-0VKq=C=846}E-8W?Q?h z%NDR5v>msdv0Wr3XG{T+KoSshicmmUEeM&y&k3g8#>`uus)(q*h%#*vhh3s|%;ULF zF7d)R6}36Sov0GyNQFoHP&?UdW2uz@WEzErFg!31j}AFdoCN&gj*{t~i7Cl04w-a1 zq7Y%D8Yg}oI5!Yb7j4|w)k%F1p{;AGVQ9KBq(!-h8uX`!f6cRb5HY4tC+TMTJyB_Xl7Ci8U!UgAm_vKJivbNG7oC9(*<%2)7$`Hub;>^uc|0g*59P$2~Y%@>zQ zj6O9brI^8bQAQ;NFqO{1b+~>b>anRtI`iN%ew{j2}*0BZL? zP`kX9e%7wJzNWTz-MZS=*4lM)ufq8K^`&L_tnjrir=PwGRinSb zt+)Q=I)0F5-u%|5dUSFu~Iw<48^3f^WYOP(C^IvMv09u&4uAPU53%3 zIt!LLnidHhB?|wkf&dR2uc!sXyXmLaCy7yOT)%$(a3D;QM3s4#7VSK^P6~9SkdvMp zidD(%j^>R6B$14#=ONBp$5Ntmh6}}}X#MamOUvQcx=*Ye=u&;SbZ?> z5jEkF^^eWpIsY;BSL-n|1xdk{MsPO^_*;rn6tc!5lSe35d~zhK37;u|Cei8#P{K}H zRYEn;CX*^?i?dick*Y)s;$7otqKXjZbv!AB=234`bTB7ToHJ0BS8*3(9OMEmm zZq%9s6vLrJ@rCio~`et>~!sNz|M_Oyu2j5Z;QGI|<-umRKr9tljRb+*fSjY+mN?$02 z$uM3;l@9Vqa@#H%S&j?D?TVqUwYN;nu?F(v0%b~8jj73_WZh!FN zfu^4?Rx8!JYJXO>`A`eJO`%X%+W8n|o~c_R!U1}vX%JxNA#bAO&WFXD24_8>T@WN3 z!r1p1l18x6$Q`V=488k+R{Ego$3{b%Xf^2id|AkPHHKxq6O@6AH32kaLRV-&ZLa9u zxWgS%&DyR)qA;bwM+$l;ynWBEUD3Ng#0llU;ipC^8R(LRhl;&tJrsWsQe$wQhy_rj zsk%qIJbY%Cb~!LKpGHLQ!-&M+LZ=vY?!@29L6QT@wF~3?_^kdsy_M#Ikwz(rBO?vz zF~VgH1^}tMXO~tz?2q0(R22}j=ef~eL|OTa>wb(b{00>AyQe`$yp-1luRVkH!Fxw$iLug?7;TVGg5V5ixzUrN%%uV~Q-CZcOi@g` z;68sIqY~@IYaOFZ#2ml~#d`743a=MYr4A~fttG70vW!+7J3KTWODE}o$CGyJN{Es1 z-@?SrN#Ql*HK9e6Bf#H|Q_95O5&{Z835bs>vjAqM8L?m(84xcV4U32q!{SHN(8Lz; z==8I^Tr@&(e(F!I6CF~&&yXdGO5!Ln0BS%;pltfjvg~7+mLDJ1cLxH-cXy%Dx29YH zJzd7==s38?mx2S*Tlq;{w+ zSFq_rhW=f3W8fWHZ_J2ye4Mh-lm|MGpJak?PsLzbfCXe-9Qj6`jBKSK)-DtmgLRZI zrlM{+AP!>#rl)rBk<@C?Ncl2~af!9$J%}15i||qndyRgpcxgP9Dx-`|L4+(HG1W){ z(CK`IqNAXB;l=dIOpSNGC7|!dt99+;K$H@AhbJxQOLOt+&J4xav09;6D?YpO_u3X=Nzd4v&)X zT9YRWj2_qk5=kN%Xyiu9^~8&@p2Q~D^6-?1t)rLfTHs?v=c(=@dZn|(Cw96xv7uPP z*Fc3Q$AMlUGH@t}Ucn7xX%Ph*q7zL^sbX5h2|Vyp=$f$`JQXXtNA1u9!%E=BcZo(# zUOY}IF=-TZL8mQDE}|bQ-o+duM!qPTj)Qy!;c7W4T8*)r=n?!~-Kf1h{H%7@Pz}su zoNaR+w03ux@4*YOSLpw%hv^ZC#GHVQm*_m3*chBnf-#w&@y{zDyWyL72_9*D{9PP8 zo(%IhyqP56IAvW1P15+{>&B$bPXfPXv=M%e@>y%^?95$rH?PjihSaaV9=*m#1 zv71bGOrnOl)5to77YJ@;5Vm=wcnaX8m;#F@93S3pC*nB7|K&KZV39yfB&}$ThKr2v z5`B?;#pr2`riP;Iw41Ru;!f~I()j`FG1)X8&W|0Dfb?hzD#|ILhY$NfM>P0%EE_-8 zh`W+@V9MNa(34r#lY$R6WIoo+dmlNObnLNA7zXV!9&H|emds~%=^C%QdH5yIVg5Z% zSv<1GLItAr$FUUr8xvwL!e?-C#{G!z#vq-WUe3In<)Pr`R7;+Dx)9Qk7|dz;-La54 zO%$KzM4*~f!07Ex90|dFVHCoA1lzTPq%j85?q;n_@<0Y)X$)~ubk1W;-9*3y;iX{d zGBTRZTULZMBy&fLMkFpyElANS&J!`HFsHv|Q|a)g5Zf&QNkW~Fn@Wsum*hxzehet) zwks}Vx8U{6o1nCy#qwyqg};PXd_;+XK_(^W6(T9DH!CuvHzJzEl+{G52}*-Wt??97 zBB>R7EGH^S495bgnWJaSUl_NiwZ3KgMAI~VuwsUe|Va@jA!?el}VYnMqyCT-7BWv;U+$BC|MLyDc0sdyX3pg;x?S6~(HHc~uH!OeB2&Qd|qOQx#xyA@z z6EC7Oz?ch>{iha>g?hviN+!U@iOC3fC5cp7d&Y?jzx8gD|C!n*<2b|&F$9#Hpm_T@ z!TiAaOm+~VBZXxEYwfhu<5*0@V@yKvg}KGVQ!I2)Fo(buC`_PRt1?ujpZ!n~RuAy! zQF6zL$g|Dn4kUaoujo%8bcl42RIy-Y;SR|unQw5A)Gd62_6*D&Lrx%#j1WR-NJ(Kz z5&R@?M4T~OsS2nM8S5ii%R8f)x%$Z;KlvlNC=7z4GR0xz6_fag#Fx-YLf20jnMS&L zO})ZD;Y3gs>nAT;8nt&J+hZB$RgqU6tD)nBrEzG;IJHdGv_9?e;S$xDX{^^az_*Mn zEMyO!Kt1XNQK+$Z}h1x_p5Iiin`o*()g7+Gh$RWxemM~avdb5 z#X_4Nuc}d_AOAa}&-g%Xk&n)tpxTTN=#&DjN!GU_y3&D;?bCqRK=LzZv8lvatPGKn zuFFJ$y~RGG`UdPn+S@#d-w;P7tP-)t#g=5l^|P-Lfb+GVsa?+kw7l2Yqi#5J_Utu@ z->5%dyLRpHigmybf#6t@qmuC(VWe(!jgOBd3&s(PShY}09PAh|DpDj;*stsYY0^QC zh&7PdPROl-Nr$2|3?G9fJP+spaHP6^;JfbPVHKEX&U{tcP_M?x6{-_PATRy(17Mwi!KrjeNwdfPb5dAVJ z#s-8T_Wx_|OW>m@vj3}kCJDK?A)H|nNFql_CU-a!NPs{{LhggZNirk@$xO^ah$y0n zNMI2Na)}7&D!LvKR%B5Tbiwr&6;BRX#2dtOK_U5lU)NO6BnD9byTa%HG5O5v>h7xd zUcL9~Rn_aRYIks~IyL+7Ms3t&6RIqsAT^-tg98RNn8F_9btz3ilhU+U2|1r{wek&3 zi}84YH>Kd^q_YSy!j3(lr+I2rt;Y_XdpDxsje#zwv9>$M`wtkaqf4U&OMXEx@1STu z`eKfU63{d;v2HVFR?nvJgr=uco^4uM^U@jw=xkFr&mFfXrRk}rJ;{4>*S%N)YnTWs zQ(+Ah2_S3e zpeUX}sWW3g3LoRy49N0meOIfs=}eQ7#^+slT`@K-O4Ds^GB&XkUT5X=Qh*chu;@!& z67bH*1U#qN*O(R3m>trX)zQALA{!s;IQ)1J#czMb$F}JmlVIAX0iNOUSPukeU1Ph) z2F6Ckj*HEXof$hP_O{qHvG>RBj6D!bk6U6-#^P}c3XOlGaXg*`q>J%b2!g#KvJpq( zBoZe(5)~TH!3nD_r!f+1s}*&lK5Sl<*s*cs8;v93$HW&Tg0Reiz9bQZ*94>SGAV-j z_!c82u=v5JyPJ9$yR=a@H<6jH+w-VLT~{~SSk|p`K=uQ`jVsw5?uke%( z%cCQcy$1JH@JU96+iJ=e_wLoJ@4>hB|FvpK4{wj2eHP7InxB>(Kj-C=e0_V5pn+Ze z`gSfWxM9c6tzG)}>sHfKZ_G~5?Wx!6xyK0i_V(2jPh;PSVD`ceAFl!A4`|o>U$tXX zT!)!Hb9`>|S>toR&rY8MK5oqVUm8MWz^~INe3D|Qw${)G6OQo6 zKy3Oe7R2kGw%+(w(|u#}W4Z@rFPfgeD0^V{sNAtl_q|nT9X)SnZSBq*M=K{+Hl3MP zS;PDHDw(%+1X3BrH@`bucTMgk($bE-sR5g4s9(g+$!nFHblEzqZinuiK2cw%U#Y(rZ#uu~ve@N0*AUl}ZlP|^wVU7W zfctYEw|G44@s7u7PY=)jo_Bja?e(>{72g!CZU1QdWB9V{YaJErjk37oLmhwaG^W$3 z&J(*Bx~%M)-Sxq4?%krh-Q4YT_wn7Aci+|hrlY)fKvfy19gGz1A7Jz2n-4QJn;L#<{*!tZb62i!9h_$@j=N!IYEU% zMr=!ot_{Jo}SYo!| z|B;w~#O#mF8Rj$Wnqk9+Z5(!Xc<}J4!&eO7HT?PEuMhux#LXktjd*P2u#tO4xsB>E zDs|NM(I-cLGx|cDdtA3TzqsJI*to>FDRI-{EOC3|UWt1z?&G*K@qzK9;!EP|;@8AK z7Jq(>-+VM|MD4kH1 z)Hi8hQbbZ*(xjv(C$5;dapD6LcTC(f@ui7}CLW*o)x@)t?w|B%a#iw8$@R%=lD8y3 zlDunjzsdV2ADn!6@@JF3o7|k@kHS5EUSH1udIM9W7deQ30WCgGqPr7)nqNmT9&mo>)xzKvvz0g&%P-qFefZ$WX^<~ zjGP%cvvUsR7UY)XR_4ynU6Ole?&jQwbDzw8KKHe}g1q}l~dPF zeP-(S(}qvGWqS1V+0!4I{_c!xX4q!Db?y9XcbdAGvP};bDusV3{HZ9tXnN7CqMD)w zMN5m;6m2OUZ|-5vo*6Rp?UIa=mrAFVK2-X-Wu)bR<=m`CX6Md6SGKY|qWsqK^VW^l z4{bwjCv2x}=PL9S9V&WN1XjFJarnAQ~&k;=`5G zSH8chVAaf3*R9&V>XU|E4S@||4Wk++He@#xHdq_#8WuIIXxP~BK*NrPXB%E_c&Fjt z4W}B;t=6yZxVrc1;MHHOK7XfSO~@L{nvH8ttnIt@!L=W)>$J{)-H3Jh>uT4nShsE6 ztLwg9-)(*P`kCu*S^xL-Uv9W&L(zsc8{WLj>#mV^Ror#&T?aQR8;u(aH*Vhe<=wsS z&b@o{-LG!)+LXEJ`c02)`s5zJd&=)Qc+Z*5?wd0=FW9_qi~p9)EsM9ju;s_CZ{9l> z9*(>mXZxFyHs3mJr*nuAs-=O0mqx<~kf~Fe%JhxFeb*E7Y{o%bbdSA-^ zhZXXlBH;ftJE^_{h8;(6Hn8~^ZhPRVJF+*Q#rt5`H4fL$uorOU4z{iN7`~NZ#JfgD z`d5#;o14TL&X0*7#=s1z{muKDKc_svun6yd5g#Yj+zfAyoyRxNsN`<=ae`EW@Zzue zNA>J@^GR@d9B&=$#+v@!SOc&h|0->wL^&OQxU2pH({A<koH6kF7HZ6pmb?sBu|p1+!F|>IVe=t&Pzk{`AIRn_$e}&^3*}I`I)*Q;eTejZ z*yD?FqR&w3IJhMWFL2Be@V6V!TkwXPS{m|Exf0y&hGdUnmPGZUf4Z6ixD&Gb7XJ?^ zXV^nX@8;3X??W04-xX+5agr>TvOTmny0I2afcwi*9hAHZu<}C(6;^86WVtE5pwzG zz|kTFT?xxD!Zze@H?DpN9UfA#0Ru@1GMLdkgZVTU&_A84ya>sYy^@`XGiH}Pv<^RGNU&d6^!l&s%0T>Iw$F?P_<&U<&agW7LEBhKeSEc_+GUFD#Kv^XGpVZNl{*HE0i6UctYjT zsXW>}l|PU7k*V~L#2YM|c*7O4B*(vsmo!XzGfD}odLUg$eW;w_ouLH$7`Z`x^R3f#a*W;%tgPMDUVgK2Vat< z%gd^&SR7+ND}shAUhp`#tPkK%h_&tRG+? zGXMqwp2gciet-j+Kj0v&K0J#rPY(bb0{9=yX#xO;vOvHP76cf|t^qWnH~9}%;0%Oi zg|k6`5o|DEBpU)4#exB&0e`@FeJEfo3jrL)LIH=v{(iuBS;GKF0)CILr-TEJW)XmK zED|uDMFEZh{0?s`M*}9X7{Ejp3pkDq102tW15N<^7H>9=0G!B10#0J10F&8hz{!B$ zz{AA>rm}dzG&Tls3L6WU4qN_)WdNSRj5`r9i;V-!X5#^K*aW~_z^_>zO9ISC1mbH} zz$O7sWyyfk*kr)zfT!6EmI8P!O9eEsG{8bO1+WP46f0)wfM%8fIFn@pmar_qQkD&9 z0sM;1VmW}bSuS81%L6QD`G8iyFPV)M09LT6fY-5UfR$`IU=^DISPl3Et6|py&S56N zTJ#%Vu(_-VunzEZc0DTwoX5<7H?Wz2H?k7I`K%Q1Ccw|w0%ifcnau*ch0O+B$jSh3 z1^g5}eL3K5%nG=e*#K{66@W|Fb%6DNpWto(O2B2T3h*zi8gMzQ0lWk7BwNAe0Ip=U zfUDSCzy?+axSCxLcqib;Yz>`Ar;a2Hz( z_z$)Ya5q~I_!Qs~jLkLxKEv(;+`~2kKFjU~+{-or?gKo`o@4g_KF>A-{*!G1+|RZG z9sv9hqqchi|HbYDe3AVX@FjLX;LGd*z*hi2V6S3S_W^s2JqY+ZdkF9i_BX(TfbX+6 z*~5Tuu}1*kW{(0MVvhm7!x{nK1$>Xa#~ufKpKS;Hfb9VMko_I-FyOoF2zvtXDBB5m zj6DhX5!(fLoc#mv-+=G16X+G+VIQ-n08gUNc!zz$o&o$6@DTfq?E(CpJq!2++Y9(5 z+Xwg+dk*jv;M?ppdmiv>_D{ewY(L;P>;T}mfN!zy*b9K)vws2pz+MFWk-Y@?ANDff zS->~hIra+RPwZ8|pV@1G=h^Fk7XS~kCiVtkGdl=~&IR2~wTH`}e}78gkCXsop00oX z{V9PzQUVOCeg6FWQv!da1Q_%FBgy?q>rV-|sq=P<)!~f-%(QhBNx)IBdkx3!nDe+| zE(^oNjCnq5=Aqokr|=A3%jaUT&nn)m^u~uRj_7XF)$5k&w&=F$9@IUn+pBv`@1;-G zPu0)Ruk_XVdiwVBjrYy*HTll=UF7?np_`$X!Pnq#2sDHmh8YqJ5BqiZ^Y;t%SNvW5 zJNS3@@8$37KiGeQ|F!<+{yKbwGqC@S`1s~)1NsNB042aHpkqMSfF1#T0tN&O35W<7 z5l|LzAmF8-E+4u5`20CUDX>NpLx$t$g{Lm;zVMF=yDr@RagUEXf6PvNbmEm0|2*;ZiESrFPzn0` zXyHez@tgBehvU7El^&}-y87tmqj*v8=#rzAM{P%^9SuG@^jPSzKF2y8ed_4rN6U`( zI5OhM@FQVILXHeMGVn;?k$y*d9O-n#{HCw@5GFLA7*r)VOG5jbFnWl2iuH!@C%r4K8%Ql2{ZUY%rT2F^ZXa) zlrLe9`3h!e#h6FFhI!>9h-;j}{QhmsDBnVK<22@KixJraj~5v`!-4s#Hls6}jI9wHMvF@L`iQHlA8Lwtjo-7d^Rk73^V0p^=WF&8}o`9H3N zVxIj3=Ed7F>wX^Xb}u3lYn3qehGHcJ4Vl#Fo|gMK=gaeU@#aO134I@T*Ugw7+{lL* z42hP6Mt*H@qcWtC5Aq9c)D1C=Yt#jdOV90}XQ(&SCl%LY=hISCaicy!J>rJBJ}<=3 z$TD&*fSI{|jd6LswWK*OZ&+}np3tBKa@Xep!7Muh9Mwx97lIpIhDH^bpr{p#+ zNa)=dmyp-n&(Dz9xHm1gac@Fzzr4KQMpunT@Oq=Ahv3$2NTchZ;Kp_WLq=|6T<=Df zS6@%~GIIO-H7=;H?_G}?$n(7|&v$Xw`a+!Z1xO%q7hjMDR5rEH>`}Ow&hj|IX zjqXDxW#lG;MZdgY7*`|0gWIwCYcSv5j^LB+#*7XP=)u|ze$qW-^hlBNC`lg~qF(b% zmUq1}WXju=ZqL`=uXnc&2%QdQPjaoWq7~LnmPPa$8W)Y6tA@r+qGsDW=-u#El@K38 zJky4l9og4X*xM37I-iFW7UiHaJ)01>#tTe6@qQP@cQG7B>}fYP70-$UPIu&?UH6EVCky3!>}fBj zw0F!W{50a9Hnf{Mv>FRqR4Lky0a4o`#B2?SZ<=s@Hc}b5E819<+R_Z@(L(Vbix@7Y zjQ9;jzIe38G8`S)s?;+x+NBv+=KvNXXFOUfwb(ipYYpxi5Ie2JwRGetfjrAp+D7Ch zI^l@xW+C>PgJ|pUHcE@K!L3SPoQk1U&TN&tD%6s=GN3+^?k{otc3Kv-ekS0pMjcI% zfEo3cvbDjdT}swNFQEt0S2grE4Ds_2csu$Z3fxuDOBL?afELwhC^Txr^-F16YIMjY zxgqQLtyBr4tZ$r_B56C@l;m+Bo(>pL4-5D&MT#uK*?J6!(36(PQd1zsa@;4&yO>1{ z#VE!b5%^MIBs+7I&H$d7z*DR0g0P4jWjGcAt5v02jO#T>lh!JcCax%N2A&HfKb<#p|SA$LrPS(=W7NmtxYpQ^b$(tE4dNIRa*Z6Xa%(^AwrrQ(>0R5IGjSoLZK;Kf>m0i09s5~Y?kLsFG! zedH_a)a%q6L?d-2>KCj^Hw_*s5t^EeCy2>vO~|*9HcOxjp>G3HQH zl-~eerAMv69UDf~W>_298CkUCq6Szi)UxDzW!)TQ$v(|?yrLg912f&HF**5O!HEgD zWiLpc*3o9DwOQ5vpFA#%^G$lC^cLYv)_mt#d zMgB^)EVX54A7H~>vLw3e=(UKFuneL>8YE9c+(-?y^ijl%)QQMN-bZjuBQ4^IWak(k z$Sc$v(|BPfel6=H>L_$VKBy47AUhPa=q_0uNyemdMm&-p<+>+JNST9M0WI?TBYY`Y3a`V67K7uKZ`0Ol8Eqq!uvSe4Pm7gK_#d? zi`MDP3CX>!kswLHhEgPJSu+#(t3a)YbAeaZgYp)EBL^=I|0ruueYmh%VfQWh6jmhe zP`xgu0pTkW&=-wEADe<-CVtfKW}uA!B}TmdxKDDJi640#SqI4t>51%5_-*nFq(92{ zOI->ZlO@}lX-ohfYU@Pn8epNG?$^m(v@7Zfg#A#@M-rE_3L#J7d#ZqwJh!wCp-Jhv zojr!|;Wp&2L{EkoOZ>@&8rxmy78^+LCiCuTon? z5jb=9)1(twbK;JC3(1)@Dn=(_%-V9MPkjJ!Of5mQ7@-%*dnzzcT!5rTeNXFA+r{{H zrb#`P7){98wXmjfum;gaGgSL>o`;DZpT-*0TT(8npU^M$P{eoZe(3kupRDI(P-@xp zWrHrY2*LBk>M8ZpmS+^!Nq40Gr7;gtkuBb)dP>Sc5~4ne?vUll{-XvqD8@lFmzO!j z=vR(aUd(^y$ptnjbSBh$7eEi@#xcsVN&ZG~`CyCazhi=qBs_+M&$ zas)^=DRd>rk`9RoOTCn3NFPfc*o;2M0BA+OL;jJhMcT2{Y`iKr>Ypm%3ETE|&RQgm zm#DZ!KStO^ZD@=mu8E$y1ZULu5=C*H+7U^BMyb^QQG2PyJsPu$wkY)K+{4jWlT4JiQ3Z~EFJTdB=iFmt)L#kK<{T_9!BYeRy{?U%1b%;tzZm<7{C&!V+j^YE~^;>P__VEo*H9R`Nq+2htX3>@Gc)?0+s_ zZ?Z%|NsN8yT#j?=?Bfy||BJNHoaE3k)}vC6v9QQX{ast0hw_sBh(3Z^a#@Sgtw++- z-#Kv0yP~FIUh~^(OFq9)o3F{*OG#1dlQRY>S)xV!ikTFps}OCV7#ZHX1@9@SsffJ^ zo0YgOZf#;TMD1F(Bw+)Dh5R;ACVFCKMzeX6QCpseq)EAH#?!jz5pCyko?47YWp6LW zosMz1uwKCz>1w9Em&Uus|4%f3z0O+CA%8jNxR|Apg$YahJ@X5a?4@QHG%BYy>)h&u zrO1{^nv}L6VhCbJC2Y6lxI&DM$qQ2dBijd+ZfjgU6nQdnPt0wI11dqi1eGRxkaIjS z&TDC(a@0qBlAQ(PDCVD3nmp88KU9WOke_G?{gNapx4BL1HSd+S^XJ-D}XC>T<2~_p}p1 z&0%}#;Hl*t*x{Mv^8n}B1&x8IKckVtTMtWma8)?}>oaaK){zcmmI7Hu1F*wJl*=pB~oO8iCv=H*~ zR9iWVkd{I@g)C)H-n##gnwIgX2vygjtvc{YX}26TUar=TQqHp!=kb~&hx3Tlq3vJJ z8$?7-^ucn5F1(1S3HeYVEzxqRCnnvKcO(m-IXks@YAd3(Q7aWu4bp_TLtca0$mOhE zV8t2<;G?L$q)nc7HgHhPgsihz9VIy|oNTG(=ug-{+w%Ceau(jvL8Wa;lP;wHk$FYz z;rF#3F%pezVK*1oAW4jLLT$$}yA_&fYve@vg_peCEJ$D`DW+26Dt>$Dy_Q#rB}F$yN@68*d2o90*m+y#ss(X90V#UoC!daGATXwe*zauR>UX`1@Pj%ZRo zDnYf#!a1eKStvzkbTtddG~^S{8woRUMch)1GzB=))#G?z zB@V=MN+BznA*X>hD``6?up%;U0sdkUdFFJdt!rM`+w(f5deHs1UzYZ->sE z`6bE!QU`6>fV3&XMcN^GB&$-$fcz4n5yB*{P6*@n z0<{FPq*Tb1?#WgtXvmhHrIwXgsa#9>%GM(D$#R$S5JE|Vk>U-hpm3?^GD%a+JVZ1}M&kaLaYf?m*F+X6A9-Sm z%3i7HBJpq~2udG_HntBk>`8+b_M5@O+n><^8^hBhkHjqK?GP#q5s8$HMOZn7AYHB4@zj znLrgdka3pQqw&`7F8}u;kJ2)(hS;Oz#aRwlBKk;tTy613;_2#%K@vAtV-%A3x?H5_ z%ETdwv)>+xq*7N?G?Msf-DgsdNzcRon0O>{aJjyUdU1;5{VyYu#K+|iz$?=p=DL;}Cj!Ax34+ zw8=`^dVcudeJ_uApy(~pr^389OI-#g2*Hu~T9Xtl#a0oe}$BM?_!jglJ%Xi2e1)u7?45n=T0NG7rT1+`)+c z2IHNG5Ufx#Vs0Of@o6O1F-2pS!dSdvH5|JVj>MYY(b$EMV!vZy1@y)?JzYq`?t+sL zt(^=zrRNS)5KqoPZ<2+l137p?lZTbX1=tmE8g>Jmp{|)P#2Aj=9G!_iih7n=c%oc} zev>>qt%Ikh*!0fm97G7`;%W8uki-pG15Yd97qFYvwfnbXefw=#OMg4oEZ1ZG@-pnG zw;U_&S77b_D(sxM8f*90V5huw*dcENR`+kjj(D4}l72JWf_47)V%NLBVz;{ou&Vz- z><92Su3)YHqu9v~Yw6WB^t76OCw8jag&pd4V`sXjxemM0J&Seq`>@LXdF(p3A8YQ3-cd^C^a@%VP@Sf0QW`8YnF zPvA*>BA>*Q`DC8LQ+XP81WCu9AelUiXJZe!^Vn12B+ucwJP&(76yW24)A)2ggI~){ zypR|1Vs7R$c?mD&7Cwv5=4JRmua(<)1?{=Ouj7@xidW;~kaN^sM(X(Wd>+4n-^k}< z$B_m2lH-Cyh&7a|W__KU3-^ZWh&+~us{rmubf&YuY$Y0_w^H=z*{5AeMe}f<7Z}PYJ z+x!rJhri3;8Ny4IxAh2u1YthJH9pBL+PpX!X7Psl)j3u(oZoceu}@+Um2hTD1l0la*Z-j z8KewWhA6?xP$fhORg6lQ60StBua!t8N{Lotlvrh$GF%yBPU&i)`iPCR0(7*;;MC zk>D0zZYrv@S=~(H=o(*GX`W+tHK_-;cw33hYM$+85=YOmMV88`|m8T@#8-KcMlogAvaXN7`e&hQOmlOL~oWl7d%>(~0K{@tWP@q@>bpww9T!#g-!1 zWK&U1wb`{yJ$NNM=PPqMaZMHiDpL>oWYk_?2H=_^%3DSG6sICyR;Lr!6j9zP1Z*`` z*s7{4Z55?vU81!_XSSBOrP?cRvsXS;H3WjZ17s&+bY%@k;=)q_5hR9a0c%@QiD z5h~5H*Q&-|t1MBg8uj3kRcW!7xYQ8z&T_7kca8IzTb5n*HR9-*D!2GVHIJLgEaZ|}Rc5Lx6=|Cz?Umsq3a=`s6V(rd z8RPB8F)r0MtF6kr*kZ0US6QmmGmrSPic*uB^h&I#vVgRD(k>C7W}xiuNof7zermae z+=)2Ja?0$HT5c{8nL1gJy#)mqm4beZx!UA1-UKJ5vJ)N~;by0&&&Mqt=(%K;BBdvS zE|W|Z6(%%^^1@=1GPy=csZnw*Fie#LCC#EsFSWU3SW3!Gx=d4zo1Gh7TB$`h7QeJA zix=UMoOG~fkmnvIN43pPHE9P1M760NYN~6ExG#&U7hTly`a)!PDWOWa6r0PcO>TBv z`s;B+M>nh0DiJc5*=m)_)G7%Xj2Wv~Ym~VbH=C%JuCmnTT1AyIx~KH5%a>jc|=dxJDyfqY*Yo5qhryf)(!LitD8f6TRR5=d|4^=r23pc7Y z3JXV@wN+GGY}VQ4x(?<_QW$7ZJdA9_mJ AjsO4v diff --git a/addons/gut/fonts/CourierPrime-Bold.ttf b/addons/gut/fonts/CourierPrime-Bold.ttf deleted file mode 100644 index 91d6de4f34673fd3030df55c98016ba84e5c8283..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 69768 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 diff --git a/addons/gut/fonts/CourierPrime-Italic.ttf b/addons/gut/fonts/CourierPrime-Italic.ttf deleted file mode 100644 index f8a20bd097284f65ea89f88f742f6d2278df9215..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 76496 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®noDRk&?P2sT zLi>JFzFUQJhqQmJCqIPv0POxa=XrL2gf%zT(L%!3=?xehoVuGf3P>V2M$n`Rz!j^| zRi_!-AD1bprn*|hQ&eTwiA52+eyi&T2_=C*Nx}~^whmvd%c`UgKg-Ou{^zgbDt=}T zGrAPjLod;Qm8QWTMz0B+nVd58C8gg>V{bEdTrKNAQ<6Y#2tzl^u~`&aA8>WKK#Icq zi6LOqSm!~DF$j3$Ct5A#6Utc}4? zZT`^5+O=Z%0%=d}r1Rgna_syKO(1TQP6wSi1@-Ee245aI>cqssN0i?Sl2q7YjC&WP zTN1e5|Cc@xReA65;rDKZAi{CWeM=#x`0=0b zARUIV+hWk67p~CSNEP4~<5spFkYBR^?T_Wi#f31kd09Sj<&Z;o=!B{Z`8kTnqO%!= z`)d7jt5-jF#b4g1SsZ$uj>dlR|K+t~*Pp!lHSMky4{qOn|8o78Bj<7GiwC#wB%rfc ze_zaDBb&v}M$)P{v$=pE%V9KAJDkw<+uI)r0D^{TC&5_&eU`9;x|S3&D}oBhrVa!p zOjJ-`5K<6A^B|xp<2LBJ=fBMk8Z2h8b{`qrn5h=TEyVnWy$kUJ?k0rgHO*?8*&1Pu ztf4Yk=J$u<1bZ=@T45W>3O9)gQi2T^IBi*I+mtwGr8>cvl^FMfChq^yV?6%4QyX8t>Z+Fq2I#oapdOM+ zlh`!Y5}5>mVh0zq;~TpIF7einutT2TyBlnT)%dh>D78n&V=;H9`zw zMht4gR754wr;%8J@aL%3$CsbFv>}*ZnvHR_4C9eN9QabI)lzKZC=9@$DMCr=>%g(0#EOTvZrNKL z_OIw(xVCc5_EpaxS@YEvl3%`}lBA-?4F5fHS16JOlz7< zx+`bg{iN3;DiXuHF^vcT=dw)tYH@P ziN3}F9BfhAU{UmD^cB`B16*tOpjVyxO$pVdCDrlztsSDTd+2yAui~|9#OE#p2PFIR z@_p+2W?u)dQFqCHp8F8}1b2t~um7o%KmsC3N$6*R^+M<)M3`wL^)(GMY_$eEDI7rv z{X9q{@{1lC)yilfWj=h05ci1D8X@xq_z?lZ7@=wo$QU7^9&C*?6A5{>vlvp{)s~{d zx#?%Wo-r?6q+KDf4i^~p`ZdocHWm1-U*B**jM!M6!Q_BviZXrZ>r?XlG2?zq=_pT6 zA9_gjis54f2!}!x;$6_(B9X~WMVTo9$UYLI=rz{YVBy67 zhDC!Q4Igbb9OuHAL7NwZk~c+vg)b3`oG#GK=5t*CWF8Gqf0mBTe7XMebTM2p2fe@k z&q?A_g5Lh*{tQ!RwDB?8 zMLjn?sl?K~>X64dG@ztQ_BEvVJc-Il-6=mRzoAvgN*tWS6Nr21U!y)Fi2lKk1v8sW zdN!Y}Kd!E`EMsocvq6UxN3mYxm~wRy{+O}(=&75z0<|^d+|{DGg6M^uQmEPLEKHZa zN=%j@82v1t@eK--&JS9>2s*^szb_;X8XISn2u%OKh%<7NTTC)1q2}{OzfHMAttoW- zZ9{$dYuS;W{P7KN_2#7~TQ8u$_+`Eh(M16xGNfjQ3`m|MoYl zC(WJe%C+(uXI`#$HZ^VNQDQNwP+Qf7l~q;jQWQf}#-R8aRcg{*l^t$4L^S{;q%`4U}{q7ypZtYed`^r7~j&5RFQ(9(r zO}~8U?Kf60=wDhKbW~&}`_oFxb*+2hte$%|yO6e`upm~V6S)jQDA8}9Nru*BGc|KR z=2hy0fldm{7V1+896@9iGljJbB`iCCQ)$;T?!r8igqI=RB~UeC{jiX|d&JYnyCqUI zUTa`SqcISKiP3to5Ux;&!Pg(EnNW(M`LXLmFb&AiQUU=Z(6k{oqNafA(V;qgs^`*Y z54^Z-+lvQQ-P_{veTkN1?m9YtgJ&J^a%D?4cPM2&_&jnDfSt6K`I$@6Bs_Z>aGVsgFU@2ov|@6*UWQ?(4s4VPjo# zQI^~9o;2;?udcrOR|ls|nj}3|G^UeLddfu@z6q}Xq zGB@4`dQvSK^3ZG|FUEZOIJX$|G^#c2YuOkiU_O2vdV-sw#&6Qou+myv8{O8luT9uL zhMv?vc;*uEfR0-3T_p+Y*-P)FOCReVK}o%Ws)(LGc~GK}Idd0Fy1IuSlk_AhNm3Q< zANKp8 zeoBbvh+qUoZ zWBb3HrL4}l@iO(X-9I|&%e~>I^01>eBc;$4uGsa)7f<|Z?{yb%-MN?c0-|8GC6*wo zW*0sQ>z{*|5K%%AzEm&_*hduvgh9=LVCN1b={Ot#&+!uL%&IS)z0VS?POXSbYP3}T#}=`8FUmy?<|_g!@4x3Pt>7j4I_)G@u$;AU zXCyADV9c4q=r03VN?>!Mkjq0(L>Y0`@36wef}ktJYV^}AL?aZ-Ai|_ktZu|Drks@b zlRN+XcTjd?fBUl!G784x#)`xGPMlYSY()97dPwx}ge(4Gh+z%LKqLz~Q90rzG_ zOGR@-9cExcAm9!JYNhlP_6&2Q0l5hPWkL>V1gWKKlD&yFuW>SIyfJ^1Q{UtQwOPYfq#)@!*1hj)DM)~!Fj=9(XGJ=J~f z(xumS&%U;=@7mc!!Qj){QE2@M(l&0>QKv2*v+)hTk=@=DB01In0UU;-Oqnh`7rhIkv(jzkkJCYjUvbl2> zoLsz25VKGT=Zf>kg=|agU*COdwVrEIFaJgFhE)%4)ZaEQNLj~_eM&oemSrCE@wFf` zg_mh`^*Mc=dQ@D+&PHT+8GC`4E_7r$do65t#4))vDN#fGtucc^khXOtII_ec_LZ2? zU}v0DL-ZtKQdEcnvg$_sup3zHR*S$B=V>MV;VrCrQd13iQeZ%+A#civ;QngWHafC8 z%FCe{rHs}*p@nN3Y4eNun)%5Y5i72Xz?`jzX(r$BdHBUM=!^>mbfW$SpVM@uIcvQ5 zj%f0SoWcCy;Gg*%^Lu?r7dqCXn57obSPq*ZiOUTht|8P&(*m%cU?PR|BF{+%E>b*! zx<2;0Odmo_FO6KL4-MzqjP|W6cY3%`lLv+Io;T|6i4kmw6-&WnmPL^4x*$} z-m{W99OK;)2~#|b*;eeh@mqq;AO#;?>~5dcHmzk6ZKWXnPrgw}1Ox|DM(v#Ou8rAE zAdL*M6WtPGrc`U+!;0VF^ORMU&nXuvOJ6WP)QJ7{dpgb)n-8r@E^3>$F17dCiOcJo z4bo8NOrO5!meu<6k`hLJg!H&ZezAJR%4NMzPTaeA=xvi1U5AyJ$nF+%`D6pfUhBxA zzt~pBI^wY}fzfFUY4I&RGXRUv94#Vu)9XfJo)s3Z+^6vH#?pIVbz&QGM*dua6!3^#OT2wcw^z zt8Q8#kJO(aH2hBVrwERvC(nc&3dQP4U;yO?hExjy0*gh6blJV((2B4?1SHeTMcWt| zZeXbM-WDkxc5-3ppV6EId2un)EE|g(sw&XgLRxE|;R7=!COl@jM=k3p5w2zl?d#b~ z9~ApWm?qc7VCdB`N*KVV(7*1{cbojv@SK~^SIx>Pu36%{?&g|wrL}x<>=y=;Dbo_zOQ*q_Z?doHNnW5CoMtjc1Ki>U-w1*JeDC!{;Tk1R>!VcEnHm$nyf8`-01%QSN27iNj;y5o?oQi3N7;ZlSx9jN=BSb<}r;thnf}yy`pVD_<~`MB=#E^ z+d`TgsId~YKl(;A1GSij37IubMoRDmqWfxADIrXBModKsq;6UyM&MU5QFX&}qkcbX zkARdk)q(qc9-#^xr>Xga3J1wh1x-O-aPPWJ5TsnhxZvuagbu*-=u4U#QGEk*b3Xqh zc-;TZKY1}a)S`bQvA(jRHc9`c^rZ?P#UY-;(-%>Pc@y;*W;ZvL5mDZubMbkX+^n#e z3;!hgS;v|p^<|+%;h-GX_ZW3fV!9Da>qiilKQz|y7&2`7_-lwWgEsweJ(3M&l4tAxuHaWtCGXgZPxYi8s5iW@+s9c z9{nqH?0f&cZt>|QI#%)N`WsS75(2*C=f9|@>%2maUH`5wBK>H9T+wxNN%;a^Z=V10 zk7D>lh5B0UW{iIV#UBLIMaUWQR^bJUZckWKh0(Z7#CJ|$rI4xcC&xPWd7>h6?2yY- zKYDP(qt%EaoAvcm4&1WUAFA!!zH+*LeT(S3L6+G(p;vr1$a3(RD^AbQc(8nmYN4WJqpj>e_ED7Vb~CH) zhLf37AuEH-kf#D45idfEF@F|>cbOUF@UV+yG-9$`L4^}T7pW{SDJsaz&O~IdnG=VU z54YQ8wPlc}02y5(g-MV!(RV#%9KcFQ=w1ldFgb1zaPuvY!N$Fnnrhj4$-(WY`E*z? z&+omZ#3@Ih`gwfEF6S2I*RNQXksGmT|C*T>-B#lap4j4w+nbPe$u7G3#1sD1{kiem zx6_A%yV9Mr^AgUU1!Bt4Z&Mz%?0|=Ff}DZRs^CCbk46OtGnNLuR89!1zpn^Y8~h(X zM6+$3C#Blo^~bdwG2d$=&Qd_q3=W}em|=1-!6cSZtO_n=%r_|<6u@0fsdHk!g*Ruo z8RC(90g`FHRhsW4ddOk?7W7^e}lOU^J%JZ#Q#-uXwrMws8_ z$7oi+a0Cjxwf1LI%{sbXy%&zO>~u{*cbUjTAlJ1sJ=sZKc^t0kQbm70Y!RQD3S0G zHje_6^dT6s6o%qn7LgE7dGIu3CJLJ%b>9~V`$W{Z;R7Z_HWH>Fa0~SofYVI}Aph0c zNYl*lg0^H3V1cdt%C4q0tqrq7F&C7#iVJ-O^V3o?a}(n_mKGO!z0nWo0DninPx+n5 zkj-WDpUO;0#zL5ijay_7^`oQM=M*)LcuAqlAX~(Uog8k#QFJC%WRK>4YA&xx)Tj6w z7Z|@1Fe=n79$C0fIX+yXZZIn|TlqE>uN#`24XX@vF;rIbz`oT4skJ(i(o*Hkb0Lhd zxjNCRA?Q6yhzK7+@bn?vA!|orz50PEL@lENtcv^=8VB~f*#^{%0aIn8r;39`6N)V4 z4ix!^(Lxs!kOop$S1)!ov;w$5j8kLxpx_xytzzi{_%a1bs^%$ZePkt7J3IJ|bsN5M zfX=3DDU{f`$hURF%qtqmr#6%n+1=H-dkUWcuUS>Zd4^EQnZ(O}a>*q>KAdm=$>nrCMYM9mt8%N`v1zgH#HkD^9puXBrP5^IRP~3N6P$+ zY6XTg%n=2*B3V(YScs2BRww)>lt>fTk0h%&kxp;xhm{5n1DIC($uopHXE~Aj|H)lY z!w}JEw-+JqRit9AV9OWIpVO5VDDwvcg@LdPT*7WPwsS3rTx58new+!~iM@h{if~w( zSP=~iUpV%{sAyP93Waw{FxqNt7+R`o<;AhDre&uVfa12Y z)!K5cht;DB->gVmQ4pq@`sy6FX0;_a6mC^fMH2)J?x-;YL)iul)G5kDGAImSu|3mT zCpDFo6o&j>zaLqNStSVcLAnx{M3@dyc0pCf>Lud<#B^g4joe$}t#k`$6;g^smRta> z&F%5HGu$WEUw--e6F9-A<1=R-ANT6eS6VOZ?7Xm59)BZSpO`xJglv6$-H{{fj*olw z%NI?ZdQp43ID&4?NU-@-ak5?H+{D3Ti)1fECxJv!^$je0eNtjPAX>u8lw>6#0eMi6 zFGAz@TPX);KAYRsF?;4TN`*p6(MpjD1vlRK6(?;bm5sLM%Q`6lD8mG0P}VSDH!p}Z ziD_ThN0Q`6n|PT1>aV^{f6UObSLp)}Z~77a-G8D@d=z&6(Sc@vB071P4$+icaUw2> zFX_8z*qIU|^HI~owE1I~im>WpQH_&ijY8(VNDlc!2=5Uck?lh z8Go5uo`m2?Q+Tb`t=G`wA#q8RO@k5&hM6`# z@mC59>+A84ziFIU=6M2_1w5k;;93FLMZN;*L^ktwwqEpP8DKahlkE_AoyBH7i2UC; zu42oh^ri#xKMs@*vLbuH3g0I@Kni5G!XT>^)>BYG<-rQt3uaGir2@0%WkrQC#ljK* z*hS|x%B|4+z5pe121Zvv5qWL~?-V>xznz^pEbnSO4hLDQ>&( ziXZRX`Qt0(ankZ)JSI6eJ~cQc)V*$9cW6p5H9j}lmYz|(ylm--Ide`d)z03f|M7tb z^grHxH_v|H0iJ!=MTdTS{P^344!wQ+_-_xL&Pa2jYG_KKJtb-Tz`*vT6nkMxa%z?{ zEo0s-8#mqp8LzPa=0DLKn8gXKfw(1w#?{p#G%f|f^J<3~nFdocX4Z;)muMs$mYaZ3 zbu=!mR_PKkX$|G|HB}UomL3TDYii>hMw0u8oC{(ELcUgw7n3F?4Jh2N=r?9&r)0a` z*_Bn@@t5AzwXf}gD=%NyHMi+VGwKK@+w!O0+Wy{*=93q0-l3hnVxRuYyev;aeolGm z=IS*w_U*~7Zl0Ext(Bq7M}nt7->|i!X7}_gbe&zpo3%Q%lclnt*x~F5gadLz7vYaW z1~7$pV@s#9WPgx?Du?s*qY<1MY9NDK4V4fRR!h}RL}cxuoy4VK1l;6uUZc&l)Cn4$ z3_HmMf=c^B++r_+5K=l2EMigMlRP9T6>cvo5)0ovYQ(}vZ_(_?)gG5p+onaD3nmv# ztgET4m{2+%r3!9iPV|^sJ$WdPfm)(bLGL%96LIJ2Xik;#U%%SF|5wM3iR0FX`uZN) zDvxuPRkk$sx>8fpTq%cVXL@qo*}1oFT-?x5TeH$qcjddskN@SWtNwEQ_`6rG+4$_9 z3!lB^X$eG<@4*B7pC`Rrl+PPr{B2Ela=M+kK|_Qe}iT`0rce4=Bf{9Z-cvp z#a!iLQLtXJUMSoaBWoNQ>;!VfaN2p*Ye`yuNK2wPwYN#d5^9#hK2)w!Lk7R77tbz* z5F_!0NgqAOWQp7(lG&b1qo`Sus?DIQS|21 zYYSpzQuH7imf$9g6@frCY6})S98^P__N5pd7-bZ33v^8bql%%}reg{Vib0+^GGklo zth;WXeQKi1o|^A;RPU@?vT9{-O=*TZJb&ThuI_1Ku#I1sVX2$6scr8h*P7WE9`zQ) z=cHtmYuR~ICb!l#=lYvknkIW%XvAAQ2|hoK_{)gXK53yi?R>F=(i5>ek&}mQc?G`S z6Bf0n#7;5d_$**RqW@gYPxC>^)~`-YMA&vRUuHR46!v5%yy-;DHmR3*$Q$|&br6<> zGRonI1E9lfLK(Lr{ZVxh&A7=aECdhE6eUw6w;@j|h`JoLh*CHCG~rC)kO&j#G*Ksc zE2QrRE(yWs-t^{$m2h6mlWW&Z^%f~bUUBGM-f_c9oT>Nb`3HYWEd9i)edYSibf!H< z$DucttY7!gTE10UvkoJgZaMI10Ym03_&$0~#dJs39gw!)0+yHNPa7w?bhab$QGK2VafVL1CyS_rS&)Q*yOhy(KdQp;29EviVDkWb5 zv!mq-ra7Dr&3Op&2sHyZThAPt&Cv@Ftm=d*$L6#SAo$6)irE~tx!v6r72Q4EJr!LQ zowH{m7dbPl)bFdLa3{eLByWzLTw#Wh@i+nSyW#TnWLbcUNL$xr;8MVg^cC90)&4Y9 zNlDMprxq7KaBX6|J1N2ai6bE?{Z7F1j>N%@HitVSF;V>@{oRtDp1w3YFE2-bDLu<- z=aZB2GdyKWe_;8w@?utP4gbMxTW&^DdfX+J)Rdv`WaTucr`d;oq~)fkraF{LEq5q4 zGdDNuCOjqmCT~GfS{&b-mYS2UU&EdHhfsqtw}sj(#@sGv=fun=$Y3*ZHr*)1I|ABC8bA3iU!WQQ| z_}R~hv<}~ndPsJghB8R@IGa80Aob&OJp}r$tD>S}dBw6`3STX%lPgVDxc}u!XQWHP z9h3#xkfUO*yAa%1ae!4R=RfzVugWgc{e{`wRZ-H~9BA`^0J2jmFPXgclGe8G@`UWX z@fUyFsKp<=IV0oZvPlK*cM7K!RW_z1EhK~OzRWmpVArb1#>RW5WS}y)#?r<9+6hVC z$u_|*_mQB2Y!equfkr{tU@LbduEj)K!E)2Ua=T2ZfOp#%JlB!vOmsL8C4+%mP-lJE ztpp~VsIY+)p6G~A>`mg%1RfXfjH3&Q@vB&TVmui0eDi_K4i}!CAA&9eR(_C;_gK5Z z+du2Ey^%l{V9J)wU7NZtSlz#3S?{8GJu}*xVe*IqmsF(Li{<}6fEN_%7b?jxD*ziX zBe=%n1-v?qFWWk4Mg8O}FQ0s%GR>BlV{;Vum9@|9o?RM9P4%|ToIPb~gIN2Dizcd0 z!4`(uLt@!d+Sa4F!#V!C#j>+%Y15kOw0X^|x98?KT?uJn)s8Yr#tjes- zz<iXHv`dMw~gQFi{BfXvQs39Z4ju zYY@OBe-Db9c>R!D@S>nR_bM`0hA9)lKdK>K(ZW$gV%_i!v;fLm;A>bW;~BG8Kh^pZ&EWt=uU&%JP-HkX;t;l=0vdY(2DqirhAy?&lH z{bn0BU_M6~)gPPRY{Ms*&oSE&a>BTF03Ak1v-9 z3Up=jsH`IWG_n8Kqg^uZo#}nrFku5>w<725_o>`q!zm~4)8;CE9swP(ArM_$(bX9H9O{p3WAwDbfN z{v=*VHBky+pWsJ;BV<~m(ho}tJoOd}BZPrOgB&yg7KKR%?T9qTHbIC@C{$S+~4WWO!Wkl z7q^O!`?}m&X}Lq+yKm_%{?`TDwr@w>vDtdM_DW8w`%uqYN2v7JysZVu`A^9_*z?vE z#GdAED@c1DWyqeqNO@`EU@uCMJ$2Dg(=de+x@a({Z|okHR6N{fJYFE%5cP@YM5!;9 zU&uB@S@O3`%Hk$vVaEtBHFiUo?PN}l7sSvN!&W2E4*10u@Cy_ULG`q1HU%;WwXamW zf)^Swy#GShzJmTO;&mwmnBsxK@K9hJ1`aR`+*gQ<2ye)XEch~{&A7BQr|7rJKbLZZ*`%kYEYmnCUTW_Uv#rgVAH}BfE{io6 zh0Eq{+Q07Uy@)~b9Xx)uD1WqS{qD`{b|M;@X8CyUS9Vx}OK)4X^k_-ZMpY_ zQFZhQWznnPqS33!w=|xMr8TcwGt8@ef(@1t){YgwM12hVpatdJ*G5*=a;rU)Lm}-T zA1%~dLa$_kgbU1)3^s)X@`0o{)HbvCCsMvxNoPD(Oa&!~B=Sh4v5~RJ?8xl4X^kz7 z&66hL+4UjNGGg_;0SRA4LU}3}j8J8;G{Wy17O_48p~}^W@+jg?SPLW5T)5r^cdAwY zc8wL7b-U$D6?2MHT=`iUIrW#%ysM|;x}L5*k&l1b&|bE1LYg}_GsC_A`kST|Zl5u| zE1s9yT3hwNjgzVyme#6|{rTsaa@qLlostlX@cx-U%ajO>|n-$Smot9M5g7Y zZ`{>5@wyGWH*ME%T(hPz(zvz-(F({HDil>4<`Sx{1gZ@o3EPqSe?YZJ05MT*$^Qz~ zj(h-6ZBKW{Y!lVGDZ@^n+RuiyhKc)gp;}Kt+t=5g2iER;YPDHBLc-e7B__U(D7e}w z_VeP}!>?X!R+TX0;YQS+0A-762Vx)1jM+z{ukL)8c7eP~`$)9q#p%OWX~)PX3^?hk z%v;9(lzGc+>Ko%-mHSh+#dBp_V}Hu4(adK=Ak#j-274}Ise#Ox%I=FKRu>lJrdSj$ z37CTH3prr8V2c_|4uau|WN{pf+H*lfLRqaQw}Z`*iX(tG8tsuX1EmQ163_!=4ZYVQ zrZwylhy{V&SkbP)o`xCOXdl~(7-&sR&D5IK38B1TNS4HfRyl0)u5yP2fRL`Q`WO$G z2sm>I8<9;h;H?bxb5(1R{jVQA`uctx_wJc><@+g#shLiD$qi+l!Ng2oO6tXZe={z9 zdC7_H?h`9kh@(1D-+koO9Xnn!;RG0ea2>%}P%ywxQKAZ-WW-SS;*WR-iAC$l%_W;qZc%Qb z--}o0$d|Rmqaue0OgGhkfCAGoA~pLM(*V9xZm+CtXu!V_4rBf6##Jrfb;_YTKYEjT#EMA4chCp!1*ha*ag+wrEAk1AtC}{o_4u12ln5I zjN9t+0e5b$DBHe}Juyl(D zkU-UU{w!X^1|wWZt_+z(Vk#(&lf!%e`HLvUB=K5AY{SShO3|64`IIv!agVTCTHa@2 zw&mQb0>{I88H?j_r$AzeYB3QLa6PU{d*?=674}YEYT6fLbY_X+mYH|*QiQDH-7r7~ zFF-GtfylQjo|ugyHYiL?e7zVb|NBq-t2?tvwF*322xr?gB$-7zQ@DS=>E7JJud6^4@y5TYVcu9=lp9X|hS zXRy`GYLkBMMc@9~S?sTwY*+IfN=iz5@A3=gbQ~l$a{gD*6h>cK(>$~kZ>33_x3AtG zN>S=kZLTb9_J&2Xt`J(4!ul*ieUxjjzn=rt-JRG6W3;CN1(iWeZo!!Qi}dMy~0wI5Vm z(iw;F9t9!tvM!ul(5BCMpS>TZOTY05=<`%rPIh#P`2>VmC=LKfC|R-#uziAZMs;Ak z2Sr*w|1+o}gX{~7ixZ4mxHcx7(tt_W#a}TT^aSqm|v?a)wG?QE( zCY?|WV6Pe{P7u0lOsPA;2S9-;n1IiqMy+R*xyJwBUcdlMRc|#~~w^P)) z(_e~GQ1=ZTz8T}zxr^4pGl=dxF1ILrM|)hnEy&RFQw4Vjjo}WWI-B6gZDI%R92&m! zG8P6RDE!nr2^AKDnJAQr8U4+E+*!ubKlM)HW#Tt^nLwnnx0mB8ddY3P#I&i#a3tB= zJ<&T$#GP}#PUmTnxN_ttcrYZ8vXxn)i#qZMq9N-ChV$_R&6dap7p4&@A$6csIT8&xx>YhF9B3=}-nl9q)iE2(=|auQqyp5-8W{6vT@9L2<<8W2lvc zbi?PmNfMlRfJr`vGFBIRKX^HUvSWT+{^a;~%R*~5fHRwwbb@Htji;RdUKCn8|82vx zj(~(%rVnawfCaQ+MnfB{m-Vd9kGdID($Gs$LD53X1QpEk0Rnrwhl5s4V;PveX{}_V z^gi|PW_^}Pw=CQ9%*Kt+?0ICb{u3g2UM}h0=h;jj^~%d;&%SKlyo*{|E}Ex)fABe4 z_WWR5=e*6Y9y|8x=93~ZMj(a!ps15I_{6NMSFXHzRu>9y9_u3RO4QyV@g9;^$!=RX zhNBBBfWUiO#QVvi^_zD>))QZb?fLw7imGL0=AA$ZbAU_eZ8Hlvlm6pY24AMdwgVzO z9>E~%K?HFcB0;zT`>?h$naS755Czqr1ml^%GXbW2<6KmeA!3|LPl9ihJfZ*VM@42w zpJGDFQBMRDH0K4HrW4xalf)cMH5TQi zM>sZJ+6d{uNtXH$q%Xu=-gEpo4JL=FL`>HnS*d{)6Jyw`1YSy!6 z))i?lgRO>Q7qRwaq=6|_u_5}7s;p@-FB^dth9I+kC}>5NF0s>^8<4L?J1ta*;5(G< zBUNhL0-H7yFx8C!UBe>h)9P;KOkUVt?5}Ui%#{GWyS;ttXB6U0XzbZn8wfi!e>gpp zAo+|drX|GI=8WzTLJ`3(^frR9}cI4}ltzABd-jH_w z$B-16JmxXrA2ti37R3|BeOydmfY?TNBYHoI@R_#*@F_-LhHoYi6~NpiGZyJa(WAEH zmShz2tg0w23Hb70ipTUN)+HJh76>U(xI7^x#U)}W45t@`TNAa5@O0Q3+`@_w3%W*? z22+0RQ(El4UAflXNu@>q!%Cl)yE8Mic>T8p+3^Y7AJP;5hlM|l-Ejak?!$Y`iBgGP zM^kDiq|;IBA0cCk&wa>ua0M0ulO}hF2yi9(cCZC_ZS<YE8#wdEl6#AoIqL|^0Ug0>GFB25`virn)-Nt>TV9fWUUo1FO!T(!& zE-xV@FeKh5LT9mW7?f3fL<9Qj5^<()-?RzS&mg2lE!Ix+6iSK9e@xji-1f)u0^R6WSrhHKp=`kpO7riSU8#`V>78q)}VnYYU-UDk7 z3c1=)$aU;(qkn*;DhQ#IbLLOaQ9iixpG2j~WpqTX#DCD=e;B11=B2})zCqtaJKjSO2ojL9B;@g(M$T=aND#-A`(`uoyG zF~T~ge?-$b{5nedbPlho6chRx;3SwoxS3|;>zE6wqMHD((Tj36O6V0Ah$dYNIV1hw z#Z-nt&||PO-ibG*M&BE1=qAvkkpr$FugTDn;RV^tad_KzWPSLuD)0GDsiFD7i29mX)uvrD60zt>n56mbtV1hci-+Hp1b77{Jub#JqED`VRG~RhbSsD~i=0IQtSNd)8wLgZvqY|+spVC^qB30fg zkVFxAizl4#9>l5|Z&i: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 Zml!TZoI3*L5^T`t!ca@8AE_7F{|CV3TfqPT diff --git a/addons/gut/fonts/LobsterTwo-Bold.ttf b/addons/gut/fonts/LobsterTwo-Bold.ttf deleted file mode 100644 index 2e979fbff2caae0ac51c9eb23d63d3f10cfc3b3b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 222040 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 diff --git a/addons/gut/fonts/LobsterTwo-Italic.ttf b/addons/gut/fonts/LobsterTwo-Italic.ttf deleted file mode 100644 index b88ec1723c6cc760834716848772f4ca035cd5ea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 229404 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 diff --git a/addons/gut/fonts/LobsterTwo-Regular.ttf b/addons/gut/fonts/LobsterTwo-Regular.ttf deleted file mode 100644 index 556c45e7d1d6e89bcb94c012d14707cd97d4718b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 234956 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 l=zbo|8hLz${wQW+TahP%4s5 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 BGSdJ6 diff --git a/addons/gut/fonts/OFL.txt b/addons/gut/fonts/OFL.txt deleted file mode 100644 index 3ed0152..0000000 --- a/addons/gut/fonts/OFL.txt +++ /dev/null @@ -1,94 +0,0 @@ -Copyright (c) 2009, Mark Simonson (http://www.ms-studio.com, mark@marksimonson.com), -with Reserved Font Name Anonymous Pro. - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -http://scripts.sil.org/OFL - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/addons/gut/gut.gd b/addons/gut/gut.gd deleted file mode 100644 index fc56ce9..0000000 --- a/addons/gut/gut.gd +++ /dev/null @@ -1,1566 +0,0 @@ -# ############################################################################## -#(G)odot (U)nit (T)est class -# -# ############################################################################## -# The MIT License (MIT) -# ===================== -# -# Copyright (c) 2020 Tom "Butch" Wesley -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ############################################################################## -# View readme for usage details. -# ############################################################################## -extends Control - -# -- Settings -- -var _select_script = '' -var _tests_like = '' -var _inner_class_name = '' -var _should_maximize = false setget set_should_maximize, get_should_maximize -var _log_level = 1 setget set_log_level, get_log_level -var _disable_strict_datatype_checks = false setget disable_strict_datatype_checks, is_strict_datatype_checks_disabled -var _test_prefix = 'test_' -var _file_prefix = 'test_' -var _file_extension = '.gd' -var _inner_class_prefix = 'Test' -var _temp_directory = 'user://gut_temp_directory' -var _export_path = '' setget set_export_path, get_export_path -var _include_subdirectories = false setget set_include_subdirectories, get_include_subdirectories -var _double_strategy = 1 setget set_double_strategy, get_double_strategy -var _pre_run_script = '' setget set_pre_run_script, get_pre_run_script -var _post_run_script = '' setget set_post_run_script, get_post_run_script -var _color_output = false setget set_color_output, get_color_output -# -- End Settings -- - - -# ########################### -# Other Vars -# ########################### -const LOG_LEVEL_FAIL_ONLY = 0 -const LOG_LEVEL_TEST_AND_FAILURES = 1 -const LOG_LEVEL_ALL_ASSERTS = 2 -const WAITING_MESSAGE = '/# waiting #/' -const PAUSE_MESSAGE = '/# Pausing. Press continue button...#/' -const COMPLETED = 'completed' - -var _utils = load('res://addons/gut/utils.gd').get_instance() -var _lgr = _utils.get_logger() -var _strutils = _utils.Strutils.new() -# Used to prevent multiple messages for deprecated setup/teardown messages -var _deprecated_tracker = _utils.ThingCounter.new() - -# The instance that is created from _pre_run_script. Accessible from -# get_pre_run_script_instance. -var _pre_run_script_instance = null -var _post_run_script_instance = null # This is not used except in tests. - - -var _script_name = null -var _test_collector = _utils.TestCollector.new() - -# The instanced scripts. This is populated as the scripts are run. -var _test_script_objects = [] - -var _waiting = false -var _done = false -var _is_running = false - -var _current_test = null -var _log_text = "" - -var _pause_before_teardown = false -# when true _pause_before_teardown will be ignored. useful -# when batch processing and you don't want to watch. -var _ignore_pause_before_teardown = false -var _wait_timer = Timer.new() - -var _yield_between = { - should = false, - timer = Timer.new(), - after_x_tests = 5, - tests_since_last_yield = 0 -} - -var _was_yield_method_called = false -# used when yielding to gut instead of some other -# signal. Start with set_yield_time() -var _yield_timer = Timer.new() - -var _unit_test_name = '' -var _new_summary = null - -var _yielding_to = { - obj = null, - signal_name = '' -} - -var _stubber = _utils.Stubber.new() -var _doubler = _utils.Doubler.new() -var _spy = _utils.Spy.new() -var _gui = null -var _orphan_counter = _utils.OrphanCounter.new() -var _autofree = _utils.AutoFree.new() - -# This is populated by test.gd each time a paramterized test is encountered -# for the first time. -var _parameter_handler = null - -# Used to cancel importing scripts if an error has occurred in the setup. This -# prevents tests from being run if they were exported and ensures that the -# error displayed is seen since importing generates a lot of text. -var _cancel_import = false - -# Used for proper assert tracking and printing during before_all -var _before_all_test_obj = load('res://addons/gut/test_collector.gd').Test.new() -# Used for proper assert tracking and printing during after_all -var _after_all_test_obj = load('res://addons/gut/test_collector.gd').Test.new() - -const SIGNAL_TESTS_FINISHED = 'tests_finished' -const SIGNAL_STOP_YIELD_BEFORE_TEARDOWN = 'stop_yield_before_teardown' - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -var _should_print_versions = true # used to cut down on output in tests. -func _init(): - _before_all_test_obj.name = 'before_all' - _after_all_test_obj.name = 'after_all' - # When running tests for GUT itself, _utils has been setup to always return - # a new logger so this does not set the gut instance on the base logger - # when creating test instances of GUT. - _lgr.set_gut(self) - - add_user_signal(SIGNAL_TESTS_FINISHED) - add_user_signal('test_finished') - add_user_signal(SIGNAL_STOP_YIELD_BEFORE_TEARDOWN) - add_user_signal('timeout') - - _doubler.set_output_dir(_temp_directory) - _doubler.set_stubber(_stubber) - _doubler.set_spy(_spy) - _doubler.set_gut(self) - - # TODO remove these, universal logger should fix this. - _doubler.set_logger(_lgr) - _spy.set_logger(_lgr) - _stubber.set_logger(_lgr) - _test_collector.set_logger(_lgr) - - _gui = load('res://addons/gut/GutScene.tscn').instance() - -# ------------------------------------------------------------------------------ -# Initialize controls -# ------------------------------------------------------------------------------ -func _ready(): - if(!_utils.is_version_ok()): - _print_versions() - push_error(_utils.get_bad_version_text()) - print('Error: ', _utils.get_bad_version_text()) - get_tree().quit() - return - - if(_should_print_versions): - _lgr.info(str('using [', OS.get_user_data_dir(), '] for temporary output.')) - - set_process_input(true) - - add_child(_wait_timer) - _wait_timer.set_wait_time(1) - _wait_timer.set_one_shot(true) - - add_child(_yield_between.timer) - _wait_timer.set_one_shot(true) - - add_child(_yield_timer) - _yield_timer.set_one_shot(true) - _yield_timer.connect('timeout', self, '_yielding_callback') - - _setup_gui() - - if(_select_script != null): - select_script(_select_script) - - if(_tests_like != null): - set_unit_test_name(_tests_like) - - if(_should_maximize): - # GUI checks for is_in_tree will not pass yet. - call_deferred('maximize') - - # hide the panel that IS gut so that only the GUI is seen - self.self_modulate = Color(1,1,1,0) - show() - _print_versions() - -# ------------------------------------------------------------------------------ -# Runs right before free is called. Can't override `free`. -# ------------------------------------------------------------------------------ -func _notification(what): - if(what == NOTIFICATION_PREDELETE): - for test_script in _test_script_objects: - if(is_instance_valid(test_script)): - test_script.free() - - _test_script_objects = [] - - if(is_instance_valid(_gui)): - _gui.free() - -func _print_versions(send_all = true): - if(!_should_print_versions): - return - - var info = _utils.get_version_text() - - if(send_all): - p(info) - else: - var printer = _lgr.get_printer('gui') - printer.send(info + "\n") - - -# ############################################################################## -# -# GUI Events and setup -# -# ############################################################################## -func _setup_gui(): - # This is how we get the size of the control to translate to the gui when - # the scene is run. This is also another reason why the min_rect_size - # must match between both gut and the gui. - _gui.rect_size = self.rect_size - add_child(_gui) - _gui.set_anchor(MARGIN_RIGHT, ANCHOR_END) - _gui.set_anchor(MARGIN_BOTTOM, ANCHOR_END) - _gui.connect('run_single_script', self, '_on_run_one') - _gui.connect('run_script', self, '_on_new_gui_run_script') - _gui.connect('end_pause', self, '_on_new_gui_end_pause') - _gui.connect('ignore_pause', self, '_on_new_gui_ignore_pause') - _gui.connect('log_level_changed', self, '_on_log_level_changed') - var _foo = connect('tests_finished', _gui, 'end_run') - -func _add_scripts_to_gui(): - var scripts = [] - for i in range(_test_collector.scripts.size()): - var s = _test_collector.scripts[i] - var txt = '' - if(s.has_inner_class()): - txt = str(' - ', s.inner_class_name, ' (', s.tests.size(), ')') - else: - txt = str(s.get_full_name(), ' (', s.tests.size(), ')') - scripts.append(txt) - _gui.set_scripts(scripts) - -func _on_run_one(index): - clear_text() - var indexes = [index] - if(!_test_collector.scripts[index].has_inner_class()): - indexes = _get_indexes_matching_path(_test_collector.scripts[index].path) - _test_the_scripts(indexes) - -func _on_new_gui_run_script(index): - var indexes = [] - clear_text() - for i in range(index, _test_collector.scripts.size()): - indexes.append(i) - _test_the_scripts(indexes) - -func _on_new_gui_end_pause(): - _pause_before_teardown = false - emit_signal(SIGNAL_STOP_YIELD_BEFORE_TEARDOWN) - -func _on_new_gui_ignore_pause(should): - _ignore_pause_before_teardown = should - -func _on_log_level_changed(value): - set_log_level(value) - -##################### -# -# Events -# -##################### - -# ------------------------------------------------------------------------------ -# Timeout for the built in timer. emits the timeout signal. Start timer -# with set_yield_time() -# ------------------------------------------------------------------------------ -func _yielding_callback(from_obj=false): - _lgr.end_yield() - if(_yielding_to.obj): - _yielding_to.obj.call_deferred( - "disconnect", - _yielding_to.signal_name, self, - '_yielding_callback') - _yielding_to.obj = null - _yielding_to.signal_name = '' - - if(from_obj): - # we must yiled for a little longer after the signal is emitted so that - # the signal can propagate to other objects. This was discovered trying - # to assert that obj/signal_name was emitted. Without this extra delay - # the yield returns and processing finishes before the rest of the - # objects can get the signal. This works b/c the timer will timeout - # and come back into this method but from_obj will be false. - _yield_timer.set_wait_time(.1) - _yield_timer.start() - else: - emit_signal('timeout') - -# ------------------------------------------------------------------------------ -# completed signal for GDScriptFucntionState returned from a test script that -# has yielded -# ------------------------------------------------------------------------------ -func _on_test_script_yield_completed(): - _waiting = false - -##################### -# -# Private -# -##################### -func _log_test_children_warning(test_script): - if(!_lgr.is_type_enabled(_lgr.types.orphan)): - return - - var kids = test_script.get_children() - if(kids.size() > 0): - var msg = '' - if(_log_level == 2): - msg = "Test script still has children when all tests finisehd.\n" - for i in range(kids.size()): - msg += str(" ", _strutils.type2str(kids[i]), "\n") - msg += "You can use autofree, autoqfree, add_child_autofree, or add_child_autoqfree to automatically free objects." - else: - msg = str("Test script has ", kids.size(), " unfreed children. Increase log level for more details.") - - - _lgr.warn(msg) - -# ------------------------------------------------------------------------------ -# Convert the _summary dictionary into text -# ------------------------------------------------------------------------------ -func _print_summary(): - _lgr.log("\n\n*** Run Summary ***", _lgr.fmts.yellow) - - _new_summary.log_summary_text(_lgr) - - var logger_text = '' - if(_lgr.get_errors().size() > 0): - logger_text += str("\n* ", _lgr.get_errors().size(), ' Errors.') - if(_lgr.get_warnings().size() > 0): - logger_text += str("\n* ", _lgr.get_warnings().size(), ' Warnings.') - if(_lgr.get_deprecated().size() > 0): - logger_text += str("\n* ", _lgr.get_deprecated().size(), ' Deprecated calls.') - if(logger_text != ''): - logger_text = "\nWarnings/Errors:" + logger_text + "\n\n" - _lgr.log(logger_text) - - if(_new_summary.get_totals().tests > 0): - var fmt = _lgr.fmts.green - var msg = str(_new_summary.get_totals().passing) + ' passed ' + str(_new_summary.get_totals().failing) + ' failed. ' + \ - str("Tests finished in ", _gui.elapsed_time_as_str()) - if(_new_summary.get_totals().failing > 0): - fmt = _lgr.fmts.red - elif(_new_summary.get_totals().pending > 0): - fmt = _lgr.fmts.yellow - - _lgr.log(msg, fmt) - else: - _lgr.log('No tests ran', _lgr.fmts.red) - - -func _validate_hook_script(path): - var result = { - valid = true, - instance = null - } - - # empty path is valid but will have a null instance - if(path == ''): - return result - - var f = File.new() - if(f.file_exists(path)): - var inst = load(path).new() - if(inst and inst is _utils.HookScript): - result.instance = inst - result.valid = true - else: - result.valid = false - _lgr.error('The hook script [' + path + '] does not extend res://addons/gut/hook_script.gd') - else: - result.valid = false - _lgr.error('The hook script [' + path + '] does not exist.') - - return result - - -# ------------------------------------------------------------------------------ -# Runs a hook script. Script must exist, and must extend -# res://addons/gut/hook_script.gd -# ------------------------------------------------------------------------------ -func _run_hook_script(inst): - if(inst != null): - inst.gut = self - inst.run() - return inst - -# ------------------------------------------------------------------------------ -# Initialize variables for each run of a single test script. -# ------------------------------------------------------------------------------ -func _init_run(): - var valid = true - _test_collector.set_test_class_prefix(_inner_class_prefix) - _test_script_objects = [] - _new_summary = _utils.Summary.new() - - _log_text = "" - - _current_test = null - - _is_running = true - - _yield_between.tests_since_last_yield = 0 - - var pre_hook_result = _validate_hook_script(_pre_run_script) - _pre_run_script_instance = pre_hook_result.instance - var post_hook_result = _validate_hook_script(_post_run_script) - _post_run_script_instance = post_hook_result.instance - - valid = pre_hook_result.valid and post_hook_result.valid - - return valid - - -# ------------------------------------------------------------------------------ -# Print out run information and close out the run. -# ------------------------------------------------------------------------------ -func _end_run(): - _gui.end_run() - _print_summary() - p("\n") - - # Do not count any of the _test_script_objects since these will be released - # when GUT is released. - _orphan_counter._counters.total += _test_script_objects.size() - if(_orphan_counter.get_counter('total') > 0 and _lgr.is_type_enabled('orphan')): - _orphan_counter.print_orphans('total', _lgr) - p("Note: This count does not include GUT objects that will be freed upon exit.") - p(" It also does not include any orphans created by global scripts") - p(" loaded before tests were ran.") - p(str("Total orphans = ", _orphan_counter.orphan_count())) - - if(!_utils.is_null_or_empty(_select_script)): - p('Ran Scripts matching "' + _select_script + '"') - if(!_utils.is_null_or_empty(_unit_test_name)): - p('Ran Tests matching "' + _unit_test_name + '"') - if(!_utils.is_null_or_empty(_inner_class_name)): - p('Ran Inner Classes matching "' + _inner_class_name + '"') - - # For some reason the text edit control isn't scrolling to the bottom after - # the summary is printed. As a workaround, yield for a short time and - # then move the cursor. I found this workaround through trial and error. - _yield_between.timer.set_wait_time(0.1) - _yield_between.timer.start() - yield(_yield_between.timer, 'timeout') - _gui.scroll_to_bottom() - - _is_running = false - update() - _run_hook_script(_post_run_script_instance) - emit_signal(SIGNAL_TESTS_FINISHED) - - _gui.set_title("Finished.") - - -# ------------------------------------------------------------------------------ -# Checks the passed in thing to see if it is a "function state" object that gets -# returned when a function yields. -# ------------------------------------------------------------------------------ -func _is_function_state(script_result): - return script_result != null and \ - typeof(script_result) == TYPE_OBJECT and \ - script_result is GDScriptFunctionState and \ - script_result.is_valid() - -# ------------------------------------------------------------------------------ -# Print out the heading for a new script -# ------------------------------------------------------------------------------ -func _print_script_heading(script): - if(_does_class_name_match(_inner_class_name, script.inner_class_name)): - var fmt = _lgr.fmts.underline - var divider = '-----------------------------------------' - - var text = '' - if(script.inner_class_name == null): - text = script.path - else: - text = script.path + '.' + script.inner_class_name - _lgr.log("\n\n" + text, fmt) - - if(!_utils.is_null_or_empty(_inner_class_name) and _does_class_name_match(_inner_class_name, script.inner_class_name)): - _lgr.log(str(' [',script.inner_class_name, '] matches [', _inner_class_name, ']'), fmt) - - if(!_utils.is_null_or_empty(_unit_test_name)): - _lgr.log(' Only running tests like: "' + _unit_test_name + '"', fmt) - - -# ------------------------------------------------------------------------------ -# Just gets more logic out of _test_the_scripts. Decides if we should yield after -# this test based on flags and counters. -# ------------------------------------------------------------------------------ -func _should_yield_now(): - var should = _yield_between.should and \ - _yield_between.tests_since_last_yield == _yield_between.after_x_tests - if(should): - _yield_between.tests_since_last_yield = 0 - else: - _yield_between.tests_since_last_yield += 1 - return should - -# ------------------------------------------------------------------------------ -# Yes if the class name is null or the script's class name includes class_name -# ------------------------------------------------------------------------------ -func _does_class_name_match(the_class_name, script_class_name): - return (the_class_name == null or the_class_name == '') or (script_class_name != null and script_class_name.findn(the_class_name) != -1) - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func _setup_script(test_script): - test_script.gut = self - test_script.set_logger(_lgr) - add_child(test_script) - _test_script_objects.append(test_script) - - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func _do_yield_between(time): - _yield_between.timer.set_wait_time(time) - _yield_between.timer.start() - return _yield_between.timer - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func _wait_for_done(result): - var iter_counter = 0 - var print_after = 3 - - # callback method sets waiting to false. - result.connect(COMPLETED, self, '_on_test_script_yield_completed') - if(!_was_yield_method_called): - _lgr.log('-- Yield detected, waiting --', _lgr.fmts.yellow) - - _was_yield_method_called = false - _waiting = true - _wait_timer.set_wait_time(0.4) - - var dots = '' - while(_waiting): - iter_counter += 1 - _lgr.yield_text('waiting' + dots) - _wait_timer.start() - yield(_wait_timer, 'timeout') - dots += '.' - if(dots.length() > 5): - dots = '' - - _lgr.end_yield() - -# ------------------------------------------------------------------------------ -# returns self so it can be integrated into the yield call. -# ------------------------------------------------------------------------------ -func _wait_for_continue_button(): - p(PAUSE_MESSAGE, 0) - _waiting = true - return self - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func _call_deprecated_script_method(script, method, alt): - if(script.has_method(method)): - var txt = str(script, '-', method) - if(!_deprecated_tracker.has(txt)): - # Removing the deprecated line. I think it's still too early to - # start bothering people with this. Left everything here though - # because I don't want to remember how I did this last time. - _lgr.deprecated(str('The method ', method, ' has been deprecated, use ', alt, ' instead.')) - _deprecated_tracker.add(txt) - script.call(method) - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func _get_indexes_matching_script_name(name): - var indexes = [] # empty runs all - for i in range(_test_collector.scripts.size()): - if(_test_collector.scripts[i].get_filename().find(name) != -1): - indexes.append(i) - return indexes - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func _get_indexes_matching_path(path): - var indexes = [] - for i in range(_test_collector.scripts.size()): - if(_test_collector.scripts[i].path == path): - indexes.append(i) - return indexes - -# ------------------------------------------------------------------------------ -# Execute all calls of a parameterized test. -# ------------------------------------------------------------------------------ -func _run_parameterized_test(test_script, test_name): - var script_result = _run_test(test_script, test_name) - if(_is_function_state(script_result)): - # _run_tests does _wait_for_done so just wait on it to complete - yield(script_result, COMPLETED) - - if(_parameter_handler == null): - _lgr.error(str('Parameterized test ', _current_test.name, ' did not call use_parameters for the default value of the parameter.')) - _fail(str('Parameterized test ', _current_test.name, ' did not call use_parameters for the default value of the parameter.')) - else: - while(!_parameter_handler.is_done()): - script_result = _run_test(test_script, test_name) - if(_is_function_state(script_result)): - # _run_tests does _wait_for_done so just wait on it to complete - yield(script_result, COMPLETED) - - _parameter_handler = null - - -# ------------------------------------------------------------------------------ -# Runs a single test given a test.gd instance and the name of the test to run. -# ------------------------------------------------------------------------------ -func _run_test(script_inst, test_name): - _lgr.log_test_name() - _lgr.set_indent_level(1) - _orphan_counter.add_counter('test') - var script_result = null - - _call_deprecated_script_method(script_inst, 'setup', 'before_each') - var before_each_result = script_inst.before_each() - if(_is_function_state(before_each_result)): - yield(_wait_for_done(before_each_result), COMPLETED) - - # When the script yields it will return a GDScriptFunctionState object - script_result = script_inst.call(test_name) - _new_summary.add_test(test_name) - - # Cannot detect future yields since we never tell the method to resume. If - # there was some way to tell the method to resume we could use what comes - # back from that to detect additional yields. I don't think this is - # possible since we only know what the yield was for except when yield_for - # and yield_to are used. - if(_is_function_state(script_result)): - yield(_wait_for_done(script_result), COMPLETED) - - # if the test called pause_before_teardown then yield until - # the continue button is pressed. - if(_pause_before_teardown and !_ignore_pause_before_teardown): - _gui.pause() - yield(_wait_for_continue_button(), SIGNAL_STOP_YIELD_BEFORE_TEARDOWN) - - script_inst.clear_signal_watcher() - - # call each post-each-test method until teardown is removed. - _call_deprecated_script_method(script_inst, 'teardown', 'after_each') - var after_each_result = script_inst.after_each() - if(_is_function_state(after_each_result)): - yield(_wait_for_done(after_each_result), COMPLETED) - - # Free up everything in the _autofree. Yield for a bit if we - # have anything with a queue_free so that they have time to - # free and are not found by the orphan counter. - var aqf_count = _autofree.get_queue_free_count() - _autofree.free_all() - if(aqf_count > 0): - yield(_do_yield_between(0.1), 'timeout') - - if(_log_level > 0): - _orphan_counter.print_orphans('test', _lgr) - - _doubler.get_ignored_methods().clear() - -# ------------------------------------------------------------------------------ -# Calls after_all on the passed in test script and takes care of settings so all -# logger output appears indented and with a proper heading -# -# Calls both pre-all-tests methods until prerun_setup is removed -# ------------------------------------------------------------------------------ -func _call_before_all(test_script): - _current_test = _before_all_test_obj - _current_test.has_printed_name = false - _lgr.inc_indent() - - # Next 3 lines can be removed when prerun_setup removed. - _current_test.name = 'prerun_setup' - _call_deprecated_script_method(test_script, 'prerun_setup', 'before_all') - _current_test.name = 'before_all' - - var result = test_script.before_all() - if(_is_function_state(result)): - yield(_wait_for_done(result), COMPLETED) - - _lgr.dec_indent() - _current_test = null - -# ------------------------------------------------------------------------------ -# Calls after_all on the passed in test script and takes care of settings so all -# logger output appears indented and with a proper heading -# -# Calls both post-all-tests methods until postrun_teardown is removed. -# ------------------------------------------------------------------------------ -func _call_after_all(test_script): - _current_test = _after_all_test_obj - _current_test.has_printed_name = false - _lgr.inc_indent() - - # Next 3 lines can be removed when postrun_teardown removed. - _current_test.name = 'postrun_teardown' - _call_deprecated_script_method(test_script, 'postrun_teardown', 'after_all') - _current_test.name = 'after_all' - - var result = test_script.after_all() - if(_is_function_state(result)): - yield(_wait_for_done(result), COMPLETED) - - - _lgr.dec_indent() - _current_test = null - -# ------------------------------------------------------------------------------ -# Run all tests in a script. This is the core logic for running tests. -# ------------------------------------------------------------------------------ -func _test_the_scripts(indexes=[]): - _orphan_counter.add_counter('total') - - _print_versions(false) - var is_valid = _init_run() - if(!is_valid): - _lgr.error('Something went wrong and the run was aborted.') - return - - _run_hook_script(_pre_run_script_instance) - if(_pre_run_script_instance!= null and _pre_run_script_instance.should_abort()): - _lgr.error('pre-run abort') - emit_signal(SIGNAL_TESTS_FINISHED) - return - - _gui.run_mode() - - var indexes_to_run = [] - if(indexes.size()==0): - for i in range(_test_collector.scripts.size()): - indexes_to_run.append(i) - else: - indexes_to_run = indexes - - _gui.set_progress_script_max(indexes_to_run.size()) # New way - _gui.set_progress_script_value(0) - - if(_doubler.get_strategy() == _utils.DOUBLE_STRATEGY.FULL): - _lgr.info("Using Double Strategy FULL as default strategy. Keep an eye out for weirdness, this is still experimental.") - - # loop through scripts - for test_indexes in range(indexes_to_run.size()): - var the_script = _test_collector.scripts[indexes_to_run[test_indexes]] - _orphan_counter.add_counter('script') - - if(the_script.tests.size() > 0): - _gui.set_title(the_script.get_full_name()) - _lgr.set_indent_level(0) - _print_script_heading(the_script) - _new_summary.add_script(the_script.get_full_name()) - - var test_script = the_script.get_new() - var script_result = null - _setup_script(test_script) - _doubler.set_strategy(_double_strategy) - - # yield between test scripts so things paint - if(_yield_between.should): - yield(_do_yield_between(0.01), 'timeout') - - # !!! - # Hack so there isn't another indent to this monster of a method. if - # inner class is set and we do not have a match then empty the tests - # for the current test. - # !!! - if(!_does_class_name_match(_inner_class_name, the_script.inner_class_name)): - the_script.tests = [] - else: - var before_all_result = _call_before_all(test_script) - if(_is_function_state(before_all_result)): - # _call_before_all calls _wait for done, just wait for that to finish - yield(before_all_result, COMPLETED) - - - _gui.set_progress_test_max(the_script.tests.size()) # New way - - # Each test in the script - for i in range(the_script.tests.size()): - _stubber.clear() - _spy.clear() - _doubler.clear_output_directory() - _current_test = the_script.tests[i] - script_result = null - - if((_unit_test_name != '' and _current_test.name.find(_unit_test_name) > -1) or - (_unit_test_name == '')): - - # yield so things paint - if(_should_yield_now()): - yield(_do_yield_between(0.001), 'timeout') - - if(_current_test.arg_count > 1): - _lgr.error(str('Parameterized test ', _current_test.name, - ' has too many parameters: ', _current_test.arg_count, '.')) - elif(_current_test.arg_count == 1): - script_result = _run_parameterized_test(test_script, _current_test.name) - else: - script_result = _run_test(test_script, _current_test.name) - - if(_is_function_state(script_result)): - # _run_test calls _wait for done, just wait for that to finish - yield(script_result, COMPLETED) - - if(_current_test.assert_count == 0 and !_current_test.pending): - _lgr.warn('Test did not assert') - _current_test.has_printed_name = false - _gui.set_progress_test_value(i + 1) - emit_signal('test_finished') - - - _current_test = null - _lgr.dec_indent() - _orphan_counter.print_orphans('script', _lgr) - - if(_does_class_name_match(_inner_class_name, the_script.inner_class_name)): - var after_all_result = _call_after_all(test_script) - if(_is_function_state(after_all_result)): - # _call_after_all calls _wait for done, just wait for that to finish - yield(after_all_result, COMPLETED) - - - _log_test_children_warning(test_script) - # This might end up being very resource intensive if the scripts - # don't clean up after themselves. Might have to consolidate output - # into some other structure and kill the script objects with - # test_script.free() instead of remove child. - remove_child(test_script) - - _lgr.set_indent_level(0) - if(test_script.get_assert_count() > 0): - var script_sum = str(test_script.get_pass_count(), '/', test_script.get_assert_count(), ' passed.') - _lgr.log(script_sum, _lgr.fmts.bold) - - _gui.set_progress_script_value(test_indexes + 1) # new way - # END TEST SCRIPT LOOP - - _lgr.set_indent_level(0) - _end_run() - - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func _pass(text=''): - _gui.add_passing() # increments counters - if(_current_test): - _current_test.assert_count += 1 - _new_summary.add_pass(_current_test.name, text) - else: - if(_new_summary != null): # b/c of tests. - _new_summary.add_pass('script level', text) - - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func _fail(text=''): - _gui.add_failing() # increments counters - if(_current_test != null): - var line_text = ' at line ' + str(_extract_line_number(_current_test)) - p(line_text, LOG_LEVEL_FAIL_ONLY) - # format for summary - line_text = "\n " + line_text - var call_count_text = '' - if(_parameter_handler != null): - call_count_text = str('(call #', _parameter_handler.get_call_count(), ') ') - _new_summary.add_fail(_current_test.name, call_count_text + text + line_text) - _current_test.passed = false - _current_test.assert_count += 1 - else: - if(_new_summary != null): # b/c of tests. - _new_summary.add_fail('script level', text) - - -# ------------------------------------------------------------------------------ -# Extracts the line number from curren stacktrace by matching the test case name -# ------------------------------------------------------------------------------ -func _extract_line_number(current_test): - var line_number = -1 - # if stack trace available than extraxt the test case line number - var stackTrace = get_stack() - if(stackTrace!=null): - for index in stackTrace.size(): - var line = stackTrace[index] - var function = line.get("function") - if function == current_test.name: - line_number = line.get("line") - return line_number - - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func _pending(text=''): - if(_current_test): - _current_test.pending = true - _new_summary.add_pending(_current_test.name, text) - - -# ------------------------------------------------------------------------------ -# Gets all the files in a directory and all subdirectories if get_include_subdirectories -# is true. The files returned are all sorted by name. -# ------------------------------------------------------------------------------ -func _get_files(path, prefix, suffix): - var files = [] - var directories = [] - - var d = Directory.new() - d.open(path) - # true parameter tells list_dir_begin not to include "." and ".." directories. - d.list_dir_begin(true) - - # Traversing a directory is kinda odd. You have to start the process of listing - # the contents of a directory with list_dir_begin then use get_next until it - # returns an empty string. Then I guess you should end it. - var fs_item = d.get_next() - var full_path = '' - while(fs_item != ''): - full_path = path.plus_file(fs_item) - - #file_exists returns fasle for directories - if(d.file_exists(full_path)): - if(fs_item.begins_with(prefix) and fs_item.ends_with(suffix)): - files.append(full_path) - elif(get_include_subdirectories() and d.dir_exists(full_path)): - directories.append(full_path) - - fs_item = d.get_next() - d.list_dir_end() - - for dir in range(directories.size()): - var dir_files = _get_files(directories[dir], prefix, suffix) - for i in range(dir_files.size()): - files.append(dir_files[i]) - - files.sort() - return files - - -######################### -# -# public -# -######################### - -# ------------------------------------------------------------------------------ -# Conditionally prints the text to the console/results variable based on the -# current log level and what level is passed in. Whenever currently in a test, -# the text will be indented under the test. It can be further indented if -# desired. -# -# The first time output is generated when in a test, the test name will be -# printed. -# -# NOT_USED_ANYMORE was indent level. This was deprecated in 7.0.0. -# ------------------------------------------------------------------------------ -func p(text, level=0, NOT_USED_ANYMORE=-123): - if(NOT_USED_ANYMORE != -123): - _lgr.deprecated('gut.p no longer supports the optional 3rd parameter for indent_level parameter.') - var str_text = str(text) - - if(level <= _utils.nvl(_log_level, 0)): - _lgr.log(str_text) - -################ -# -# RUN TESTS/ADD SCRIPTS -# -################ -func get_minimum_size(): - return Vector2(810, 380) - - -# ------------------------------------------------------------------------------ -# Runs all the scripts that were added using add_script -# ------------------------------------------------------------------------------ -func test_scripts(run_rest=false): - clear_text() - - if(_script_name != null and _script_name != ''): - var indexes = _get_indexes_matching_script_name(_script_name) - if(indexes == []): - _lgr.error('Could not find script matching ' + _script_name) - else: - _test_the_scripts(indexes) - else: - _test_the_scripts([]) - -# alias -func run_tests(run_rest=false): - test_scripts(run_rest) - - -# ------------------------------------------------------------------------------ -# Runs a single script passed in. -# ------------------------------------------------------------------------------ -func test_script(script): - _test_collector.set_test_class_prefix(_inner_class_prefix) - _test_collector.clear() - _test_collector.add_script(script) - _test_the_scripts() - - -# ------------------------------------------------------------------------------ -# Adds a script to be run when test_scripts called. -# ------------------------------------------------------------------------------ -func add_script(script): - if(!Engine.is_editor_hint()): - _test_collector.set_test_class_prefix(_inner_class_prefix) - _test_collector.add_script(script) - _add_scripts_to_gui() - - -# ------------------------------------------------------------------------------ -# Add all scripts in the specified directory that start with the prefix and end -# with the suffix. Does not look in sub directories. Can be called multiple -# times. -# ------------------------------------------------------------------------------ -func add_directory(path, prefix=_file_prefix, suffix=_file_extension): - # check for '' b/c the calls to addin the exported directories 1-6 will pass - # '' if the field has not been populated. This will cause res:// to be - # processed which will include all files if include_subdirectories is true. - if(path == '' or path == null): - return - - var d = Directory.new() - if(!d.dir_exists(path)): - _lgr.error(str('The path [', path, '] does not exist.')) - OS.exit_code = 1 - else: - var files = _get_files(path, prefix, suffix) - for i in range(files.size()): - add_script(files[i]) - - -# ------------------------------------------------------------------------------ -# This will try to find a script in the list of scripts to test that contains -# the specified script name. It does not have to be a full match. It will -# select the first matching occurrence so that this script will run when run_tests -# is called. Works the same as the select_this_one option of add_script. -# -# returns whether it found a match or not -# ------------------------------------------------------------------------------ -func select_script(script_name): - _script_name = script_name - - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func export_tests(path=_export_path): - if(path == null): - _lgr.error('You must pass a path or set the export_path before calling export_tests') - else: - var result = _test_collector.export_tests(path) - if(result): - p(_test_collector.to_s()) - p("Exported to " + path) - - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func import_tests(path=_export_path): - if(!_utils.file_exists(path)): - _lgr.error(str('Cannot import tests: the path [', path, '] does not exist.')) - else: - _test_collector.clear() - var result = _test_collector.import_tests(path) - if(result): - p(_test_collector.to_s()) - p("Imported from " + path) - _add_scripts_to_gui() - - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func import_tests_if_none_found(): - if(!_cancel_import and _test_collector.scripts.size() == 0): - import_tests() - - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func export_if_tests_found(): - if(_test_collector.scripts.size() > 0): - export_tests() - -################ -# -# MISC -# -################ - - -# ------------------------------------------------------------------------------ -# Maximize test runner window to fit the viewport. -# ------------------------------------------------------------------------------ -func set_should_maximize(should): - _should_maximize = should - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func get_should_maximize(): - return _should_maximize - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func maximize(): - _gui.maximize() - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func disable_strict_datatype_checks(should): - _disable_strict_datatype_checks = should - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func is_strict_datatype_checks_disabled(): - return _disable_strict_datatype_checks - -# ------------------------------------------------------------------------------ -# Pauses the test and waits for you to press a confirmation button. Useful when -# you want to watch a test play out onscreen or inspect results. -# ------------------------------------------------------------------------------ -func end_yielded_test(): - _lgr.deprecated('end_yielded_test is no longer necessary, you can remove it.') - -# ------------------------------------------------------------------------------ -# Clears the text of the text box. This resets all counters. -# ------------------------------------------------------------------------------ -func clear_text(): - _gui.clear_text() - update() - -# ------------------------------------------------------------------------------ -# Get the number of tests that were ran -# ------------------------------------------------------------------------------ -func get_test_count(): - return _new_summary.get_totals().tests - -# ------------------------------------------------------------------------------ -# Get the number of assertions that were made -# ------------------------------------------------------------------------------ -func get_assert_count(): - var t = _new_summary.get_totals() - return t.passing + t.failing - -# ------------------------------------------------------------------------------ -# Get the number of assertions that passed -# ------------------------------------------------------------------------------ -func get_pass_count(): - return _new_summary.get_totals().passing - -# ------------------------------------------------------------------------------ -# Get the number of assertions that failed -# ------------------------------------------------------------------------------ -func get_fail_count(): - return _new_summary.get_totals().failing - -# ------------------------------------------------------------------------------ -# Get the number of tests flagged as pending -# ------------------------------------------------------------------------------ -func get_pending_count(): - return _new_summary.get_totals().pending - -# ------------------------------------------------------------------------------ -# Get the results of all tests ran as text. This string is the same as is -# displayed in the text box, and similar to what is printed to the console. -# ------------------------------------------------------------------------------ -func get_result_text(): - return _log_text - -# ------------------------------------------------------------------------------ -# Set the log level. Use one of the various LOG_LEVEL_* constants. -# ------------------------------------------------------------------------------ -func set_log_level(level): - _log_level = max(level, 0) - - # Level 0 settings - _lgr.set_less_test_names(level == 0) - # Explicitly always enabled - _lgr.set_type_enabled(_lgr.types.normal, true) - _lgr.set_type_enabled(_lgr.types.error, true) - _lgr.set_type_enabled(_lgr.types.pending, true) - - # Level 1 types - _lgr.set_type_enabled(_lgr.types.warn, level > 0) - _lgr.set_type_enabled(_lgr.types.deprecated, level > 0) - - # Level 2 types - _lgr.set_type_enabled(_lgr.types.passed, level > 1) - _lgr.set_type_enabled(_lgr.types.info, level > 1) - _lgr.set_type_enabled(_lgr.types.debug, level > 1) - - if(!Engine.is_editor_hint()): - _gui.set_log_level(level) - -# ------------------------------------------------------------------------------ -# Get the current log level. -# ------------------------------------------------------------------------------ -func get_log_level(): - return _log_level - -# ------------------------------------------------------------------------------ -# Call this method to make the test pause before teardown so that you can inspect -# anything that you have rendered to the screen. -# ------------------------------------------------------------------------------ -func pause_before_teardown(): - _pause_before_teardown = true; - -# ------------------------------------------------------------------------------ -# For batch processing purposes, you may want to ignore any calls to -# pause_before_teardown that you forgot to remove. -# ------------------------------------------------------------------------------ -func set_ignore_pause_before_teardown(should_ignore): - _ignore_pause_before_teardown = should_ignore - _gui.set_ignore_pause(should_ignore) - -func get_ignore_pause_before_teardown(): - return _ignore_pause_before_teardown - -# ------------------------------------------------------------------------------ -# Set to true so that painting of the screen will occur between tests. Allows you -# to see the output as tests occur. Especially useful with long running tests that -# make it appear as though it has humg. -# -# NOTE: not compatible with 1.0 so this is disabled by default. This will -# change in future releases. -# ------------------------------------------------------------------------------ -func set_yield_between_tests(should): - _yield_between.should = should - -func get_yield_between_tests(): - return _yield_between.should - -# ------------------------------------------------------------------------------ -# Call _process or _fixed_process, if they exist, on obj and all it's children -# and their children and so and so forth. Delta will be passed through to all -# the _process or _fixed_process methods. -# ------------------------------------------------------------------------------ -func simulate(obj, times, delta): - for _i in range(times): - if(obj.has_method("_process")): - obj._process(delta) - if(obj.has_method("_physics_process")): - obj._physics_process(delta) - - for kid in obj.get_children(): - simulate(kid, 1, delta) - -# ------------------------------------------------------------------------------ -# Starts an internal timer with a timeout of the passed in time. A 'timeout' -# signal will be sent when the timer ends. Returns itself so that it can be -# used in a call to yield...cutting down on lines of code. -# -# Example, yield to the Gut object for 10 seconds: -# yield(gut.set_yield_time(10), 'timeout') -# ------------------------------------------------------------------------------ -func set_yield_time(time, text=''): - _yield_timer.set_wait_time(time) - _yield_timer.start() - var msg = '-- Yielding (' + str(time) + 's)' - if(text == ''): - msg += ' --' - else: - msg += ': ' + text + ' --' - _lgr.log(msg, _lgr.fmts.yellow) - _was_yield_method_called = true - return self - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func set_yield_signal_or_time(obj, signal_name, max_wait, text=''): - obj.connect(signal_name, self, '_yielding_callback', [true]) - _yielding_to.obj = obj - _yielding_to.signal_name = signal_name - - _yield_timer.set_wait_time(max_wait) - _yield_timer.start() - _was_yield_method_called = true - _lgr.log(str('-- Yielding to signal "', signal_name, '" or for ', max_wait, ' seconds -- ', text), _lgr.fmts.yellow) - return self - -# ------------------------------------------------------------------------------ -# get the specific unit test that should be run -# ------------------------------------------------------------------------------ -func get_unit_test_name(): - return _unit_test_name - -# ------------------------------------------------------------------------------ -# set the specific unit test that should be run. -# ------------------------------------------------------------------------------ -func set_unit_test_name(test_name): - _unit_test_name = test_name - -# ------------------------------------------------------------------------------ -# Creates an empty file at the specified path -# ------------------------------------------------------------------------------ -func file_touch(path): - var f = File.new() - f.open(path, f.WRITE) - f.close() - -# ------------------------------------------------------------------------------ -# deletes the file at the specified path -# ------------------------------------------------------------------------------ -func file_delete(path): - var d = Directory.new() - var result = d.open(path.get_base_dir()) - if(result == OK): - d.remove(path) - -# ------------------------------------------------------------------------------ -# Checks to see if the passed in file has any data in it. -# ------------------------------------------------------------------------------ -func is_file_empty(path): - var f = File.new() - f.open(path, f.READ) - var empty = f.get_len() == 0 - f.close() - return empty - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func get_file_as_text(path): - return _utils.get_file_as_text(path) - -# ------------------------------------------------------------------------------ -# deletes all files in a given directory -# ------------------------------------------------------------------------------ -func directory_delete_files(path): - var d = Directory.new() - var result = d.open(path) - - # SHORTCIRCUIT - if(result != OK): - return - - # Traversing a directory is kinda odd. You have to start the process of listing - # the contents of a directory with list_dir_begin then use get_next until it - # returns an empty string. Then I guess you should end it. - d.list_dir_begin() - var thing = d.get_next() # could be a dir or a file or something else maybe? - var full_path = '' - while(thing != ''): - full_path = path + "/" + thing - #file_exists returns fasle for directories - if(d.file_exists(full_path)): - d.remove(full_path) - thing = d.get_next() - d.list_dir_end() - -# ------------------------------------------------------------------------------ -# Returns the instantiated script object that is currently being run. -# ------------------------------------------------------------------------------ -func get_current_script_object(): - var to_return = null - if(_test_script_objects.size() > 0): - to_return = _test_script_objects[-1] - return to_return - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func get_current_test_object(): - return _current_test - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func get_stubber(): - return _stubber - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func get_doubler(): - return _doubler - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func get_spy(): - return _spy - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func get_temp_directory(): - return _temp_directory - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func set_temp_directory(temp_directory): - _temp_directory = temp_directory - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func get_inner_class_name(): - return _inner_class_name - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func set_inner_class_name(inner_class_name): - _inner_class_name = inner_class_name - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func get_summary(): - return _new_summary - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func get_double_strategy(): - return _double_strategy - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func set_double_strategy(double_strategy): - _double_strategy = double_strategy - _doubler.set_strategy(double_strategy) - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func get_include_subdirectories(): - return _include_subdirectories - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func get_logger(): - return _lgr - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func set_logger(logger): - _lgr = logger - _lgr.set_gut(self) - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func set_include_subdirectories(include_subdirectories): - _include_subdirectories = include_subdirectories - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func get_test_collector(): - return _test_collector - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func get_export_path(): - return _export_path - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func set_export_path(export_path): - _export_path = export_path - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func get_version(): - return _utils.version - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func get_pre_run_script(): - return _pre_run_script - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func set_pre_run_script(pre_run_script): - _pre_run_script = pre_run_script - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func get_post_run_script(): - return _post_run_script - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func set_post_run_script(post_run_script): - _post_run_script = post_run_script - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func get_pre_run_script_instance(): - return _pre_run_script_instance - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func get_post_run_script_instance(): - return _post_run_script_instance - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func get_color_output(): - return _color_output - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func set_color_output(color_output): - _color_output = color_output - _lgr.disable_formatting(!color_output) - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func get_parameter_handler(): - return _parameter_handler - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func set_parameter_handler(parameter_handler): - _parameter_handler = parameter_handler - _parameter_handler.set_logger(_lgr) - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func get_gui(): - return _gui - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func get_orphan_counter(): - return _orphan_counter - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func show_orphans(should): - _lgr.set_type_enabled(_lgr.types.orphan, should) - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func get_autofree(): - return _autofree diff --git a/addons/gut/gut_cmdln.gd b/addons/gut/gut_cmdln.gd deleted file mode 100644 index b1df4d3..0000000 --- a/addons/gut/gut_cmdln.gd +++ /dev/null @@ -1,402 +0,0 @@ -# ############################################################################## -#(G)odot (U)nit (T)est class -# -# ############################################################################## -# The MIT License (MIT) -# ===================== -# -# Copyright (c) 2020 Tom "Butch" Wesley -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ############################################################################## -# Description -# ----------- -# Command line interface for the GUT unit testing tool. Allows you to run tests -# from the command line instead of running a scene. Place this script along with -# gut.gd into your scripts directory at the root of your project. Once there you -# can run this script (from the root of your project) using the following command: -# godot -s -d test/gut/gut_cmdln.gd -# -# See the readme for a list of options and examples. You can also use the -gh -# option to get more information about how to use the command line interface. -# ############################################################################## -extends SceneTree - -var Optparse = load('res://addons/gut/optparse.gd') -var Gut = load('res://addons/gut/gut.gd') - -# ------------------------------------------------------------------------------ -# Helper class to resolve the various different places where an option can -# be set. Using the get_value method will enforce the order of precedence of: -# 1. command line value -# 2. config file value -# 3. default value -# -# The idea is that you set the base_opts. That will get you a copies of the -# hash with null values for the other types of values. Lower precedented hashes -# will punch through null values of higher precedented hashes. -# ------------------------------------------------------------------------------ -class OptionResolver: - var base_opts = null - var cmd_opts = null - var config_opts = null - - - func get_value(key): - return _nvl(cmd_opts[key], _nvl(config_opts[key], base_opts[key])) - - func set_base_opts(opts): - base_opts = opts - cmd_opts = _null_copy(opts) - config_opts = _null_copy(opts) - - # creates a copy of a hash with all values null. - func _null_copy(h): - var new_hash = {} - for key in h: - new_hash[key] = null - return new_hash - - func _nvl(a, b): - if(a == null): - return b - else: - return a - func _string_it(h): - var to_return = '' - for key in h: - to_return += str('(',key, ':', _nvl(h[key], 'NULL'), ')') - return to_return - - func to_s(): - return str("base:\n", _string_it(base_opts), "\n", \ - "config:\n", _string_it(config_opts), "\n", \ - "cmd:\n", _string_it(cmd_opts), "\n", \ - "resolved:\n", _string_it(get_resolved_values())) - - func get_resolved_values(): - var to_return = {} - for key in base_opts: - to_return[key] = get_value(key) - return to_return - - func to_s_verbose(): - var to_return = '' - var resolved = get_resolved_values() - for key in base_opts: - to_return += str(key, "\n") - to_return += str(' default: ', _nvl(base_opts[key], 'NULL'), "\n") - to_return += str(' config: ', _nvl(config_opts[key], ' --'), "\n") - to_return += str(' cmd: ', _nvl(cmd_opts[key], ' --'), "\n") - to_return += str(' final: ', _nvl(resolved[key], 'NULL'), "\n") - - return to_return - -# ------------------------------------------------------------------------------ -# Here starts the actual script that uses the Options class to kick off Gut -# and run your tests. -# ------------------------------------------------------------------------------ -var _utils = load('res://addons/gut/utils.gd').get_instance() -# instance of gut -var _tester = null -# array of command line options specified -var _final_opts = [] -# Hash for easier access to the options in the code. Options will be -# extracted into this hash and then the hash will be used afterwards so -# that I don't make any dumb typos and get the neat code-sense when I -# type a dot. -var options = { - background_color = Color(.15, .15, .15, 1).to_html(), - config_file = 'res://.gutconfig.json', - dirs = [], - disable_colors = false, - double_strategy = 'partial', - font_color = Color(.8, .8, .8, 1).to_html(), - font_name = 'CourierPrime', - font_size = 16, - hide_orphans = false, - ignore_pause = false, - include_subdirs = false, - inner_class = '', - log_level = 1, - opacity = 100, - post_run_script = '', - pre_run_script = '', - prefix = 'test_', - selected = '', - should_exit = false, - should_exit_on_success = false, - should_maximize = false, - show_help = false, - suffix = '.gd', - tests = [], - unit_test_name = '', -} -var valid_fonts = ['AnonymousPro', 'CourierPro', 'LobsterTwo', 'Default'] - -# flag to indicate if only a single script should be run. -var _run_single = false - - -func setup_options(): - var opts = Optparse.new() - opts.set_banner(('This is the command line interface for the unit testing tool Gut. With this ' + - 'interface you can run one or more test scripts from the command line. In order ' + - 'for the Gut options to not clash with any other godot options, each option starts ' + - 'with a "g". Also, any option that requires a value will take the form of ' + - '"-g=". There cannot be any spaces between the option, the "=", or ' + - 'inside a specified value or godot will think you are trying to run a scene.')) - opts.add('-gtest', [], 'Comma delimited list of full paths to test scripts to run.') - opts.add('-gdir', options.dirs, 'Comma delimited list of directories to add tests from.') - opts.add('-gprefix', options.prefix, 'Prefix used to find tests when specifying -gdir. Default "[default]".') - opts.add('-gsuffix', options.suffix, 'Suffix used to find tests when specifying -gdir. Default "[default]".') - opts.add('-ghide_orphans', false, 'Display orphan counts for tests and scripts. Default "[default]".') - opts.add('-gmaximize', false, 'Maximizes test runner window to fit the viewport.') - opts.add('-gexit', false, 'Exit after running tests. If not specified you have to manually close the window.') - opts.add('-gexit_on_success', false, 'Only exit if all tests pass.') - opts.add('-glog', options.log_level, 'Log level. Default [default]') - opts.add('-gignore_pause', false, 'Ignores any calls to gut.pause_before_teardown.') - opts.add('-gselect', '', ('Select a script to run initially. The first script that ' + - 'was loaded using -gtest or -gdir that contains the specified ' + - 'string will be executed. You may run others by interacting ' + - 'with the GUI.')) - opts.add('-gunit_test_name', '', ('Name of a test to run. Any test that contains the specified ' + - 'text will be run, all others will be skipped.')) - opts.add('-gh', false, 'Print this help, then quit') - opts.add('-gconfig', 'res://.gutconfig.json', 'A config file that contains configuration information. Default is res://.gutconfig.json') - opts.add('-ginner_class', '', 'Only run inner classes that contain this string') - opts.add('-gopacity', options.opacity, 'Set opacity of test runner window. Use range 0 - 100. 0 = transparent, 100 = opaque.') - opts.add('-gpo', false, 'Print option values from all sources and the value used, then quit.') - opts.add('-ginclude_subdirs', false, 'Include subdirectories of -gdir.') - opts.add('-gdouble_strategy', 'partial', 'Default strategy to use when doubling. Valid values are [partial, full]. Default "[default]"') - opts.add('-gdisable_colors', false, 'Disable command line colors.') - opts.add('-gpre_run_script', '', 'pre-run hook script path') - opts.add('-gpost_run_script', '', 'post-run hook script path') - opts.add('-gprint_gutconfig_sample', false, 'Print out json that can be used to make a gutconfig file then quit.') - - opts.add('-gfont_name', options.font_name, str('Valid values are: ', valid_fonts, '. Default "[default]"')) - opts.add('-gfont_size', options.font_size, 'Font size, default "[default]"') - opts.add('-gbackground_color', options.background_color, 'Background color as an html color, default "[default]"') - opts.add('-gfont_color',options.font_color, 'Font color as an html color, default "[default]"') - return opts - - -# Parses options, applying them to the _tester or setting values -# in the options struct. -func extract_command_line_options(from, to): - to.config_file = from.get_value('-gconfig') - to.dirs = from.get_value('-gdir') - to.disable_colors = from.get_value('-gdisable_colors') - to.double_strategy = from.get_value('-gdouble_strategy') - to.ignore_pause = from.get_value('-gignore_pause') - to.include_subdirs = from.get_value('-ginclude_subdirs') - to.inner_class = from.get_value('-ginner_class') - to.log_level = from.get_value('-glog') - to.opacity = from.get_value('-gopacity') - to.post_run_script = from.get_value('-gpost_run_script') - to.pre_run_script = from.get_value('-gpre_run_script') - to.prefix = from.get_value('-gprefix') - to.selected = from.get_value('-gselect') - to.should_exit = from.get_value('-gexit') - to.should_exit_on_success = from.get_value('-gexit_on_success') - to.should_maximize = from.get_value('-gmaximize') - to.hide_orphans = from.get_value('-ghide_orphans') - to.suffix = from.get_value('-gsuffix') - to.tests = from.get_value('-gtest') - to.unit_test_name = from.get_value('-gunit_test_name') - - to.font_size = from.get_value('-gfont_size') - to.font_name = from.get_value('-gfont_name') - to.background_color = from.get_value('-gbackground_color') - to.font_color = from.get_value('-gfont_color') - - -func load_options_from_config_file(file_path, into): - # SHORTCIRCUIT - var f = File.new() - if(!f.file_exists(file_path)): - if(file_path != 'res://.gutconfig.json'): - print('ERROR: Config File "', file_path, '" does not exist.') - return -1 - else: - return 1 - - f.open(file_path, f.READ) - var json = f.get_as_text() - f.close() - - var results = JSON.parse(json) - # SHORTCIRCUIT - if(results.error != OK): - print("\n\n",'!! ERROR parsing file: ', file_path) - print(' at line ', results.error_line, ':') - print(' ', results.error_string) - return -1 - - # Get all the options out of the config file using the option name. The - # options hash is now the default source of truth for the name of an option. - for key in into: - if(results.result.has(key)): - into[key] = results.result[key] - - return 1 - - -# Apply all the options specified to _tester. This is where the rubber meets -# the road. -func apply_options(opts): - _tester = Gut.new() - get_root().add_child(_tester) - _tester.connect('tests_finished', self, '_on_tests_finished', [opts.should_exit, opts.should_exit_on_success]) - _tester.set_yield_between_tests(true) - _tester.set_modulate(Color(1.0, 1.0, 1.0, min(1.0, float(opts.opacity) / 100))) - _tester.show() - - _tester.set_include_subdirectories(opts.include_subdirs) - - if(opts.should_maximize): - _tester.maximize() - - if(opts.inner_class != ''): - _tester.set_inner_class_name(opts.inner_class) - _tester.set_log_level(opts.log_level) - _tester.set_ignore_pause_before_teardown(opts.ignore_pause) - - for i in range(opts.dirs.size()): - _tester.add_directory(opts.dirs[i], opts.prefix, opts.suffix) - - for i in range(opts.tests.size()): - _tester.add_script(opts.tests[i]) - - if(opts.selected != ''): - _tester.select_script(opts.selected) - _run_single = true - - if(opts.double_strategy == 'full'): - _tester.set_double_strategy(_utils.DOUBLE_STRATEGY.FULL) - elif(opts.double_strategy == 'partial'): - _tester.set_double_strategy(_utils.DOUBLE_STRATEGY.PARTIAL) - - _tester.set_unit_test_name(opts.unit_test_name) - _tester.set_pre_run_script(opts.pre_run_script) - _tester.set_post_run_script(opts.post_run_script) - _tester.set_color_output(!opts.disable_colors) - _tester.show_orphans(!opts.hide_orphans) - - _tester.get_gui().set_font_size(opts.font_size) - _tester.get_gui().set_font(opts.font_name) - if(opts.font_color != null and opts.font_color.is_valid_html_color()): - _tester.get_gui().set_default_font_color(Color(opts.font_color)) - if(opts.background_color != null and opts.background_color.is_valid_html_color()): - _tester.get_gui().set_background_color(Color(opts.background_color)) - - -func _print_gutconfigs(values): - var header = """Here is a sample of a full .gutconfig.json file. -You do not need to specify all values in your own file. The values supplied in -this sample are what would be used if you ran gut w/o the -gprint_gutconfig_sample -option (the resolved values where default < .gutconfig < command line).""" - print("\n", header.replace("\n", ' '), "\n\n") - var resolved = values - - # remove some options that don't make sense to be in config - resolved.erase("config_file") - resolved.erase("show_help") - - print("Here's a config with all the properties set based off of your current command and config.") - var text = JSON.print(resolved) - print(text.replace(',', ",\n")) - - for key in resolved: - resolved[key] = null - - print("\n\nAnd here's an empty config for you fill in what you want.") - text = JSON.print(resolved) - print(text.replace(',', ",\n")) - - -# parse options and run Gut -func _run_gut(): - var opt_resolver = OptionResolver.new() - opt_resolver.set_base_opts(options) - - print("\n\n", ' --- Gut ---') - var o = setup_options() - - var all_options_valid = o.parse() - extract_command_line_options(o, opt_resolver.cmd_opts) - var load_result = \ - load_options_from_config_file(opt_resolver.get_value('config_file'), opt_resolver.config_opts) - - if(load_result == -1): # -1 indicates json parse error - quit(1) - else: - if(!all_options_valid): - quit(1) - elif(o.get_value('-gh')): - print(_utils.get_version_text()) - o.print_help() - quit() - elif(o.get_value('-gpo')): - print('All command line options and where they are specified. ' + - 'The "final" value shows which value will actually be used ' + - 'based on order of precedence (default < .gutconfig < cmd line).' + "\n") - print(opt_resolver.to_s_verbose()) - quit() - elif(o.get_value('-gprint_gutconfig_sample')): - _print_gutconfigs(opt_resolver.get_resolved_values()) - quit() - else: - _final_opts = opt_resolver.get_resolved_values(); - apply_options(_final_opts) - _tester.test_scripts(!_run_single) - - -# exit if option is set. -func _on_tests_finished(should_exit, should_exit_on_success): - if(_final_opts.dirs.size() == 0): - if(_tester.get_summary().get_totals().scripts == 0): - var lgr = _tester.get_logger() - lgr.error('No directories configured. Add directories with options or a .gutconfig.json file. Use the -gh option for more information.') - - if(_tester.get_fail_count()): - OS.exit_code = 1 - - # Overwrite the exit code with the post_script - var post_inst = _tester.get_post_run_script_instance() - if(post_inst != null and post_inst.get_exit_code() != null): - OS.exit_code = post_inst.get_exit_code() - - if(should_exit or (should_exit_on_success and _tester.get_fail_count() == 0)): - quit() - else: - print("Tests finished, exit manually") - -# ------------------------------------------------------------------------------ -# MAIN -# ------------------------------------------------------------------------------ -func _init(): - if(!_utils.is_version_ok()): - print("\n\n", _utils.get_version_text()) - push_error(_utils.get_bad_version_text()) - OS.exit_code = 1 - quit() - else: - _run_gut() diff --git a/addons/gut/gut_plugin.gd b/addons/gut/gut_plugin.gd deleted file mode 100644 index 8981f28..0000000 --- a/addons/gut/gut_plugin.gd +++ /dev/null @@ -1,12 +0,0 @@ -tool -extends EditorPlugin - -func _enter_tree(): - # Initialization of the plugin goes here - # Add the new type with a name, a parent type, a script and an icon - add_custom_type("Gut", "Control", preload("plugin_control.gd"), preload("icon.png")) - -func _exit_tree(): - # Clean-up of the plugin goes here - # Always remember to remove it from the engine when deactivated - remove_custom_type("Gut") diff --git a/addons/gut/hook_script.gd b/addons/gut/hook_script.gd deleted file mode 100644 index 10a5e12..0000000 --- a/addons/gut/hook_script.gd +++ /dev/null @@ -1,35 +0,0 @@ -# ------------------------------------------------------------------------------ -# This script is the base for custom scripts to be used in pre and post -# run hooks. -# ------------------------------------------------------------------------------ - -# This is the instance of GUT that is running the tests. You can get -# information about the run from this object. This is set by GUT when the -# script is instantiated. -var gut = null - -# the exit code to be used by gut_cmdln. See set method. -var _exit_code = null - -var _should_abort = false -# Virtual method that will be called by GUT after instantiating -# this script. -func run(): - pass - -# Set the exit code when running from the command line. If not set then the -# default exit code will be returned (0 when no tests fail, 1 when any tests -# fail). -func set_exit_code(code): - _exit_code = code - -func get_exit_code(): - return _exit_code - -# Usable by pre-run script to cause the run to end AFTER the run() method -# finishes. post-run script will not be ran. -func abort(): - _should_abort = true - -func should_abort(): - return _should_abort diff --git a/addons/gut/icon.png b/addons/gut/icon.png deleted file mode 100644 index 9ad6a1dd9959cc5f88d593408c8adbdbd18e7639..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 129 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!93?!50ihlx9JOMr-u0VR}`!zG}%>V!Yzhuw) z5+I+kB*-tA!Qt7BG$2RW)5S5Q;#RT)^P+_d6$6?svU+e>HF1b*h%2t%nst?nqmheY Xk`Z^_GVkS*K;;acu6{1-oD!M= 0 and type_flag < _supported_defaults.size() and [type_flag] != null - -# Creates a list of parameters with defaults of null unless a default value is -# found in the metadata. If a default is found in the meta then it is used if -# it is one we know how support. -# -# If a default is found that we don't know how to handle then this method will -# return null. -func _get_arg_text(method_meta): - var text = '' - var args = method_meta.args - var defaults = [] - var has_unsupported_defaults = false - - # fill up the defaults with null defaults for everything that doesn't have - # a default in the meta data. default_args is an array of default values - # for the last n parameters where n is the size of default_args so we only - # add nulls for everything up to the first parameter with a default. - for _i in range(args.size() - method_meta.default_args.size()): - defaults.append('null') - - # Add meta-data defaults. - for i in range(method_meta.default_args.size()): - var t = args[defaults.size()]['type'] - var value = '' - if(_is_supported_default(t)): - # strings are special, they need quotes around the value - if(t == TYPE_STRING): - value = str("'", str(method_meta.default_args[i]), "'") - # Colors need the parens but things like Vector2 and Rect2 don't - elif(t == TYPE_COLOR): - value = str(_supported_defaults[t], '(', str(method_meta.default_args[i]), ')') - elif(t == TYPE_OBJECT): - if(str(method_meta.default_args[i]) == "[Object:null]"): - value = str(_supported_defaults[t], 'null') - else: - value = str(_supported_defaults[t], str(method_meta.default_args[i]).to_lower()) - - # Everything else puts the prefix (if one is there) form _supported_defaults - # in front. The to_lower is used b/c for some reason the defaults for - # null, true, false are all "Null", "True", "False". - else: - value = str(_supported_defaults[t], str(method_meta.default_args[i]).to_lower()) - else: - _lgr.warn(str( - 'Unsupported default param type: ',method_meta.name, '-', args[defaults.size()].name, ' ', t, ' = ', method_meta.default_args[i])) - value = str('unsupported=',t) - has_unsupported_defaults = true - - defaults.append(value) - - # construct the string of parameters - for i in range(args.size()): - text += str(PARAM_PREFIX, args[i].name, '=', defaults[i]) - if(i != args.size() -1): - text += ', ' - - # if we don't know how to make a default then we have to return null b/c - # it will cause a runtime error and it's one thing we could return to let - # callers know it didn't work. - if(has_unsupported_defaults): - text = null - - return text - -# ############### -# Public -# ############### - -# Creates a delceration for a function based off of function metadata. All -# types whose defaults are supported will have their values. If a datatype -# is not supported and the parameter has a default, a warning message will be -# printed and the declaration will return null. -func get_function_text(meta): - var method_params = _get_arg_text(meta) - var text = null - - var param_array = get_spy_call_parameters_text(meta) - if(param_array == 'null'): - param_array = '[]' - - if(method_params != null): - var decleration = str('func ', meta.name, '(', method_params, '):') - text = _func_text.format({ - "func_decleration":decleration, - "method_name":meta.name, - "param_array":param_array, - "super_call":get_super_call_text(meta) - }) - return text - -# creates a call to the function in meta in the super's class. -func get_super_call_text(meta): - var params = '' - - for i in range(meta.args.size()): - params += PARAM_PREFIX + meta.args[i].name - if(meta.args.size() > 1 and i != meta.args.size() -1): - params += ', ' - if(meta.name == '_init'): - return 'null' - else: - return str('.', meta.name, '(', params, ')') - -func get_spy_call_parameters_text(meta): - var called_with = 'null' - if(meta.args.size() > 0): - called_with = '[' - for i in range(meta.args.size()): - called_with += str(PARAM_PREFIX, meta.args[i].name) - if(i < meta.args.size() - 1): - called_with += ', ' - called_with += ']' - return called_with - -func get_logger(): - return _lgr - -func set_logger(logger): - _lgr = logger diff --git a/addons/gut/one_to_many.gd b/addons/gut/one_to_many.gd deleted file mode 100644 index 6a0f818..0000000 --- a/addons/gut/one_to_many.gd +++ /dev/null @@ -1,38 +0,0 @@ -# ------------------------------------------------------------------------------ -# This datastructure represents a simple one-to-many relationship. It manages -# a dictionary of value/array pairs. It ignores duplicates of both the "one" -# and the "many". -# ------------------------------------------------------------------------------ -var _items = {} - -# return the size of _items or the size of an element in _items if "one" was -# specified. -func size(one=null): - var to_return = 0 - if(one == null): - to_return = _items.size() - elif(_items.has(one)): - to_return = _items[one].size() - return to_return - -# Add an element to "one" if it does not already exist -func add(one, many_item): - if(_items.has(one) and !_items[one].has(many_item)): - _items[one].append(many_item) - else: - _items[one] = [many_item] - -func clear(): - _items.clear() - -func has(one, many_item): - var to_return = false - if(_items.has(one)): - to_return = _items[one].has(many_item) - return to_return - -func to_s(): - var to_return = '' - for key in _items: - to_return += str(key, ": ", _items[key], "\n") - return to_return diff --git a/addons/gut/optparse.gd b/addons/gut/optparse.gd deleted file mode 100644 index 290ed9d..0000000 --- a/addons/gut/optparse.gd +++ /dev/null @@ -1,248 +0,0 @@ -# ############################################################################## -#(G)odot (U)nit (T)est class -# -# ############################################################################## -# The MIT License (MIT) -# ===================== -# -# Copyright (c) 2020 Tom "Butch" Wesley -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ############################################################################## -# Description -# ----------- -# Command line interface for the GUT unit testing tool. Allows you to run tests -# from the command line instead of running a scene. Place this script along with -# gut.gd into your scripts directory at the root of your project. Once there you -# can run this script (from the root of your project) using the following command: -# godot -s -d test/gut/gut_cmdln.gd -# -# See the readme for a list of options and examples. You can also use the -gh -# option to get more information about how to use the command line interface. -# ############################################################################## - -#------------------------------------------------------------------------------- -# Parses the command line arguments supplied into an array that can then be -# examined and parsed based on how the gut options work. -#------------------------------------------------------------------------------- -class CmdLineParser: - var _used_options = [] - # an array of arrays. Each element in this array will contain an option - # name and if that option contains a value then it will have a sedond - # element. For example: - # [[-gselect, test.gd], [-gexit]] - var _opts = [] - - func _init(): - for i in range(OS.get_cmdline_args().size()): - var opt_val = OS.get_cmdline_args()[i].split('=') - _opts.append(opt_val) - - # Parse out multiple comma delimited values from a command line - # option. Values are separated from option name with "=" and - # additional values are comma separated. - func _parse_array_value(full_option): - var value = _parse_option_value(full_option) - var split = value.split(',') - return split - - # Parse out the value of an option. Values are separated from - # the option name with "=" - func _parse_option_value(full_option): - if(full_option.size() > 1): - return full_option[1] - else: - return null - - # Search _opts for an element that starts with the option name - # specified. - func find_option(name): - var found = false - var idx = 0 - - while(idx < _opts.size() and !found): - if(_opts[idx][0] == name): - found = true - else: - idx += 1 - - if(found): - return idx - else: - return -1 - - func get_array_value(option): - _used_options.append(option) - var to_return = [] - var opt_loc = find_option(option) - if(opt_loc != -1): - to_return = _parse_array_value(_opts[opt_loc]) - _opts.remove(opt_loc) - - return to_return - - # returns the value of an option if it was specified, null otherwise. This - # used to return the default but that became problemnatic when trying to - # punch through the different places where values could be specified. - func get_value(option): - _used_options.append(option) - var to_return = null - var opt_loc = find_option(option) - if(opt_loc != -1): - to_return = _parse_option_value(_opts[opt_loc]) - _opts.remove(opt_loc) - - return to_return - - # returns true if it finds the option, false if not. - func was_specified(option): - _used_options.append(option) - return find_option(option) != -1 - - # Returns any unused command line options. I found that only the -s and - # script name come through from godot, all other options that godot uses - # are not sent through OS.get_cmdline_args(). - # - # This is a onetime thing b/c i kill all items in _used_options - func get_unused_options(): - var to_return = [] - for i in range(_opts.size()): - to_return.append(_opts[i][0]) - - var script_option = to_return.find('-s') - if script_option != -1: - to_return.remove(script_option + 1) - to_return.remove(script_option) - - while(_used_options.size() > 0): - var index = to_return.find(_used_options[0].split("=")[0]) - if(index != -1): - to_return.remove(index) - _used_options.remove(0) - - return to_return - -#------------------------------------------------------------------------------- -# Simple class to hold a command line option -#------------------------------------------------------------------------------- -class Option: - var value = null - var option_name = '' - var default = null - var description = '' - - func _init(name, default_value, desc=''): - option_name = name - default = default_value - description = desc - value = null#default_value - - func pad(to_pad, size, pad_with=' '): - var to_return = to_pad - for _i in range(to_pad.length(), size): - to_return += pad_with - - return to_return - - func to_s(min_space=0): - var subbed_desc = description - if(subbed_desc.find('[default]') != -1): - subbed_desc = subbed_desc.replace('[default]', str(default)) - return pad(option_name, min_space) + subbed_desc - -#------------------------------------------------------------------------------- -# The high level interface between this script and the command line options -# supplied. Uses Option class and CmdLineParser to extract information from -# the command line and make it easily accessible. -#------------------------------------------------------------------------------- -var options = [] -var _opts = [] -var _banner = '' - -func add(name, default, desc): - options.append(Option.new(name, default, desc)) - -func get_value(name): - var found = false - var idx = 0 - - while(idx < options.size() and !found): - if(options[idx].option_name == name): - found = true - else: - idx += 1 - - if(found): - return options[idx].value - else: - print("COULD NOT FIND OPTION " + name) - return null - -func set_banner(banner): - _banner = banner - -func print_help(): - var longest = 0 - for i in range(options.size()): - if(options[i].option_name.length() > longest): - longest = options[i].option_name.length() - - print('---------------------------------------------------------') - print(_banner) - - print("\nOptions\n-------") - for i in range(options.size()): - print(' ' + options[i].to_s(longest + 2)) - print('---------------------------------------------------------') - -func print_options(): - for i in range(options.size()): - print(options[i].option_name + '=' + str(options[i].value)) - -func parse(): - var parser = CmdLineParser.new() - - for i in range(options.size()): - var t = typeof(options[i].default) - # only set values that were specified at the command line so that - # we can punch through default and config values correctly later. - # Without this check, you can't tell the difference between the - # defaults and what was specified, so you can't punch through - # higher level options. - if(parser.was_specified(options[i].option_name)): - if(t == TYPE_INT): - options[i].value = int(parser.get_value(options[i].option_name)) - elif(t == TYPE_STRING): - options[i].value = parser.get_value(options[i].option_name) - elif(t == TYPE_ARRAY): - options[i].value = parser.get_array_value(options[i].option_name) - elif(t == TYPE_BOOL): - options[i].value = parser.was_specified(options[i].option_name) - elif(t == TYPE_NIL): - print(options[i].option_name + ' cannot be processed, it has a nil datatype') - else: - print(options[i].option_name + ' cannot be processed, it has unknown datatype:' + str(t)) - - var unused = parser.get_unused_options() - if(unused.size() > 0): - print("Unrecognized options: ", unused) - return false - - return true diff --git a/addons/gut/orphan_counter.gd b/addons/gut/orphan_counter.gd deleted file mode 100644 index cbfbea4..0000000 --- a/addons/gut/orphan_counter.gd +++ /dev/null @@ -1,55 +0,0 @@ -# ############################################################################## -#(G)odot (U)nit (T)est class -# -# ############################################################################## -# The MIT License (MIT) -# ===================== -# -# Copyright (c) 2020 Tom "Butch" Wesley -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ############################################################################## -# This is a utility for tracking changes in the orphan count. Each time -# add_counter is called it adds/resets the value in the dictionary to the -# current number of orphans. Each call to get_counter will return the change -# in orphans since add_counter was last called. -# ############################################################################## -var _counters = {} - -func orphan_count(): - return Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT) - -func add_counter(name): - _counters[name] = orphan_count() - -# Returns the number of orphans created since add_counter was last called for -# the name. Returns -1 to avoid blowing up with an invalid name but still -# be somewhat visible that we've done something wrong. -func get_counter(name): - return orphan_count() - _counters[name] if _counters.has(name) else -1 - -func print_orphans(name, lgr): - var count = get_counter(name) - - if(count > 0): - var o = 'orphan' - if(count > 1): - o = 'orphans' - lgr.orphan(str(count, ' new ', o, '(', name, ').')) diff --git a/addons/gut/parameter_factory.gd b/addons/gut/parameter_factory.gd deleted file mode 100644 index 6bffe4c..0000000 --- a/addons/gut/parameter_factory.gd +++ /dev/null @@ -1,75 +0,0 @@ -# ############################################################################## -#(G)odot (U)nit (T)est class -# -# ############################################################################## -# The MIT License (MIT) -# ===================== -# -# Copyright (c) 2020 Tom "Butch" Wesley -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ############################################################################## -# This is the home for all parameter creation helpers. These functions should -# all return an array of values to be used as parameters for parameterized -# tests. -# ############################################################################## - -# ------------------------------------------------------------------------------ -# Creates an array of dictionaries. It pairs up the names array with each set -# of values in values. If more names than values are specified then the missing -# values will be filled with nulls. If more values than names are specified -# those values will be ignored. -# -# Example: -# create_named_parameters(['a', 'b'], [[1, 2], ['one', 'two']]) returns -# [{a:1, b:2}, {a:'one', b:'two'}] -# -# This allows you to increase readability of your parameterized tests: -# var params = create_named_parameters(['a', 'b'], [[1, 2], ['one', 'two']]) -# func test_foo(p = use_parameters(params)): -# assert_eq(p.a, p.b) -# -# Parameters: -# names: an array of names to be used as keys in the dictionaries -# values: an array of arrays of values. -# ------------------------------------------------------------------------------ -static func named_parameters(names, values): - var named = [] - for i in range(values.size()): - var entry = {} - - var parray = values[i] - if(typeof(parray) != TYPE_ARRAY): - parray = [values[i]] - - for j in range(names.size()): - if(j >= parray.size()): - entry[names[j]] = null - else: - entry[names[j]] = parray[j] - named.append(entry) - - return named - -# Additional Helper Ideas -# * File. IDK what it would look like. csv maybe. -# * Random values within a range? -# * All int values in a range or add an optioanal step. -# * diff --git a/addons/gut/parameter_handler.gd b/addons/gut/parameter_handler.gd deleted file mode 100644 index 6b37e82..0000000 --- a/addons/gut/parameter_handler.gd +++ /dev/null @@ -1,37 +0,0 @@ -var _utils = load('res://addons/gut/utils.gd').get_instance() -var _params = null -var _call_count = 0 -var _logger = null - -func _init(params=null): - _params = params - _logger = _utils.get_logger() - if(typeof(_params) != TYPE_ARRAY): - _logger.error('You must pass an array to parameter_handler constructor.') - _params = null - - -func next_parameters(): - _call_count += 1 - return _params[_call_count -1] - -func get_current_parameters(): - return _params[_call_count] - -func is_done(): - var done = true - if(_params != null): - done = _call_count == _params.size() - return done - -func get_logger(): - return _logger - -func set_logger(logger): - _logger = logger - -func get_call_count(): - return _call_count - -func get_parameter_count(): - return _params.size() diff --git a/addons/gut/plugin.cfg b/addons/gut/plugin.cfg deleted file mode 100644 index 128ccd0..0000000 --- a/addons/gut/plugin.cfg +++ /dev/null @@ -1,7 +0,0 @@ -[plugin] - -name="Gut" -description="Unit Testing tool for Godot." -author="Butch Wesley" -version="7.1.0" -script="gut_plugin.gd" diff --git a/addons/gut/plugin_control.gd b/addons/gut/plugin_control.gd deleted file mode 100644 index 959ce17..0000000 --- a/addons/gut/plugin_control.gd +++ /dev/null @@ -1,247 +0,0 @@ -# ############################################################################## -#(G)odot (U)nit (T)est class -# -# ############################################################################## -# The MIT License (MIT) -# ===================== -# -# Copyright (c) 2020 Tom "Butch" Wesley -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ############################################################################## -# This is the control that is added via the editor. It exposes GUT settings -# through the editor and delays the creation of the GUT instance until -# Engine.get_main_loop() works as expected. -# ############################################################################## -tool -extends Control - -# ------------------------------------------------------------------------------ -# GUT Settings -# ------------------------------------------------------------------------------ -export(String, 'AnonymousPro', 'CourierPrime', 'LobsterTwo', 'Default') var _font_name = 'AnonymousPro' -export(int) var _font_size = 20 -export(Color) var _font_color = Color(.8, .8, .8, 1) -export(Color) var _background_color = Color(.15, .15, .15, 1) -# Enable/Disable coloring of output. -export(bool) var _color_output = true -# The full/partial name of a script to select upon startup -export(String) var _select_script = '' -# The full/partial name of a test. All tests that contain the string will be -# run -export(String) var _tests_like = '' -# The full/partial name of an Inner Class to be run. All Inner Classes that -# contain the string will be run. -export(String) var _inner_class_name = '' -# Start running tests when the scene finishes loading -export var _run_on_load = false -# Maximize the GUT control on startup -export var _should_maximize = false -# Print output to the consol as well -export var _should_print_to_console = true -# Display orphan counts at the end of tests/scripts. -export var _show_orphans = true -# The log level. -export(int, 'Fail/Errors', 'Errors/Warnings/Test Names', 'Everything') var _log_level = 1 -# When enabled GUT will yield between tests to give the GUI time to paint. -# Disabling this can make the program appear to hang and can have some -# unwanted consequences with the timing of freeing objects -export var _yield_between_tests = true -# When GUT compares values it first checks the types to prevent runtime errors. -# This behavior can be disabled if desired. This flag was added early in -# development to prevent any breaking changes and will likely be removed in -# the future. -export var _disable_strict_datatype_checks = false -# The prefix used to find test methods. -export var _test_prefix = 'test_' -# The prefix used to find test scripts. -export var _file_prefix = 'test_' -# The file extension for test scripts (I don't think you can change this and -# everythign work). -export var _file_extension = '.gd' -# The prefix used to find Inner Test Classes. -export var _inner_class_prefix = 'Test' -# The directory GUT will use to write any temporary files. This isn't used -# much anymore since there was a change to the double creation implementation. -# This will be removed in a later release. -export(String) var _temp_directory = 'user://gut_temp_directory' -# The path and filename for exported test information. -export(String) var _export_path = '' -# When enabled, any directory added will also include its subdirectories when -# GUT looks for test scripts. -export var _include_subdirectories = false -# Allow user to add test directories via editor. This is done with strings -# instead of an array because the interface for editing arrays is really -# cumbersome and complicates testing because arrays set through the editor -# apply to ALL instances. This also allows the user to use the built in -# dialog to pick a directory. -export(String, DIR) var _directory1 = '' -export(String, DIR) var _directory2 = '' -export(String, DIR) var _directory3 = '' -export(String, DIR) var _directory4 = '' -export(String, DIR) var _directory5 = '' -export(String, DIR) var _directory6 = '' -# Must match the types in _utils for double strategy -export(int, 'FULL', 'PARTIAL') var _double_strategy = 1 -# Path and filename to the script to run before all tests are run. -export(String, FILE) var _pre_run_script = '' -# Path and filename to the script to run after all tests are run. -export(String, FILE) var _post_run_script = '' -# ------------------------------------------------------------------------------ - - -# ------------------------------------------------------------------------------ -# Signals -# ------------------------------------------------------------------------------ -# Emitted when all the tests have finished running. -signal tests_finished -# Emitted when GUT is ready to be interacted with, and before any tests are run. -signal gut_ready - - -# ------------------------------------------------------------------------------ -# Private stuff. -# ------------------------------------------------------------------------------ -var _gut = null -var _lgr = null -var _cancel_import = false -var _placeholder = null - -func _init(): - # This min size has to be what the min size of the GutScene's min size is - # but it has to be set here and not inferred i think. - rect_min_size = Vector2(740, 250) - -func _ready(): - # Must call this deferred so that there is enough time for - # Engine.get_main_loop() is populated and the psuedo singleton utils.gd - # can be setup correctly. - if(Engine.editor_hint): - _placeholder = load('res://addons/gut/GutScene.tscn').instance() - call_deferred('add_child', _placeholder) - _placeholder.rect_size = rect_size - else: - call_deferred('_setup_gut') - - connect('resized', self, '_on_resized') - -func _on_resized(): - if(_placeholder != null): - _placeholder.rect_size = rect_size - - -# Templates can be missing if tests are exported and the export config for the -# project does not include '*.txt' files. This check and related flags make -# sure GUT does not blow up and that the error is not lost in all the import -# output that is generated as well as ensuring that no tests are run. -# -# Assumption: This is only a concern when running from the scene since you -# cannot run GUT from the command line in an exported game. -func _check_for_templates(): - var f = File.new() - if(!f.file_exists('res://addons/gut/double_templates/function_template.txt')): - _lgr.error('Templates are missing. Make sure you are exporting "*.txt" or "addons/gut/double_templates/*.txt".') - _run_on_load = false - _cancel_import = true - return false - return true - -func _setup_gut(): - var _utils = load('res://addons/gut/utils.gd').get_instance() - - _lgr = _utils.get_logger() - _gut = load('res://addons/gut/gut.gd').new() - _gut.connect('tests_finished', self, '_on_tests_finished') - - if(!_check_for_templates()): - return - - _gut._select_script = _select_script - _gut._tests_like = _tests_like - _gut._inner_class_name = _inner_class_name - - _gut._test_prefix = _test_prefix - _gut._file_prefix = _file_prefix - _gut._file_extension = _file_extension - _gut._inner_class_prefix = _inner_class_prefix - _gut._temp_directory = _temp_directory - - _gut.set_should_maximize(_should_maximize) - _gut.set_yield_between_tests(_yield_between_tests) - _gut.disable_strict_datatype_checks(_disable_strict_datatype_checks) - _gut.set_export_path(_export_path) - _gut.set_include_subdirectories(_include_subdirectories) - _gut.set_double_strategy(_double_strategy) - _gut.set_pre_run_script(_pre_run_script) - _gut.set_post_run_script(_post_run_script) - _gut.set_color_output(_color_output) - _gut.show_orphans(_show_orphans) - - get_parent().add_child(_gut) - - if(!_utils.is_version_ok()): - return - - _gut.set_log_level(_log_level) - - _gut.add_directory(_directory1) - _gut.add_directory(_directory2) - _gut.add_directory(_directory3) - _gut.add_directory(_directory4) - _gut.add_directory(_directory5) - _gut.add_directory(_directory6) - - _gut.get_logger().disable_printer('console', !_should_print_to_console) - # When file logging enabled then the log will contain terminal escape - # strings. So when running the scene this is disabled. Also if enabled - # this may cause duplicate entries into the logs. - _gut.get_logger().disable_printer('terminal', true) - - _gut.get_gui().set_font_size(_font_size) - _gut.get_gui().set_font(_font_name) - _gut.get_gui().set_default_font_color(_font_color) - _gut.get_gui().set_background_color(_background_color) - _gut.get_gui().rect_size = rect_size - emit_signal('gut_ready') - - if(_run_on_load): - # Run the test scripts. If one has been selected then only run that one - # otherwise all tests will be run. - var run_rest_of_scripts = _select_script == null - _gut.test_scripts(run_rest_of_scripts) - -func _is_ready_to_go(action): - if(_gut == null): - push_error(str('GUT is not ready for ', action, ' yet. Perform actions on GUT in/after the gut_ready signal.')) - return _gut != null - -func _on_tests_finished(): - emit_signal('tests_finished') - -func get_gut(): - return _gut - -func export_if_tests_found(): - if(_is_ready_to_go('export_if_tests_found')): - _gut.export_if_tests_found() - -func import_tests_if_none_found(): - if(_is_ready_to_go('import_tests_if_none_found') and !_cancel_import): - _gut.import_tests_if_none_found() diff --git a/addons/gut/printers.gd b/addons/gut/printers.gd deleted file mode 100644 index c543603..0000000 --- a/addons/gut/printers.gd +++ /dev/null @@ -1,157 +0,0 @@ -# ------------------------------------------------------------------------------ -# Interface and some basic functionality for all printers. -# ------------------------------------------------------------------------------ -class Printer: - var _format_enabled = true - var _disabled = false - var _printer_name = 'NOT SET' - var _show_name = false # used for debugging, set manually - - func get_format_enabled(): - return _format_enabled - - func set_format_enabled(format_enabled): - _format_enabled = format_enabled - - func send(text, fmt=null): - if(_disabled): - return - - var formatted = text - if(fmt != null and _format_enabled): - formatted = format_text(text, fmt) - - if(_show_name): - formatted = str('(', _printer_name, ')') + formatted - - _output(formatted) - - func get_disabled(): - return _disabled - - func set_disabled(disabled): - _disabled = disabled - - # -------------------- - # Virtual Methods (some have some default behavior) - # -------------------- - func _output(text): - pass - - func format_text(text, fmt): - return text - -# ------------------------------------------------------------------------------ -# Responsible for sending text to a GUT gui. -# ------------------------------------------------------------------------------ -class GutGuiPrinter: - extends Printer - var _gut = null - - var _colors = { - red = Color.red, - yellow = Color.yellow, - green = Color.green - } - - func _init(): - _printer_name = 'gui' - - func _wrap_with_tag(text, tag): - return str('[', tag, ']', text, '[/', tag, ']') - - func _color_text(text, c_word): - return '[color=' + c_word + ']' + text + '[/color]' - - func format_text(text, fmt): - var box = _gut.get_gui().get_text_box() - - if(fmt == 'bold'): - box.push_bold() - elif(fmt == 'underline'): - box.push_underline() - elif(_colors.has(fmt)): - box.push_color(_colors[fmt]) - else: - # just pushing something to pop. - box.push_normal() - - box.add_text(text) - box.pop() - - return '' - - func _output(text): - _gut.get_gui().get_text_box().add_text(text) - - func get_gut(): - return _gut - - func set_gut(gut): - _gut = gut - - # This can be very very slow when the box has a lot of text. - func clear_line(): - var box = _gut.get_gui().get_text_box() - box.remove_line(box.get_line_count() - 1) - box.update() - -# ------------------------------------------------------------------------------ -# This AND TerminalPrinter should not be enabled at the same time since it will -# result in duplicate output. printraw does not print to the console so i had -# to make another one. -# ------------------------------------------------------------------------------ -class ConsolePrinter: - extends Printer - var _buffer = '' - - func _init(): - _printer_name = 'console' - - # suppresses output until it encounters a newline to keep things - # inline as much as possible. - func _output(text): - if(text.ends_with("\n")): - print(_buffer + text.left(text.length() -1)) - _buffer = '' - else: - _buffer += text - -# ------------------------------------------------------------------------------ -# Prints text to terminal, formats some words. -# ------------------------------------------------------------------------------ -class TerminalPrinter: - extends Printer - - var escape = PoolByteArray([0x1b]).get_string_from_ascii() - var cmd_colors = { - red = escape + '[31m', - yellow = escape + '[33m', - green = escape + '[32m', - - underline = escape + '[4m', - bold = escape + '[1m', - - default = escape + '[0m', - - clear_line = escape + '[2K' - } - - func _init(): - _printer_name = 'terminal' - - func _output(text): - # Note, printraw does not print to the console. - printraw(text) - - func format_text(text, fmt): - return cmd_colors[fmt] + text + cmd_colors.default - - func clear_line(): - send(cmd_colors.clear_line) - - func back(n): - send(escape + str('[', n, 'D')) - - func forward(n): - send(escape + str('[', n, 'C')) diff --git a/addons/gut/signal_watcher.gd b/addons/gut/signal_watcher.gd deleted file mode 100644 index e127421..0000000 --- a/addons/gut/signal_watcher.gd +++ /dev/null @@ -1,166 +0,0 @@ -# ############################################################################## -# The MIT License (MIT) -# ===================== -# -# Copyright (c) 2020 Tom "Butch" Wesley -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ############################################################################## - -# Some arbitrary string that should never show up by accident. If it does, then -# shame on you. -const ARG_NOT_SET = '_*_argument_*_is_*_not_set_*_' - -# This hash holds the objects that are being watched, the signals that are being -# watched, and an array of arrays that contains arguments that were passed -# each time the signal was emitted. -# -# For example: -# _watched_signals => { -# ref1 => { -# 'signal1' => [[], [], []], -# 'signal2' => [[p1, p2]], -# 'signal3' => [[p1]] -# }, -# ref2 => { -# 'some_signal' => [], -# 'other_signal' => [[p1, p2, p3], [p1, p2, p3], [p1, p2, p3]] -# } -# } -# -# In this sample: -# - signal1 on the ref1 object was emitted 3 times and each time, zero -# parameters were passed. -# - signal3 on ref1 was emitted once and passed a single parameter -# - some_signal on ref2 was never emitted. -# - other_signal on ref2 was emitted 3 times, each time with 3 parameters. -var _watched_signals = {} -var _utils = load('res://addons/gut/utils.gd').get_instance() - -func _add_watched_signal(obj, name): - # SHORTCIRCUIT - ignore dupes - if(_watched_signals.has(obj) and _watched_signals[obj].has(name)): - return - - if(!_watched_signals.has(obj)): - _watched_signals[obj] = {name:[]} - else: - _watched_signals[obj][name] = [] - obj.connect(name, self, '_on_watched_signal', [obj, name]) - -# This handles all the signals that are watched. It supports up to 9 parameters -# which could be emitted by the signal and the two parameters used when it is -# connected via watch_signal. I chose 9 since you can only specify up to 9 -# parameters when dynamically calling a method via call (per the Godot -# documentation, i.e. some_object.call('some_method', 1, 2, 3...)). -# -# Based on the documentation of emit_signal, it appears you can only pass up -# to 4 parameters when firing a signal. I haven't verified this, but this should -# future proof this some if the value ever grows. -func _on_watched_signal(arg1=ARG_NOT_SET, arg2=ARG_NOT_SET, arg3=ARG_NOT_SET, \ - arg4=ARG_NOT_SET, arg5=ARG_NOT_SET, arg6=ARG_NOT_SET, \ - arg7=ARG_NOT_SET, arg8=ARG_NOT_SET, arg9=ARG_NOT_SET, \ - arg10=ARG_NOT_SET, arg11=ARG_NOT_SET): - var args = [arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11] - - # strip off any unused vars. - var idx = args.size() -1 - while(str(args[idx]) == ARG_NOT_SET): - args.remove(idx) - idx -= 1 - - # retrieve object and signal name from the array and remove them. These - # will always be at the end since they are added when the connect happens. - var signal_name = args[args.size() -1] - args.pop_back() - var object = args[args.size() -1] - args.pop_back() - - _watched_signals[object][signal_name].append(args) - -func does_object_have_signal(object, signal_name): - var signals = object.get_signal_list() - for i in range(signals.size()): - if(signals[i]['name'] == signal_name): - return true - return false - -func watch_signals(object): - var signals = object.get_signal_list() - for i in range(signals.size()): - _add_watched_signal(object, signals[i]['name']) - -func watch_signal(object, signal_name): - var did = false - if(does_object_have_signal(object, signal_name)): - _add_watched_signal(object, signal_name) - did = true - return did - -func get_emit_count(object, signal_name): - var to_return = -1 - if(is_watching(object, signal_name)): - to_return = _watched_signals[object][signal_name].size() - return to_return - -func did_emit(object, signal_name): - var did = false - if(is_watching(object, signal_name)): - did = get_emit_count(object, signal_name) != 0 - return did - -func print_object_signals(object): - var list = object.get_signal_list() - for i in range(list.size()): - print(list[i].name, "\n ", list[i]) - -func get_signal_parameters(object, signal_name, index=-1): - var params = null - if(is_watching(object, signal_name)): - var all_params = _watched_signals[object][signal_name] - if(all_params.size() > 0): - if(index == -1): - index = all_params.size() -1 - params = all_params[index] - return params - -func is_watching_object(object): - return _watched_signals.has(object) - -func is_watching(object, signal_name): - return _watched_signals.has(object) and _watched_signals[object].has(signal_name) - -func clear(): - for obj in _watched_signals: - if(_utils.is_not_freed(obj)): - for signal_name in _watched_signals[obj]: - obj.disconnect(signal_name, self, '_on_watched_signal') - _watched_signals.clear() - -# Returns a list of all the signal names that were emitted by the object. -# If the object is not being watched then an empty list is returned. -func get_signals_emitted(obj): - var emitted = [] - if(is_watching_object(obj)): - for signal_name in _watched_signals[obj]: - if(_watched_signals[obj][signal_name].size() > 0): - emitted.append(signal_name) - - return emitted diff --git a/addons/gut/source_code_pro.fnt b/addons/gut/source_code_pro.fnt deleted file mode 100644 index 3367650f77041f8020c39f0b1bb90d21e2ddf425..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 26499 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

F`ud^Dyusp94YMD$s2-p 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 max_size - 10 and max_size != -1): - to_return = str(src.substr(0, max_size - 10), '...', src.substr(src.length() - 10, src.length())) - return to_return - - -func _get_indent_text(times, pad): - var to_return = '' - for i in range(times): - to_return += pad - - return to_return - -func indent_text(text, times, pad): - if(times == 0): - return text - - var to_return = text - var ending_newline = '' - - if(text.ends_with("\n")): - ending_newline = "\n" - to_return = to_return.left(to_return.length() -1) - - var padding = _get_indent_text(times, pad) - to_return = to_return.replace("\n", "\n" + padding) - to_return += ending_newline - - return padding + to_return diff --git a/addons/gut/stub_params.gd b/addons/gut/stub_params.gd deleted file mode 100644 index 8a749f4..0000000 --- a/addons/gut/stub_params.gd +++ /dev/null @@ -1,43 +0,0 @@ -var return_val = null -var stub_target = null -var target_subpath = null -var parameters = null -var stub_method = null -var call_super = false - -const NOT_SET = '|_1_this_is_not_set_1_|' - -func _init(target=null, method=null, subpath=null): - stub_target = target - stub_method = method - target_subpath = subpath - -func to_return(val): - return_val = val - call_super = false - return self - -func to_do_nothing(): - return to_return(null) - -func to_call_super(): - call_super = true - return self - -func when_passed(p1=NOT_SET,p2=NOT_SET,p3=NOT_SET,p4=NOT_SET,p5=NOT_SET,p6=NOT_SET,p7=NOT_SET,p8=NOT_SET,p9=NOT_SET,p10=NOT_SET): - parameters = [p1,p2,p3,p4,p5,p6,p7,p8,p9,p10] - var idx = 0 - while(idx < parameters.size()): - if(str(parameters[idx]) == NOT_SET): - parameters.remove(idx) - else: - idx += 1 - return self - -func to_s(): - var base_string = str(stub_target, '[', target_subpath, '].', stub_method) - if(call_super): - base_string += " to call SUPER" - else: - base_string += str(' with (', parameters, ') = ', return_val) - return base_string diff --git a/addons/gut/stubber.gd b/addons/gut/stubber.gd deleted file mode 100644 index 590f17d..0000000 --- a/addons/gut/stubber.gd +++ /dev/null @@ -1,163 +0,0 @@ -# { -# inst_id_or_path1:{ -# method_name1: [StubParams, StubParams], -# method_name2: [StubParams, StubParams] -# }, -# inst_id_or_path2:{ -# method_name1: [StubParams, StubParams], -# method_name2: [StubParams, StubParams] -# } -# } -var returns = {} -var _utils = load('res://addons/gut/utils.gd').get_instance() -var _lgr = _utils.get_logger() -var _strutils = _utils.Strutils.new() - -func _make_key_from_metadata(doubled): - var to_return = doubled.__gut_metadata_.path - if(doubled.__gut_metadata_.subpath != ''): - to_return += str('-', doubled.__gut_metadata_.subpath) - return to_return - -# Creates they key for the returns hash based on the type of object passed in -# obj could be a string of a path to a script with an optional subpath or -# it could be an instance of a doubled object. -func _make_key_from_variant(obj, subpath=null): - var to_return = null - - match typeof(obj): - TYPE_STRING: - # this has to match what is done in _make_key_from_metadata - to_return = obj - if(subpath != null and subpath != ''): - to_return += str('-', subpath) - TYPE_OBJECT: - if(_utils.is_instance(obj)): - to_return = _make_key_from_metadata(obj) - elif(_utils.is_native_class(obj)): - to_return = _utils.get_native_class_name(obj) - else: - to_return = obj.resource_path - return to_return - -func _add_obj_method(obj, method, subpath=null): - var key = _make_key_from_variant(obj, subpath) - if(_utils.is_instance(obj)): - key = obj - - if(!returns.has(key)): - returns[key] = {} - if(!returns[key].has(method)): - returns[key][method] = [] - - return key - -# ############## -# Public -# ############## - -# TODO: This method is only used in tests and should be refactored out. It -# does not support inner classes and isn't helpful. -func set_return(obj, method, value, parameters=null): - var key = _add_obj_method(obj, method) - var sp = _utils.StubParams.new(key, method) - sp.parameters = parameters - sp.return_val = value - returns[key][method].append(sp) - -func add_stub(stub_params): - if(stub_params.stub_method == '_init'): - _lgr.error("You cannot stub _init. Super's _init is ALWAYS called.") - else: - var key = _add_obj_method(stub_params.stub_target, stub_params.stub_method, stub_params.target_subpath) - returns[key][stub_params.stub_method].append(stub_params) - -# Searches returns for an entry that matches the instance or the class that -# passed in obj is. -# -# obj can be an instance, class, or a path. -func _find_stub(obj, method, parameters=null): - var key = _make_key_from_variant(obj) - var to_return = null - - if(_utils.is_instance(obj)): - if(returns.has(obj) and returns[obj].has(method)): - key = obj - elif(obj.get('__gut_metadata_')): - key = _make_key_from_metadata(obj) - - if(returns.has(key) and returns[key].has(method)): - var param_idx = -1 - var null_idx = -1 - - for i in range(returns[key][method].size()): - if(returns[key][method][i].parameters == parameters): - param_idx = i - if(returns[key][method][i].parameters == null): - null_idx = i - - # We have matching parameter values so return the stub value for that - if(param_idx != -1): - to_return = returns[key][method][param_idx] - # We found a case where the parameters were not specified so return - # parameters for that - elif(null_idx != -1): - to_return = returns[key][method][null_idx] - else: - _lgr.warn(str('Call to [', method, '] was not stubbed for the supplied parameters ', parameters, '. Null was returned.')) - - return to_return - -# Gets a stubbed return value for the object and method passed in. If the -# instance was stubbed it will use that, otherwise it will use the path and -# subpath of the object to try to find a value. -# -# It will also use the optional list of parameter values to find a value. If -# the object was stubbed with no parameters than any parameters will match. -# If it was stubbed with specific parameter values then it will try to match. -# If the parameters do not match BUT there was also an empty parameter list stub -# then it will return those. -# If it cannot find anything that matches then null is returned.for -# -# Parameters -# obj: this should be an instance of a doubled object. -# method: the method called -# parameters: optional array of parameter vales to find a return value for. -func get_return(obj, method, parameters=null): - var stub_info = _find_stub(obj, method, parameters) - - if(stub_info != null): - return stub_info.return_val - else: - return null - -func should_call_super(obj, method, parameters=null): - var stub_info = _find_stub(obj, method, parameters) - if(stub_info != null): - return stub_info.call_super - else: - # this log message is here because of how the generated doubled scripts - # are structured. With this log msg here, you will only see one - # "unstubbed" info instead of multiple. - _lgr.info('Unstubbed call to ' + method + '::' + _strutils.type2str(obj)) - return false - - -func clear(): - returns.clear() - -func get_logger(): - return _lgr - -func set_logger(logger): - _lgr = logger - -func to_s(): - var text = '' - for thing in returns: - text += str(thing) + "\n" - for method in returns[thing]: - text += str("\t", method, "\n") - for i in range(returns[thing][method].size()): - text += "\t\t" + returns[thing][method][i].to_s() + "\n" - return text diff --git a/addons/gut/summary.gd b/addons/gut/summary.gd deleted file mode 100644 index 288a584..0000000 --- a/addons/gut/summary.gd +++ /dev/null @@ -1,171 +0,0 @@ -# ------------------------------------------------------------------------------ -# Contains all the results of a single test. Allows for multiple asserts results -# and pending calls. -# ------------------------------------------------------------------------------ -class Test: - var pass_texts = [] - var fail_texts = [] - var pending_texts = [] - - # NOTE: The "failed" and "pending" text must match what is outputted by - # the logger in order for text highlighting to occur in summary. - func to_s(): - var pad = ' ' - var to_return = '' - for i in range(fail_texts.size()): - to_return += str(pad, '[Failed]: ', fail_texts[i], "\n") - for i in range(pending_texts.size()): - to_return += str(pad, '[Pending]: ', pending_texts[i], "\n") - return to_return - -# ------------------------------------------------------------------------------ -# Contains all the results for a single test-script/inner class. Persists the -# names of the tests and results and the order in which the tests were run. -# ------------------------------------------------------------------------------ -class TestScript: - var name = 'NOT_SET' - var _tests = {} - var _test_order = [] - - func _init(script_name): - name = script_name - - func get_pass_count(): - var count = 0 - for key in _tests: - count += _tests[key].pass_texts.size() - return count - - func get_fail_count(): - var count = 0 - for key in _tests: - count += _tests[key].fail_texts.size() - return count - - func get_pending_count(): - var count = 0 - for key in _tests: - count += _tests[key].pending_texts.size() - return count - - func get_test_obj(obj_name): - if(!_tests.has(obj_name)): - _tests[obj_name] = Test.new() - _test_order.append(obj_name) - return _tests[obj_name] - - func add_pass(test_name, reason): - var t = get_test_obj(test_name) - t.pass_texts.append(reason) - - func add_fail(test_name, reason): - var t = get_test_obj(test_name) - t.fail_texts.append(reason) - - func add_pending(test_name, reason): - var t = get_test_obj(test_name) - t.pending_texts.append(reason) - -# ------------------------------------------------------------------------------ -# Summary Class -# -# This class holds the results of all the test scripts and Inner Classes that -# were run. -# -------------------------------------------d----------------------------------- -var _scripts = [] - -func add_script(name): - _scripts.append(TestScript.new(name)) - -func get_scripts(): - return _scripts - -func get_current_script(): - return _scripts[_scripts.size() - 1] - -func add_test(test_name): - get_current_script().get_test_obj(test_name) - -func add_pass(test_name, reason = ''): - get_current_script().add_pass(test_name, reason) - -func add_fail(test_name, reason = ''): - get_current_script().add_fail(test_name, reason) - -func add_pending(test_name, reason = ''): - get_current_script().add_pending(test_name, reason) - -func get_test_text(test_name): - return test_name + "\n" + get_current_script().get_test_obj(test_name).to_s() - -# Gets the count of unique script names minus the . at the -# end. Used for displaying the number of scripts without including all the -# Inner Classes. -func get_non_inner_class_script_count(): - var unique_scripts = {} - for i in range(_scripts.size()): - var ext_loc = _scripts[i].name.find_last('.gd.') - if(ext_loc == -1): - unique_scripts[_scripts[i].name] = 1 - else: - unique_scripts[_scripts[i].name.substr(0, ext_loc + 3)] = 1 - return unique_scripts.keys().size() - -func get_totals(): - var totals = { - passing = 0, - pending = 0, - failing = 0, - tests = 0, - scripts = 0 - } - - for s in range(_scripts.size()): - totals.passing += _scripts[s].get_pass_count() - totals.pending += _scripts[s].get_pending_count() - totals.failing += _scripts[s].get_fail_count() - totals.tests += _scripts[s]._test_order.size() - - totals.scripts = get_non_inner_class_script_count() - - return totals - -func log_summary_text(lgr): - var orig_indent = lgr.get_indent_level() - var found_failing_or_pending = false - - for s in range(_scripts.size()): - lgr.set_indent_level(0) - if(_scripts[s].get_fail_count() > 0 or _scripts[s].get_pending_count() > 0): - lgr.log(_scripts[s].name, lgr.fmts.underline) - - - for t in range(_scripts[s]._test_order.size()): - var tname = _scripts[s]._test_order[t] - var test = _scripts[s].get_test_obj(tname) - if(test.fail_texts.size() > 0 or test.pending_texts.size() > 0): - found_failing_or_pending = true - lgr.log(str('- ', tname)) - lgr.inc_indent() - - for i in range(test.fail_texts.size()): - lgr.failed(test.fail_texts[i]) - for i in range(test.pending_texts.size()): - lgr.pending(test.pending_texts[i]) - lgr.dec_indent() - - lgr.set_indent_level(0) - if(!found_failing_or_pending): - lgr.log('All tests passed', lgr.fmts.green) - - lgr.log() - var _totals = get_totals() - lgr.log("Totals", lgr.fmts.yellow) - lgr.log(str('Scripts: ', get_non_inner_class_script_count())) - lgr.log(str('Tests: ', _totals.tests)) - lgr.log(str('Passing asserts: ', _totals.passing)) - lgr.log(str('Failing asserts: ',_totals.failing)) - lgr.log(str('Pending: ', _totals.pending)) - - lgr.set_indent_level(orig_indent) - diff --git a/addons/gut/test.gd b/addons/gut/test.gd deleted file mode 100644 index ca665f7..0000000 --- a/addons/gut/test.gd +++ /dev/null @@ -1,1566 +0,0 @@ -# ############################################################################## -#(G)odot (U)nit (T)est class -# -# ############################################################################## -# The MIT License (MIT) -# ===================== -# -# Copyright (c) 2020 Tom "Butch" Wesley -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ############################################################################## -# View readme for usage details. -# -# Version - see gut.gd -# ############################################################################## -# Class that all test scripts must extend. -# -# This provides all the asserts and other testing features. Test scripts are -# run by the Gut class in gut.gd -# ############################################################################## -extends Node - -# ------------------------------------------------------------------------------ -# Helper class to hold info for objects to double. This extracts info and has -# some convenience methods. This is key in being able to make the "smart double" -# method which makes doubling much easier for the user. -# ------------------------------------------------------------------------------ -class DoubleInfo: - var path - var subpath - var strategy - var make_partial - var extension - var _utils = load('res://addons/gut/utils.gd').get_instance() - var _is_native = false - var is_valid = false - - # Flexible init method. p2 can be subpath or stategy unless p3 is - # specified, then p2 must be subpath and p3 is strategy. - # - # Examples: - # (object_to_double) - # (object_to_double, subpath) - # (object_to_double, strategy) - # (object_to_double, subpath, strategy) - func _init(thing, p2=null, p3=null): - strategy = p2 - - # short-circuit and ensure that is_valid - # is not set to true. - if(_utils.is_instance(thing)): - return - - if(typeof(p2) == TYPE_STRING): - strategy = p3 - subpath = p2 - - if(typeof(thing) == TYPE_OBJECT): - if(_utils.is_native_class(thing)): - path = thing - _is_native = true - extension = 'native_class_not_used' - else: - path = thing.resource_path - else: - path = thing - - if(!_is_native): - extension = path.get_extension() - - is_valid = true - - func is_scene(): - return extension == 'tscn' - - func is_script(): - return extension == 'gd' - - func is_native(): - return _is_native - -# ------------------------------------------------------------------------------ -# Begin test.gd -# ------------------------------------------------------------------------------ -var _utils = load('res://addons/gut/utils.gd').get_instance() -var _compare = _utils.Comparator.new() - -# constant for signal when calling yield_for -const YIELD = 'timeout' - -# Need a reference to the instance that is running the tests. This -# is set by the gut class when it runs the tests. This gets you -# access to the asserts in the tests you write. -var gut = null - -var _disable_strict_datatype_checks = false -# Holds all the text for a test's fail/pass. This is used for testing purposes -# to see the text of a failed sub-test in test_test.gd -var _fail_pass_text = [] - -const EDITOR_PROPERTY = PROPERTY_USAGE_SCRIPT_VARIABLE | PROPERTY_USAGE_DEFAULT -const VARIABLE_PROPERTY = PROPERTY_USAGE_SCRIPT_VARIABLE - -# Used with assert_setget -enum { - DEFAULT_SETTER_GETTER, - SETTER_ONLY, - GETTER_ONLY -} - -# Summary counts for the test. -var _summary = { - asserts = 0, - passed = 0, - failed = 0, - tests = 0, - pending = 0 -} - -# This is used to watch signals so we can make assertions about them. -var _signal_watcher = load('res://addons/gut/signal_watcher.gd').new() - -# Convenience copy of _utils.DOUBLE_STRATEGY -var DOUBLE_STRATEGY = null -var _lgr = _utils.get_logger() -var _strutils = _utils.Strutils.new() - -# syntax sugar -var ParameterFactory = _utils.ParameterFactory -var CompareResult = _utils.CompareResult - -func _init(): - DOUBLE_STRATEGY = _utils.DOUBLE_STRATEGY # yes, this is right - -func _str(thing): - return _strutils.type2str(thing) - -# ------------------------------------------------------------------------------ -# Fail an assertion. Causes test and script to fail as well. -# ------------------------------------------------------------------------------ -func _fail(text): - _summary.asserts += 1 - _summary.failed += 1 - _fail_pass_text.append('failed: ' + text) - if(gut): - _lgr.failed(text) - gut._fail(text) - -# ------------------------------------------------------------------------------ -# Pass an assertion. -# ------------------------------------------------------------------------------ -func _pass(text): - _summary.asserts += 1 - _summary.passed += 1 - _fail_pass_text.append('passed: ' + text) - if(gut): - _lgr.passed(text) - gut._pass(text) - -# ------------------------------------------------------------------------------ -# Checks if the datatypes passed in match. If they do not then this will cause -# a fail to occur. If they match then TRUE is returned, FALSE if not. This is -# used in all the assertions that compare values. -# ------------------------------------------------------------------------------ -func _do_datatypes_match__fail_if_not(got, expected, text): - var did_pass = true - - if(!_disable_strict_datatype_checks): - var got_type = typeof(got) - var expect_type = typeof(expected) - if(got_type != expect_type and got != null and expected != null): - # If we have a mismatch between float and int (types 2 and 3) then - # print out a warning but do not fail. - if([2, 3].has(got_type) and [2, 3].has(expect_type)): - _lgr.warn(str('Warn: Float/Int comparison. Got ', _strutils.types[got_type], - ' but expected ', _strutils.types[expect_type])) - else: - _fail('Cannot compare ' + _strutils.types[got_type] + '[' + _str(got) + '] to ' + \ - _strutils.types[expect_type] + '[' + _str(expected) + ']. ' + text) - did_pass = false - - return did_pass - -# ------------------------------------------------------------------------------ -# Create a string that lists all the methods that were called on an spied -# instance. -# ------------------------------------------------------------------------------ -func _get_desc_of_calls_to_instance(inst): - var BULLET = ' * ' - var calls = gut.get_spy().get_call_list_as_string(inst) - # indent all the calls - calls = BULLET + calls.replace("\n", "\n" + BULLET) - # remove trailing newline and bullet - calls = calls.substr(0, calls.length() - BULLET.length() - 1) - return "Calls made on " + str(inst) + "\n" + calls - -# ------------------------------------------------------------------------------ -# Signal assertion helper. Do not call directly, use _can_make_signal_assertions -# ------------------------------------------------------------------------------ -func _fail_if_does_not_have_signal(object, signal_name): - var did_fail = false - if(!_signal_watcher.does_object_have_signal(object, signal_name)): - _fail(str('Object ', object, ' does not have the signal [', signal_name, ']')) - did_fail = true - return did_fail - -# ------------------------------------------------------------------------------ -# Signal assertion helper. Do not call directly, use _can_make_signal_assertions -# ------------------------------------------------------------------------------ -func _fail_if_not_watching(object): - var did_fail = false - if(!_signal_watcher.is_watching_object(object)): - _fail(str('Cannot make signal assertions because the object ', object, \ - ' is not being watched. Call watch_signals(some_object) to be able to make assertions about signals.')) - did_fail = true - return did_fail - -# ------------------------------------------------------------------------------ -# Returns text that contains original text and a list of all the signals that -# were emitted for the passed in object. -# ------------------------------------------------------------------------------ -func _get_fail_msg_including_emitted_signals(text, object): - return str(text," (Signals emitted: ", _signal_watcher.get_signals_emitted(object), ")") - -# ------------------------------------------------------------------------------ -# This validates that parameters is an array and generates a specific error -# and a failure with a specific message -# ------------------------------------------------------------------------------ -func _fail_if_parameters_not_array(parameters): - var invalid = parameters != null and typeof(parameters) != TYPE_ARRAY - if(invalid): - _lgr.error('The "parameters" parameter must be an array of expected parameter values.') - _fail('Cannot compare paramter values because an array was not passed.') - return invalid - - -func _create_obj_from_type(type): - var obj = null - if type.is_class("PackedScene"): - obj = type.instance() - add_child(obj) - else: - obj = type.new() - return obj - - -func _get_type_from_obj(obj): - var type = null - if obj.has_method(get_filename()): - type = load(obj.get_filename()) - else: - type = obj.get_script() - return type - -# ####################### -# Virtual Methods -# ####################### - -# alias for prerun_setup -func before_all(): - pass - -# alias for setup -func before_each(): - pass - -# alias for postrun_teardown -func after_all(): - pass - -# alias for teardown -func after_each(): - pass - -# ####################### -# Public -# ####################### - -func get_logger(): - return _lgr - -func set_logger(logger): - _lgr = logger - - -# ####################### -# Asserts -# ####################### - -# ------------------------------------------------------------------------------ -# Asserts that the expected value equals the value got. -# ------------------------------------------------------------------------------ -func assert_eq(got, expected, text=""): - - if(_do_datatypes_match__fail_if_not(got, expected, text)): - var disp = "[" + _str(got) + "] expected to equal [" + _str(expected) + "]: " + text - var result = null - - if(typeof(got) == TYPE_ARRAY): - result = _compare.shallow(got, expected) - else: - result = _compare.simple(got, expected) - - if(typeof(got) in [TYPE_ARRAY, TYPE_DICTIONARY]): - disp = str(result.summary, ' ', text) - - if(result.are_equal): - _pass(disp) - else: - _fail(disp) - - -# ------------------------------------------------------------------------------ -# Asserts that the value got does not equal the "not expected" value. -# ------------------------------------------------------------------------------ -func assert_ne(got, not_expected, text=""): - if(_do_datatypes_match__fail_if_not(got, not_expected, text)): - var disp = "[" + _str(got) + "] expected to not equal [" + _str(not_expected) + "]: " + text - var result = null - - if(typeof(got) == TYPE_ARRAY): - result = _compare.shallow(got, not_expected) - else: - result = _compare.simple(got, not_expected) - - if(typeof(got) in [TYPE_ARRAY, TYPE_DICTIONARY]): - disp = str(result.summary, ' ', text) - - if(result.are_equal): - _fail(disp) - else: - _pass(disp) - - -# ------------------------------------------------------------------------------ -# Asserts that the expected value almost equals the value got. -# ------------------------------------------------------------------------------ -func assert_almost_eq(got, expected, error_interval, text=''): - var disp = "[" + _str(got) + "] expected to equal [" + _str(expected) + "] +/- [" + str(error_interval) + "]: " + text - if(_do_datatypes_match__fail_if_not(got, expected, text) and _do_datatypes_match__fail_if_not(got, error_interval, text)): - if(got < (expected - error_interval) or got > (expected + error_interval)): - _fail(disp) - else: - _pass(disp) - -# ------------------------------------------------------------------------------ -# Asserts that the expected value does not almost equal the value got. -# ------------------------------------------------------------------------------ -func assert_almost_ne(got, not_expected, error_interval, text=''): - var disp = "[" + _str(got) + "] expected to not equal [" + _str(not_expected) + "] +/- [" + str(error_interval) + "]: " + text - if(_do_datatypes_match__fail_if_not(got, not_expected, text) and _do_datatypes_match__fail_if_not(got, error_interval, text)): - if(got < (not_expected - error_interval) or got > (not_expected + error_interval)): - _pass(disp) - else: - _fail(disp) - -# ------------------------------------------------------------------------------ -# Asserts got is greater than expected -# ------------------------------------------------------------------------------ -func assert_gt(got, expected, text=""): - var disp = "[" + _str(got) + "] expected to be > than [" + _str(expected) + "]: " + text - if(_do_datatypes_match__fail_if_not(got, expected, text)): - if(got > expected): - _pass(disp) - else: - _fail(disp) - -# ------------------------------------------------------------------------------ -# Asserts got is less than expected -# ------------------------------------------------------------------------------ -func assert_lt(got, expected, text=""): - var disp = "[" + _str(got) + "] expected to be < than [" + _str(expected) + "]: " + text - if(_do_datatypes_match__fail_if_not(got, expected, text)): - if(got < expected): - _pass(disp) - else: - _fail(disp) - -# ------------------------------------------------------------------------------ -# asserts that got is true -# ------------------------------------------------------------------------------ -func assert_true(got, text=""): - if(typeof(got) == TYPE_BOOL): - if(got): - _pass(text) - else: - _fail(text) - else: - var msg = str("Cannot convert ", _strutils.type2str(got), " to boolean") - _fail(msg) - -# ------------------------------------------------------------------------------ -# Asserts that got is false -# ------------------------------------------------------------------------------ -func assert_false(got, text=""): - if(typeof(got) == TYPE_BOOL): - if(got): - _fail(text) - else: - _pass(text) - else: - var msg = str("Cannot convert ", _strutils.type2str(got), " to boolean") - _fail(msg) - -# ------------------------------------------------------------------------------ -# Asserts value is between (inclusive) the two expected values. -# ------------------------------------------------------------------------------ -func assert_between(got, expect_low, expect_high, text=""): - var disp = "[" + _str(got) + "] expected to be between [" + _str(expect_low) + "] and [" + str(expect_high) + "]: " + text - - if(_do_datatypes_match__fail_if_not(got, expect_low, text) and _do_datatypes_match__fail_if_not(got, expect_high, text)): - if(expect_low > expect_high): - disp = "INVALID range. [" + str(expect_low) + "] is not less than [" + str(expect_high) + "]" - _fail(disp) - else: - if(got < expect_low or got > expect_high): - _fail(disp) - else: - _pass(disp) - -# ------------------------------------------------------------------------------ -# Asserts value is not between (exclusive) the two expected values. -# ------------------------------------------------------------------------------ -func assert_not_between(got, expect_low, expect_high, text=""): - var disp = "[" + _str(got) + "] expected not to be between [" + _str(expect_low) + "] and [" + str(expect_high) + "]: " + text - - if(_do_datatypes_match__fail_if_not(got, expect_low, text) and _do_datatypes_match__fail_if_not(got, expect_high, text)): - if(expect_low > expect_high): - disp = "INVALID range. [" + str(expect_low) + "] is not less than [" + str(expect_high) + "]" - _fail(disp) - else: - if(got > expect_low and got < expect_high): - _fail(disp) - else: - _pass(disp) - -# ------------------------------------------------------------------------------ -# Uses the 'has' method of the object passed in to determine if it contains -# the passed in element. -# ------------------------------------------------------------------------------ -func assert_has(obj, element, text=""): - var disp = str('Expected [', _str(obj), '] to contain value: [', _str(element), ']: ', text) - if(obj.has(element)): - _pass(disp) - else: - _fail(disp) - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func assert_does_not_have(obj, element, text=""): - var disp = str('Expected [', _str(obj), '] to NOT contain value: [', _str(element), ']: ', text) - if(obj.has(element)): - _fail(disp) - else: - _pass(disp) - -# ------------------------------------------------------------------------------ -# Asserts that a file exists -# ------------------------------------------------------------------------------ -func assert_file_exists(file_path): - var disp = 'expected [' + file_path + '] to exist.' - var f = File.new() - if(f.file_exists(file_path)): - _pass(disp) - else: - _fail(disp) - -# ------------------------------------------------------------------------------ -# Asserts that a file should not exist -# ------------------------------------------------------------------------------ -func assert_file_does_not_exist(file_path): - var disp = 'expected [' + file_path + '] to NOT exist' - var f = File.new() - if(!f.file_exists(file_path)): - _pass(disp) - else: - _fail(disp) - -# ------------------------------------------------------------------------------ -# Asserts the specified file is empty -# ------------------------------------------------------------------------------ -func assert_file_empty(file_path): - var disp = 'expected [' + file_path + '] to be empty' - var f = File.new() - if(f.file_exists(file_path) and gut.is_file_empty(file_path)): - _pass(disp) - else: - _fail(disp) - -# ------------------------------------------------------------------------------ -# Asserts the specified file is not empty -# ------------------------------------------------------------------------------ -func assert_file_not_empty(file_path): - var disp = 'expected [' + file_path + '] to contain data' - if(!gut.is_file_empty(file_path)): - _pass(disp) - else: - _fail(disp) - -# ------------------------------------------------------------------------------ -# Asserts the object has the specified method -# ------------------------------------------------------------------------------ -func assert_has_method(obj, method): - assert_true(obj.has_method(method), _str(obj) + ' should have method: ' + method) - -# Old deprecated method name -func assert_get_set_methods(obj, property, default, set_to): - _lgr.deprecated('assert_get_set_methods', 'assert_accessors') - assert_accessors(obj, property, default, set_to) - -# ------------------------------------------------------------------------------ -# Verifies the object has get and set methods for the property passed in. The -# property isn't tied to anything, just a name to be appended to the end of -# get_ and set_. Asserts the get_ and set_ methods exist, if not, it stops there. -# If they exist then it asserts get_ returns the expected default then calls -# set_ and asserts get_ has the value it was set to. -# ------------------------------------------------------------------------------ -func assert_accessors(obj, property, default, set_to): - var fail_count = _summary.failed - var get = 'get_' + property - var set = 'set_' + property - assert_has_method(obj, get) - assert_has_method(obj, set) - # SHORT CIRCUIT - if(_summary.failed > fail_count): - return - assert_eq(obj.call(get), default, 'It should have the expected default value.') - obj.call(set, set_to) - assert_eq(obj.call(get), set_to, 'The set value should have been returned.') - - -# --------------------------------------------------------------------------- -# Property search helper. Used to retrieve Dictionary of specified property -# from passed object. Returns null if not found. -# If provided, property_usage constrains the type of property returned by -# passing either: -# EDITOR_PROPERTY for properties defined as: export(int) var some_value -# VARIABLE_PROPERTY for properties defined as: var another_value -# --------------------------------------------------------------------------- -func _find_object_property(obj, property_name, property_usage=null): - var result = null - var found = false - var properties = obj.get_property_list() - - while !found and !properties.empty(): - var property = properties.pop_back() - if property['name'] == property_name: - if property_usage == null or property['usage'] == property_usage: - result = property - found = true - return result - -# ------------------------------------------------------------------------------ -# Asserts a class exports a variable. -# ------------------------------------------------------------------------------ -func assert_exports(obj, property_name, type): - var disp = 'expected %s to have editor property [%s]' % [_str(obj), property_name] - var property = _find_object_property(obj, property_name, EDITOR_PROPERTY) - if property != null: - disp += ' of type [%s]. Got type [%s].' % [_strutils.types[type], _strutils.types[property['type']]] - if property['type'] == type: - _pass(disp) - else: - _fail(disp) - else: - _fail(disp) - -# ------------------------------------------------------------------------------ -# Signal assertion helper. -# -# Verifies that the object and signal are valid for making signal assertions. -# This will fail with specific messages that indicate why they are not valid. -# This returns true/false to indicate if the object and signal are valid. -# ------------------------------------------------------------------------------ -func _can_make_signal_assertions(object, signal_name): - return !(_fail_if_not_watching(object) or _fail_if_does_not_have_signal(object, signal_name)) - -# ------------------------------------------------------------------------------ -# Check if an object is connected to a signal on another object. Returns True -# if it is and false otherwise -# ------------------------------------------------------------------------------ -func _is_connected(signaler_obj, connect_to_obj, signal_name, method_name=""): - if(method_name != ""): - return signaler_obj.is_connected(signal_name, connect_to_obj, method_name) - else: - var connections = signaler_obj.get_signal_connection_list(signal_name) - for conn in connections: - if((conn.source == signaler_obj) and (conn.target == connect_to_obj)): - return true - return false -# ------------------------------------------------------------------------------ -# Watch the signals for an object. This must be called before you can make -# any assertions about the signals themselves. -# ------------------------------------------------------------------------------ -func watch_signals(object): - _signal_watcher.watch_signals(object) - -# ------------------------------------------------------------------------------ -# Asserts that an object is connected to a signal on another object -# -# This will fail with specific messages if the target object is not connected -# to the specified signal on the source object. -# ------------------------------------------------------------------------------ -func assert_connected(signaler_obj, connect_to_obj, signal_name, method_name=""): - pass - var method_disp = '' - if (method_name != ""): - method_disp = str(' using method: [', method_name, '] ') - var disp = str('Expected object ', _str(signaler_obj),\ - ' to be connected to signal: [', signal_name, '] on ',\ - _str(connect_to_obj), method_disp) - if(_is_connected(signaler_obj, connect_to_obj, signal_name, method_name)): - _pass(disp) - else: - _fail(disp) - -# ------------------------------------------------------------------------------ -# Asserts that an object is not connected to a signal on another object -# -# This will fail with specific messages if the target object is connected -# to the specified signal on the source object. -# ------------------------------------------------------------------------------ -func assert_not_connected(signaler_obj, connect_to_obj, signal_name, method_name=""): - var method_disp = '' - if (method_name != ""): - method_disp = str(' using method: [', method_name, '] ') - var disp = str('Expected object ', _str(signaler_obj),\ - ' to not be connected to signal: [', signal_name, '] on ',\ - _str(connect_to_obj), method_disp) - if(_is_connected(signaler_obj, connect_to_obj, signal_name, method_name)): - _fail(disp) - else: - _pass(disp) - -# ------------------------------------------------------------------------------ -# Asserts that a signal has been emitted at least once. -# -# This will fail with specific messages if the object is not being watched or -# the object does not have the specified signal -# ------------------------------------------------------------------------------ -func assert_signal_emitted(object, signal_name, text=""): - var disp = str('Expected object ', _str(object), ' to have emitted signal [', signal_name, ']: ', text) - if(_can_make_signal_assertions(object, signal_name)): - if(_signal_watcher.did_emit(object, signal_name)): - _pass(disp) - else: - _fail(_get_fail_msg_including_emitted_signals(disp, object)) - -# ------------------------------------------------------------------------------ -# Asserts that a signal has not been emitted. -# -# This will fail with specific messages if the object is not being watched or -# the object does not have the specified signal -# ------------------------------------------------------------------------------ -func assert_signal_not_emitted(object, signal_name, text=""): - var disp = str('Expected object ', _str(object), ' to NOT emit signal [', signal_name, ']: ', text) - if(_can_make_signal_assertions(object, signal_name)): - if(_signal_watcher.did_emit(object, signal_name)): - _fail(disp) - else: - _pass(disp) - -# ------------------------------------------------------------------------------ -# Asserts that a signal was fired with the specified parameters. The expected -# parameters should be passed in as an array. An optional index can be passed -# when a signal has fired more than once. The default is to retrieve the most -# recent emission of the signal. -# -# This will fail with specific messages if the object is not being watched or -# the object does not have the specified signal -# ------------------------------------------------------------------------------ -func assert_signal_emitted_with_parameters(object, signal_name, parameters, index=-1): - var disp = str('Expected object ', _str(object), ' to emit signal [', signal_name, '] with parameters ', parameters, ', got ') - if(_can_make_signal_assertions(object, signal_name)): - if(_signal_watcher.did_emit(object, signal_name)): - var parms_got = _signal_watcher.get_signal_parameters(object, signal_name, index) - var diff_result = _compare.deep(parameters, parms_got) - if(diff_result.are_equal()): - _pass(str(disp, parms_got)) - else: - _fail(str('Expected object ', _str(object), ' to emit signal [', signal_name, '] with parameters ', diff_result.summarize())) - else: - var text = str('Object ', object, ' did not emit signal [', signal_name, ']') - _fail(_get_fail_msg_including_emitted_signals(text, object)) - -# ------------------------------------------------------------------------------ -# Assert that a signal has been emitted a specific number of times. -# -# This will fail with specific messages if the object is not being watched or -# the object does not have the specified signal -# ------------------------------------------------------------------------------ -func assert_signal_emit_count(object, signal_name, times, text=""): - - if(_can_make_signal_assertions(object, signal_name)): - var count = _signal_watcher.get_emit_count(object, signal_name) - var disp = str('Expected the signal [', signal_name, '] emit count of [', count, '] to equal [', times, ']: ', text) - if(count== times): - _pass(disp) - else: - _fail(_get_fail_msg_including_emitted_signals(disp, object)) - -# ------------------------------------------------------------------------------ -# Assert that the passed in object has the specified signal -# ------------------------------------------------------------------------------ -func assert_has_signal(object, signal_name, text=""): - var disp = str('Expected object ', _str(object), ' to have signal [', signal_name, ']: ', text) - if(_signal_watcher.does_object_have_signal(object, signal_name)): - _pass(disp) - else: - _fail(disp) - -# ------------------------------------------------------------------------------ -# Returns the number of times a signal was emitted. -1 returned if the object -# is not being watched. -# ------------------------------------------------------------------------------ -func get_signal_emit_count(object, signal_name): - return _signal_watcher.get_emit_count(object, signal_name) - -# ------------------------------------------------------------------------------ -# Get the parmaters of a fired signal. If the signal was not fired null is -# returned. You can specify an optional index (use get_signal_emit_count to -# determine the number of times it was emitted). The default index is the -# latest time the signal was fired (size() -1 insetead of 0). The parameters -# returned are in an array. -# ------------------------------------------------------------------------------ -func get_signal_parameters(object, signal_name, index=-1): - return _signal_watcher.get_signal_parameters(object, signal_name, index) - -# ------------------------------------------------------------------------------ -# Get the parameters for a method call to a doubled object. By default it will -# return the most recent call. You can optionally specify an index. -# -# Returns: -# * an array of parameter values if a call the method was found -# * null when a call to the method was not found or the index specified was -# invalid. -# ------------------------------------------------------------------------------ -func get_call_parameters(object, method_name, index=-1): - var to_return = null - if(_utils.is_double(object)): - to_return = gut.get_spy().get_call_parameters(object, method_name, index) - else: - _lgr.error('You must pass a doulbed object to get_call_parameters.') - - return to_return - -# ------------------------------------------------------------------------------ -# Assert that object is an instance of a_class -# ------------------------------------------------------------------------------ -func assert_extends(object, a_class, text=''): - _lgr.deprecated('assert_extends', 'assert_is') - assert_is(object, a_class, text) - -# Alias for assert_extends -func assert_is(object, a_class, text=''): - var disp = ''#var disp = str('Expected [', _str(object), '] to be type of [', a_class, ']: ', text) - var NATIVE_CLASS = 'GDScriptNativeClass' - var GDSCRIPT_CLASS = 'GDScript' - var bad_param_2 = 'Parameter 2 must be a Class (like Node2D or Label). You passed ' - - if(typeof(object) != TYPE_OBJECT): - _fail(str('Parameter 1 must be an instance of an object. You passed: ', _str(object))) - elif(typeof(a_class) != TYPE_OBJECT): - _fail(str(bad_param_2, _str(a_class))) - else: - var a = _str(a_class) - disp = str('Expected [', _str(object), '] to extend [', _str(a_class), ']: ', text) - if(a_class.get_class() != NATIVE_CLASS and a_class.get_class() != GDSCRIPT_CLASS): - _fail(str(bad_param_2, _str(a_class))) - else: - if(object is a_class): - _pass(disp) - else: - _fail(disp) - -func _get_typeof_string(the_type): - var to_return = "" - if(_strutils.types.has(the_type)): - to_return += str(the_type, '(', _strutils.types[the_type], ')') - else: - to_return += str(the_type) - return to_return - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func assert_typeof(object, type, text=''): - var disp = str('Expected [typeof(', object, ') = ') - disp += _get_typeof_string(typeof(object)) - disp += '] to equal [' - disp += _get_typeof_string(type) + ']' - disp += '. ' + text - if(typeof(object) == type): - _pass(disp) - else: - _fail(disp) - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func assert_not_typeof(object, type, text=''): - var disp = str('Expected [typeof(', object, ') = ') - disp += _get_typeof_string(typeof(object)) - disp += '] to not equal [' - disp += _get_typeof_string(type) + ']' - disp += '. ' + text - if(typeof(object) != type): - _pass(disp) - else: - _fail(disp) - -# ------------------------------------------------------------------------------ -# Assert that text contains given search string. -# The match_case flag determines case sensitivity. -# ------------------------------------------------------------------------------ -func assert_string_contains(text, search, match_case=true): - var empty_search = 'Expected text and search strings to be non-empty. You passed \'%s\' and \'%s\'.' - var disp = 'Expected \'%s\' to contain \'%s\', match_case=%s' % [text, search, match_case] - if(text == '' or search == ''): - _fail(empty_search % [text, search]) - elif(match_case): - if(text.find(search) == -1): - _fail(disp) - else: - _pass(disp) - else: - if(text.to_lower().find(search.to_lower()) == -1): - _fail(disp) - else: - _pass(disp) - -# ------------------------------------------------------------------------------ -# Assert that text starts with given search string. -# match_case flag determines case sensitivity. -# ------------------------------------------------------------------------------ -func assert_string_starts_with(text, search, match_case=true): - var empty_search = 'Expected text and search strings to be non-empty. You passed \'%s\' and \'%s\'.' - var disp = 'Expected \'%s\' to start with \'%s\', match_case=%s' % [text, search, match_case] - if(text == '' or search == ''): - _fail(empty_search % [text, search]) - elif(match_case): - if(text.find(search) == 0): - _pass(disp) - else: - _fail(disp) - else: - if(text.to_lower().find(search.to_lower()) == 0): - _pass(disp) - else: - _fail(disp) - -# ------------------------------------------------------------------------------ -# Assert that text ends with given search string. -# match_case flag determines case sensitivity. -# ------------------------------------------------------------------------------ -func assert_string_ends_with(text, search, match_case=true): - var empty_search = 'Expected text and search strings to be non-empty. You passed \'%s\' and \'%s\'.' - var disp = 'Expected \'%s\' to end with \'%s\', match_case=%s' % [text, search, match_case] - var required_index = len(text) - len(search) - if(text == '' or search == ''): - _fail(empty_search % [text, search]) - elif(match_case): - if(text.find(search) == required_index): - _pass(disp) - else: - _fail(disp) - else: - if(text.to_lower().find(search.to_lower()) == required_index): - _pass(disp) - else: - _fail(disp) - -# ------------------------------------------------------------------------------ -# Assert that a method was called on an instance of a doubled class. If -# parameters are supplied then the params passed in when called must match. -# TODO make 3rd parameter "param_or_text" and add fourth parameter of "text" and -# then work some magic so this can have a "text" parameter without being -# annoying. -# ------------------------------------------------------------------------------ -func assert_called(inst, method_name, parameters=null): - var disp = str('Expected [',method_name,'] to have been called on ',_str(inst)) - - if(_fail_if_parameters_not_array(parameters)): - return - - if(!_utils.is_double(inst)): - _fail('You must pass a doubled instance to assert_called. Check the wiki for info on using double.') - else: - if(gut.get_spy().was_called(inst, method_name, parameters)): - _pass(disp) - else: - if(parameters != null): - disp += str(' with parameters ', parameters) - _fail(str(disp, "\n", _get_desc_of_calls_to_instance(inst))) - -# ------------------------------------------------------------------------------ -# Assert that a method was not called on an instance of a doubled class. If -# parameters are specified then this will only fail if it finds a call that was -# sent matching parameters. -# ------------------------------------------------------------------------------ -func assert_not_called(inst, method_name, parameters=null): - var disp = str('Expected [', method_name, '] to NOT have been called on ', _str(inst)) - - if(_fail_if_parameters_not_array(parameters)): - return - - if(!_utils.is_double(inst)): - _fail('You must pass a doubled instance to assert_not_called. Check the wiki for info on using double.') - else: - if(gut.get_spy().was_called(inst, method_name, parameters)): - if(parameters != null): - disp += str(' with parameters ', parameters) - _fail(str(disp, "\n", _get_desc_of_calls_to_instance(inst))) - else: - _pass(disp) - -# ------------------------------------------------------------------------------ -# Assert that a method on an instance of a doubled class was called a number -# of times. If parameters are specified then only calls with matching -# parameter values will be counted. -# ------------------------------------------------------------------------------ -func assert_call_count(inst, method_name, expected_count, parameters=null): - var count = gut.get_spy().call_count(inst, method_name, parameters) - - if(_fail_if_parameters_not_array(parameters)): - return - - var param_text = '' - if(parameters): - param_text = ' with parameters ' + str(parameters) - var disp = 'Expected [%s] on %s to be called [%s] times%s. It was called [%s] times.' - disp = disp % [method_name, _str(inst), expected_count, param_text, count] - - if(!_utils.is_double(inst)): - _fail('You must pass a doubled instance to assert_call_count. Check the wiki for info on using double.') - else: - if(count == expected_count): - _pass(disp) - else: - _fail(str(disp, "\n", _get_desc_of_calls_to_instance(inst))) - -# ------------------------------------------------------------------------------ -# Asserts the passed in value is null -# ------------------------------------------------------------------------------ -func assert_null(got, text=''): - var disp = str('Expected [', _str(got), '] to be NULL: ', text) - if(got == null): - _pass(disp) - else: - _fail(disp) - -# ------------------------------------------------------------------------------ -# Asserts the passed in value is null -# ------------------------------------------------------------------------------ -func assert_not_null(got, text=''): - var disp = str('Expected [', _str(got), '] to be anything but NULL: ', text) - if(got == null): - _fail(disp) - else: - _pass(disp) - -# ----------------------------------------------------------------------------- -# Asserts object has been freed from memory -# We pass in a title (since if it is freed, we lost all identity data) -# ----------------------------------------------------------------------------- -func assert_freed(obj, title): - var disp = title - if(is_instance_valid(obj)): - disp = _strutils.type2str(obj) + title - assert_true(not is_instance_valid(obj), "Expected [%s] to be freed" % disp) - -# ------------------------------------------------------------------------------ -# Asserts Object has not been freed from memory -# ----------------------------------------------------------------------------- -func assert_not_freed(obj, title): - var disp = title - if(is_instance_valid(obj)): - disp = _strutils.type2str(obj) + title - assert_true(is_instance_valid(obj), "Expected [%s] to not be freed" % disp) - -# ------------------------------------------------------------------------------ -# Asserts that the current test has not introduced any new orphans. This only -# applies to the test code that preceedes a call to this method so it should be -# the last thing your test does. -# ------------------------------------------------------------------------------ -func assert_no_new_orphans(text=''): - var count = gut.get_orphan_counter().get_counter('test') - var msg = '' - if(text != ''): - msg = ': ' + text - # Note that get_counter will return -1 if the counter does not exist. This - # can happen with a misplaced assert_no_new_orphans. Checking for > 0 - # ensures this will not cause some weird failure. - if(count > 0): - _fail(str('Expected no orphans, but found ', count, msg)) - else: - _pass('No new orphans found.' + msg) - -# ------------------------------------------------------------------------------ -# Returns a dictionary that contains -# - an is_valid flag whether validation was successful or not and -# - a message that gives some information about the validation errors. -# ------------------------------------------------------------------------------ -func _validate_assert_setget_called_input(type, name_property - , name_setter, name_getter): - var obj = null - var result = {"is_valid": true, "msg": ""} - - if null == type or typeof(type) != TYPE_OBJECT or not type.is_class("Resource"): - result.is_valid = false - result.msg = str("The type parameter should be a ressource, input is ", _str(type)) - return result - - if null == double(type): - result.is_valid = false - result.msg = str("Attempt to double the type parameter failed. The type parameter should be a ressource that can be doubled.") - return result - - obj = _create_obj_from_type(type) - var property = _find_object_property(obj, str(name_property)) - - if null == property: - result.is_valid = false - result.msg += str("The property %s does not exist." % _str(name_property)) - if name_setter == "" and name_getter == "": - result.is_valid = false - result.msg += str("Either setter or getter method must be specified.") - if name_setter != "" and not obj.has_method(str(name_setter)): - result.is_valid = false - result.msg += str("Setter method %s does not exist. " % _str(name_setter)) - if name_getter != "" and not obj.has_method(str(name_getter)): - result.is_valid = false - result.msg += str("Getter method %s does not exist. " %_str(name_getter)) - - obj.free() - return result - -# ------------------------------------------------------------------------------ -# Asserts the given setter and getter methods are called when the given property -# is accessed. -# ------------------------------------------------------------------------------ -func _assert_setget_called(type, name_property, setter = "", getter = ""): - var name_setter = _utils.nvl(setter, "") - var name_getter = _utils.nvl(getter, "") - - var validation = _validate_assert_setget_called_input(type, name_property, str(name_setter), str(name_getter)) - if not validation.is_valid: - _fail(validation.msg) - return - - var message = "" - var amount_calls_setter = 0 - var amount_calls_getter = 0 - var expected_calls_setter = 0 - var expected_calls_getter = 0 - var obj = _create_obj_from_type(double(type)) - - if name_setter != '': - expected_calls_setter = 1 - stub(obj, name_setter).to_do_nothing() - obj.set(name_property, null) - amount_calls_setter = gut.get_spy().call_count(obj, str(name_setter)) - - if name_getter != '': - expected_calls_getter = 1 - stub(obj, name_getter).to_do_nothing() - var new_property = obj.get(name_property) - amount_calls_getter = gut.get_spy().call_count(obj, str(name_getter)) - - obj.free() - - # assert - - if amount_calls_setter == expected_calls_setter and amount_calls_getter == expected_calls_getter: - _pass(str("setget for %s is correctly configured." % _str(name_property))) - else: - if amount_calls_setter < expected_calls_setter: - message += " The setter was not called." - elif amount_calls_setter > expected_calls_setter: - message += " The setter was called but should not have been." - if amount_calls_getter < expected_calls_getter: - message += " The getter was not called." - elif amount_calls_getter > expected_calls_getter: - message += " The getter was called but should not have been." - _fail(str(message)) - -# ------------------------------------------------------------------------------ -# Wrapper: invokes assert_setget_called but provides a slightly more convenient -# signature -# ------------------------------------------------------------------------------ -func assert_setget( - instance, name_property, - const_or_setter = DEFAULT_SETTER_GETTER, getter="__not_set__"): - - var getter_name = null - if(getter != "__not_set__"): - getter_name = getter - - var setter_name = null - if(typeof(const_or_setter) == TYPE_INT): - if(const_or_setter in [SETTER_ONLY, DEFAULT_SETTER_GETTER]): - setter_name = str("set_", name_property) - - if(const_or_setter in [GETTER_ONLY, DEFAULT_SETTER_GETTER]): - getter_name = str("get_", name_property) - else: - setter_name = const_or_setter - - var resource = null - if instance.is_class("Resource"): - resource = instance - else: - resource = _get_type_from_obj(instance) - - _assert_setget_called(resource, str(name_property), setter_name, getter_name) - - -# ------------------------------------------------------------------------------ -# Wrapper: asserts if the property exists, the accessor methods exist and the -# setget keyword is set for accessor methods -# ------------------------------------------------------------------------------ -func assert_property(instance, name_property, default_value, new_value) -> void: - var free_me = [] - var resource = null - var obj = null - if instance.is_class("Resource"): - resource = instance - obj = _create_obj_from_type(resource) - free_me.append(obj) - else: - resource = _get_type_from_obj(instance) - obj = instance - - var name_setter = "set_" + str(name_property) - var name_getter = "get_" + str(name_property) - - var pre_fail_count = get_fail_count() - assert_accessors(obj, str(name_property), default_value, new_value) - _assert_setget_called(resource, str(name_property), name_setter, name_getter) - - for entry in free_me: - entry.free() - - # assert - if get_fail_count() == pre_fail_count: - _pass(str("The property is set up as expected.")) - else: - _fail(str("The property is not set up as expected. Examine subtests to see what failed.")) - - -# ------------------------------------------------------------------------------ -# Mark the current test as pending. -# ------------------------------------------------------------------------------ -func pending(text=""): - _summary.pending += 1 - if(gut): - _lgr.pending(text) - gut._pending(text) - -# ------------------------------------------------------------------------------ -# Returns the number of times a signal was emitted. -1 returned if the object -# is not being watched. -# ------------------------------------------------------------------------------ - -# ------------------------------------------------------------------------------ -# Yield for the time sent in. The optional message will be printed when -# Gut detects the yield. When the time expires the YIELD signal will be -# emitted. -# ------------------------------------------------------------------------------ -func yield_for(time, msg=''): - return gut.set_yield_time(time, msg) - -# ------------------------------------------------------------------------------ -# Yield to a signal or a maximum amount of time, whichever comes first. When -# the conditions are met the YIELD signal will be emitted. -# ------------------------------------------------------------------------------ -func yield_to(obj, signal_name, max_wait, msg=''): - watch_signals(obj) - gut.set_yield_signal_or_time(obj, signal_name, max_wait, msg) - - return gut - -# ------------------------------------------------------------------------------ -# Ends a test that had a yield in it. You only need to use this if you do -# not make assertions after a yield. -# ------------------------------------------------------------------------------ -func end_test(): - _lgr.deprecated('end_test is no longer necessary, you can remove it.') - #gut.end_yielded_test() - -func get_summary(): - return _summary - -func get_fail_count(): - return _summary.failed - -func get_pass_count(): - return _summary.passed - -func get_pending_count(): - return _summary.pending - -func get_assert_count(): - return _summary.asserts - -func clear_signal_watcher(): - _signal_watcher.clear() - -func get_double_strategy(): - return gut.get_doubler().get_strategy() - -func set_double_strategy(double_strategy): - gut.get_doubler().set_strategy(double_strategy) - -func pause_before_teardown(): - gut.pause_before_teardown() - -# ------------------------------------------------------------------------------ -# Convert the _summary dictionary into text -# ------------------------------------------------------------------------------ -func get_summary_text(): - var to_return = get_script().get_path() + "\n" - to_return += str(' ', _summary.passed, ' of ', _summary.asserts, ' passed.') - if(_summary.pending > 0): - to_return += str("\n ", _summary.pending, ' pending') - if(_summary.failed > 0): - to_return += str("\n ", _summary.failed, ' failed.') - return to_return - -# ------------------------------------------------------------------------------ -# Double a script, inner class, or scene using a path or a loaded script/scene. -# -# -# ------------------------------------------------------------------------------ - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func _smart_double(double_info): - var override_strat = _utils.nvl(double_info.strategy, gut.get_doubler().get_strategy()) - var to_return = null - - if(double_info.is_scene()): - if(double_info.make_partial): - to_return = gut.get_doubler().partial_double_scene(double_info.path, override_strat) - else: - to_return = gut.get_doubler().double_scene(double_info.path, override_strat) - - elif(double_info.is_native()): - if(double_info.make_partial): - to_return = gut.get_doubler().partial_double_gdnative(double_info.path) - else: - to_return = gut.get_doubler().double_gdnative(double_info.path) - - elif(double_info.is_script()): - if(double_info.subpath == null): - if(double_info.make_partial): - to_return = gut.get_doubler().partial_double(double_info.path, override_strat) - else: - to_return = gut.get_doubler().double(double_info.path, override_strat) - else: - if(double_info.make_partial): - to_return = gut.get_doubler().partial_double_inner(double_info.path, double_info.subpath, override_strat) - else: - to_return = gut.get_doubler().double_inner(double_info.path, double_info.subpath, override_strat) - return to_return - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func double(thing, p2=null, p3=null): - var double_info = DoubleInfo.new(thing, p2, p3) - if(!double_info.is_valid): - _lgr.error('double requires a class or path, you passed an instance: ' + _str(thing)) - return null - - double_info.make_partial = false - - return _smart_double(double_info) - -# ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -func partial_double(thing, p2=null, p3=null): - var double_info = DoubleInfo.new(thing, p2, p3) - if(!double_info.is_valid): - _lgr.error('partial_double requires a class or path, you passed an instance: ' + _str(thing)) - return null - - double_info.make_partial = true - - return _smart_double(double_info) - - -# ------------------------------------------------------------------------------ -# Specifically double a scene -# ------------------------------------------------------------------------------ -func double_scene(path, strategy=null): - var override_strat = _utils.nvl(strategy, gut.get_doubler().get_strategy()) - return gut.get_doubler().double_scene(path, override_strat) - -# ------------------------------------------------------------------------------ -# Specifically double a script -# ------------------------------------------------------------------------------ -func double_script(path, strategy=null): - var override_strat = _utils.nvl(strategy, gut.get_doubler().get_strategy()) - return gut.get_doubler().double(path, override_strat) - -# ------------------------------------------------------------------------------ -# Specifically double an Inner class in a a script -# ------------------------------------------------------------------------------ -func double_inner(path, subpath, strategy=null): - var override_strat = _utils.nvl(strategy, gut.get_doubler().get_strategy()) - return gut.get_doubler().double_inner(path, subpath, override_strat) - -# ------------------------------------------------------------------------------ -# Add a method that the doubler will ignore. You can pass this the path to a -# script or scene or a loaded script or scene. When running tests, these -# ignores are cleared after every test. -# ------------------------------------------------------------------------------ -func ignore_method_when_doubling(thing, method_name): - var double_info = DoubleInfo.new(thing) - var path = double_info.path - - if(double_info.is_scene()): - var inst = thing.instance() - if(inst.get_script()): - path = inst.get_script().get_path() - - gut.get_doubler().add_ignored_method(path, method_name) - -# ------------------------------------------------------------------------------ -# Stub something. -# -# Parameters -# 1: the thing to stub, a file path or a instance or a class -# 2: either an inner class subpath or the method name -# 3: the method name if an inner class subpath was specified -# NOTE: right now we cannot stub inner classes at the path level so this should -# only be called with two parameters. I did the work though so I'm going -# to leave it but not update the wiki. -# ------------------------------------------------------------------------------ -func stub(thing, p2, p3=null): - if(_utils.is_instance(thing) and !_utils.is_double(thing)): - _lgr.error(str('You cannot use stub on ', _str(thing), ' because it is not a double.')) - return _utils.StubParams.new() - - var method_name = p2 - var subpath = null - if(p3 != null): - subpath = p2 - method_name = p3 - - var sp = _utils.StubParams.new(thing, method_name, subpath) - gut.get_stubber().add_stub(sp) - return sp - -# ------------------------------------------------------------------------------ -# convenience wrapper. -# ------------------------------------------------------------------------------ -func simulate(obj, times, delta): - gut.simulate(obj, times, delta) - -# ------------------------------------------------------------------------------ -# Replace the node at base_node.get_node(path) with with_this. All references -# to the node via $ and get_node(...) will now return with_this. with_this will -# get all the groups that the node that was replaced had. -# -# The node that was replaced is queued to be freed. -# -# TODO see replace_by method, this could simplify the logic here. -# ------------------------------------------------------------------------------ -func replace_node(base_node, path_or_node, with_this): - var path = path_or_node - - if(typeof(path_or_node) != TYPE_STRING): - # This will cause an engine error if it fails. It always returns a - # NodePath, even if it fails. Checking the name count is the only way - # I found to check if it found something or not (after it worked I - # didn't look any farther). - path = base_node.get_path_to(path_or_node) - if(path.get_name_count() == 0): - _lgr.error('You passed an object that base_node does not have. Cannot replace node.') - return - - if(!base_node.has_node(path)): - _lgr.error(str('Could not find node at path [', path, ']')) - return - - var to_replace = base_node.get_node(path) - var parent = to_replace.get_parent() - var replace_name = to_replace.get_name() - - parent.remove_child(to_replace) - parent.add_child(with_this) - with_this.set_name(replace_name) - with_this.set_owner(parent) - - var groups = to_replace.get_groups() - for i in range(groups.size()): - with_this.add_to_group(groups[i]) - - to_replace.queue_free() - - -# ------------------------------------------------------------------------------ -# This method does a somewhat complicated dance with Gut. It assumes that Gut -# will clear its parameter handler after it finishes calling a parameterized test -# enough times. -# ------------------------------------------------------------------------------ -func use_parameters(params): - var ph = gut.get_parameter_handler() - if(ph == null): - ph = _utils.ParameterHandler.new(params) - gut.set_parameter_handler(ph) - - var output = str('(call #', ph.get_call_count() + 1, ') with paramters: ', ph.get_current_parameters()) - _lgr.log(output) - _lgr.inc_indent() - return ph.next_parameters() - -# ------------------------------------------------------------------------------ -# Marks whatever is passed in to be freed after the test finishes. It also -# returns what is passed in so you can save a line of code. -# var thing = autofree(Thing.new()) -# ------------------------------------------------------------------------------ -func autofree(thing): - gut.get_autofree().add_free(thing) - return thing - -# ------------------------------------------------------------------------------ -# Works the same as autofree except queue_free will be called on the object -# instead. This also imparts a brief pause after the test finishes so that -# the queued object has time to free. -# ------------------------------------------------------------------------------ -func autoqfree(thing): - gut.get_autofree().add_queue_free(thing) - return thing - -# ------------------------------------------------------------------------------ -# The same as autofree but it also adds the object as a child of the test. -# ------------------------------------------------------------------------------ -func add_child_autofree(node, legible_unique_name = false): - gut.get_autofree().add_free(node) - # Explicitly calling super here b/c add_child MIGHT change and I don't want - # a bug sneaking its way in here. - .add_child(node, legible_unique_name) - return node - -# ------------------------------------------------------------------------------ -# The same as autoqfree but it also adds the object as a child of the test. -# ------------------------------------------------------------------------------ -func add_child_autoqfree(node, legible_unique_name=false): - gut.get_autofree().add_queue_free(node) - # Explicitly calling super here b/c add_child MIGHT change and I don't want - # a bug sneaking its way in here. - .add_child(node, legible_unique_name) - return node - -# ------------------------------------------------------------------------------ -# Returns true if the test is passing as of the time of this call. False if not. -# ------------------------------------------------------------------------------ -func is_passing(): - if(gut.get_current_test_object() != null and - !['before_all', 'after_all'].has(gut.get_current_test_object().name)): - return gut.get_current_test_object().passed and \ - gut.get_current_test_object().assert_count > 0 - else: - _lgr.error('No current test object found. is_passing must be called inside a test.') - return null - -# ------------------------------------------------------------------------------ -# Returns true if the test is failing as of the time of this call. False if not. -# ------------------------------------------------------------------------------ -func is_failing(): - if(gut.get_current_test_object() != null and - !['before_all', 'after_all'].has(gut.get_current_test_object().name)): - return !gut.get_current_test_object().passed - else: - _lgr.error('No current test object found. is_failing must be called inside a test.') - return null - -# ------------------------------------------------------------------------------ -# Marks the test as passing. Does not override any failing asserts or calls to -# fail_test. Same as a passing assert. -# ------------------------------------------------------------------------------ -func pass_test(text): - _pass(text) - -# ------------------------------------------------------------------------------ -# Marks the test as failing. Same as a failing assert. -# ------------------------------------------------------------------------------ -func fail_test(text): - _fail(text) - -# ------------------------------------------------------------------------------ -# Peforms a deep compare on both values, a CompareResult instnace is returned. -# The optional max_differences paramter sets the max_differences to be displayed. -# ------------------------------------------------------------------------------ -func compare_deep(v1, v2, max_differences=null): - var result = _compare.deep(v1, v2) - if(max_differences != null): - result.max_differences = max_differences - return result - -# ------------------------------------------------------------------------------ -# Peforms a shallow compare on both values, a CompareResult instnace is returned. -# The optional max_differences paramter sets the max_differences to be displayed. -# ------------------------------------------------------------------------------ -func compare_shallow(v1, v2, max_differences=null): - var result = _compare.shallow(v1, v2) - if(max_differences != null): - result.max_differences = max_differences - return result - -# ------------------------------------------------------------------------------ -# Performs a deep compare and asserts the values are equal -# ------------------------------------------------------------------------------ -func assert_eq_deep(v1, v2): - var result = compare_deep(v1, v2) - if(result.are_equal): - _pass(result.get_short_summary()) - else: - _fail(result.summary) - -# ------------------------------------------------------------------------------ -# Performs a deep compare and asserts the values are not equal -# ------------------------------------------------------------------------------ -func assert_ne_deep(v1, v2): - var result = compare_deep(v1, v2) - if(!result.are_equal): - _pass(result.get_short_summary()) - else: - _fail(result.get_short_summary()) - -# ------------------------------------------------------------------------------ -# Performs a shallow compare and asserts the values are equal -# ------------------------------------------------------------------------------ -func assert_eq_shallow(v1, v2): - var result = compare_shallow(v1, v2) - if(result.are_equal): - _pass(result.get_short_summary()) - else: - _fail(result.summary) - -# ------------------------------------------------------------------------------ -# Performs a shallow compare and asserts the values are not equal -# ------------------------------------------------------------------------------ -func assert_ne_shallow(v1, v2): - var result = compare_shallow(v1, v2) - if(!result.are_equal): - _pass(result.get_short_summary()) - else: - _fail(result.get_short_summary()) diff --git a/addons/gut/test_collector.gd b/addons/gut/test_collector.gd deleted file mode 100644 index bbed3e0..0000000 --- a/addons/gut/test_collector.gd +++ /dev/null @@ -1,286 +0,0 @@ -# ------------------------------------------------------------------------------ -# Used to keep track of info about each test ran. -# ------------------------------------------------------------------------------ -class Test: - # indicator if it passed or not. defaults to true since it takes only - # one failure to make it not pass. _fail in gut will set this. - var passed = true - # the name of the function - var name = "" - # flag to know if the name has been printed yet. - var has_printed_name = false - # the number of arguments the method has - var arg_count = 0 - # The number of asserts in the test - var assert_count = 0 - # if the test has been marked pending at anypont during - # execution. - var pending = false - - -# ------------------------------------------------------------------------------ -# This holds all the meta information for a test script. It contains the -# name of the inner class and an array of Test "structs". -# -# This class also facilitates all the exporting and importing of tests. -# ------------------------------------------------------------------------------ -class TestScript: - var inner_class_name = null - var tests = [] - var path = null - var _utils = null - var _lgr = null - - func _init(utils=null, logger=null): - _utils = utils - _lgr = logger - - func to_s(): - var to_return = path - if(inner_class_name != null): - to_return += str('.', inner_class_name) - to_return += "\n" - for i in range(tests.size()): - to_return += str(' ', tests[i].name, "\n") - return to_return - - func get_new(): - return load_script().new() - - func load_script(): - #print('loading: ', get_full_name()) - var to_return = load(path) - if(inner_class_name != null): - # If we wanted to do inner classes in inner classses - # then this would have to become some kind of loop or recursive - # call to go all the way down the chain or this class would - # have to change to hold onto the loaded class instead of - # just path information. - to_return = to_return.get(inner_class_name) - return to_return - - func get_filename_and_inner(): - var to_return = get_filename() - if(inner_class_name != null): - to_return += '.' + inner_class_name - return to_return - - func get_full_name(): - var to_return = path - if(inner_class_name != null): - to_return += '.' + inner_class_name - return to_return - - func get_filename(): - return path.get_file() - - func has_inner_class(): - return inner_class_name != null - - # Note: although this no longer needs to export the inner_class names since - # they are pulled from metadata now, it is easier to leave that in - # so we don't have to cut the export down to unique script names. - func export_to(config_file, section): - config_file.set_value(section, 'path', path) - config_file.set_value(section, 'inner_class', inner_class_name) - var names = [] - for i in range(tests.size()): - names.append(tests[i].name) - config_file.set_value(section, 'tests', names) - - func _remap_path(source_path): - var to_return = source_path - if(!_utils.file_exists(source_path)): - _lgr.debug('Checking for remap for: ' + source_path) - var remap_path = source_path.get_basename() + '.gd.remap' - if(_utils.file_exists(remap_path)): - var cf = ConfigFile.new() - cf.load(remap_path) - to_return = cf.get_value('remap', 'path') - else: - _lgr.warn('Could not find remap file ' + remap_path) - return to_return - - func import_from(config_file, section): - path = config_file.get_value(section, 'path') - path = _remap_path(path) - # Null is an acceptable value, but you can't pass null as a default to - # get_value since it thinks you didn't send a default...then it spits - # out red text. This works around that. - var inner_name = config_file.get_value(section, 'inner_class', 'Placeholder') - if(inner_name != 'Placeholder'): - inner_class_name = inner_name - else: # just being explicit - inner_class_name = null - - func get_test_named(name): - return _utils.search_array(tests, 'name', name) - -# ------------------------------------------------------------------------------ -# start test_collector, I don't think I like the name. -# ------------------------------------------------------------------------------ -var scripts = [] -var _test_prefix = 'test_' -var _test_class_prefix = 'Test' - -var _utils = load('res://addons/gut/utils.gd').get_instance() -var _lgr = _utils.get_logger() - -func _does_inherit_from_test(thing): - var base_script = thing.get_base_script() - var to_return = false - if(base_script != null): - var base_path = base_script.get_path() - if(base_path == 'res://addons/gut/test.gd'): - to_return = true - else: - to_return = _does_inherit_from_test(base_script) - return to_return - -func _populate_tests(test_script): - var methods = test_script.load_script().get_script_method_list() - for i in range(methods.size()): - var name = methods[i]['name'] - if(name.begins_with(_test_prefix)): - var t = Test.new() - t.name = name - t.arg_count = methods[i]['args'].size() - test_script.tests.append(t) - -func _get_inner_test_class_names(loaded): - var inner_classes = [] - var const_map = loaded.get_script_constant_map() - for key in const_map: - var thing = const_map[key] - if(typeof(thing) == TYPE_OBJECT): - if(key.begins_with(_test_class_prefix)): - if(_does_inherit_from_test(thing)): - inner_classes.append(key) - else: - _lgr.warn(str('Ignoring Inner Class ', key, - ' because it does not extend res://addons/gut/test.gd')) - - # This could go deeper and find inner classes within inner classes - # but requires more experimentation. Right now I'm keeping it at - # one level since that is what the previous version did and there - # has been no demand for deeper nesting. - # _populate_inner_test_classes(thing) - return inner_classes - -func _parse_script(test_script): - var inner_classes = [] - var scripts_found = [] - - var loaded = load(test_script.path) - if(_does_inherit_from_test(loaded)): - _populate_tests(test_script) - scripts_found.append(test_script.path) - inner_classes = _get_inner_test_class_names(loaded) - - for i in range(inner_classes.size()): - var loaded_inner = loaded.get(inner_classes[i]) - if(_does_inherit_from_test(loaded_inner)): - var ts = TestScript.new(_utils, _lgr) - ts.path = test_script.path - ts.inner_class_name = inner_classes[i] - _populate_tests(ts) - scripts.append(ts) - scripts_found.append(test_script.path + '[' + inner_classes[i] +']') - - return scripts_found - -# ----------------- -# Public -# ----------------- -func add_script(path): - # SHORTCIRCUIT - if(has_script(path)): - return [] - - var f = File.new() - # SHORTCIRCUIT - if(!f.file_exists(path)): - _lgr.error('Could not find script: ' + path) - return - - var ts = TestScript.new(_utils, _lgr) - ts.path = path - scripts.append(ts) - return _parse_script(ts) - -func clear(): - scripts.clear() - -func has_script(path): - var found = false - var idx = 0 - while(idx < scripts.size() and !found): - if(scripts[idx].get_full_name() == path): - found = true - else: - idx += 1 - return found - -func export_tests(path): - var success = true - var f = ConfigFile.new() - for i in range(scripts.size()): - scripts[i].export_to(f, str('TestScript-', i)) - var result = f.save(path) - if(result != OK): - _lgr.error(str('Could not save exported tests to [', path, ']. Error code: ', result)) - success = false - return success - -func import_tests(path): - var success = false - var f = ConfigFile.new() - var result = f.load(path) - if(result != OK): - _lgr.error(str('Could not load exported tests from [', path, ']. Error code: ', result)) - else: - var sections = f.get_sections() - for key in sections: - var ts = TestScript.new(_utils, _lgr) - ts.import_from(f, key) - _populate_tests(ts) - scripts.append(ts) - success = true - return success - -func get_script_named(name): - return _utils.search_array(scripts, 'get_filename_and_inner', name) - -func get_test_named(script_name, test_name): - var s = get_script_named(script_name) - if(s != null): - return s.get_test_named(test_name) - else: - return null - -func to_s(): - var to_return = '' - for i in range(scripts.size()): - to_return += scripts[i].to_s() + "\n" - return to_return - -# --------------------- -# Accessors -# --------------------- -func get_logger(): - return _lgr - -func set_logger(logger): - _lgr = logger - -func get_test_prefix(): - return _test_prefix - -func set_test_prefix(test_prefix): - _test_prefix = test_prefix - -func get_test_class_prefix(): - return _test_class_prefix - -func set_test_class_prefix(test_class_prefix): - _test_class_prefix = test_class_prefix diff --git a/addons/gut/thing_counter.gd b/addons/gut/thing_counter.gd deleted file mode 100644 index a9b0b48..0000000 --- a/addons/gut/thing_counter.gd +++ /dev/null @@ -1,43 +0,0 @@ -var things = {} - -func get_unique_count(): - return things.size() - -func add(thing): - if(things.has(thing)): - things[thing] += 1 - else: - things[thing] = 1 - -func has(thing): - return things.has(thing) - -func get(thing): - var to_return = 0 - if(things.has(thing)): - to_return = things[thing] - return to_return - -func sum(): - var count = 0 - for key in things: - count += things[key] - return count - -func to_s(): - var to_return = "" - for key in things: - to_return += str(key, ": ", things[key], "\n") - to_return += str("sum: ", sum()) - return to_return - -func get_max_count(): - var max_val = null - for key in things: - if(max_val == null or things[key] > max_val): - max_val = things[key] - return max_val - -func add_array_items(array): - for i in range(array.size()): - add(array[i]) diff --git a/addons/gut/utils.gd b/addons/gut/utils.gd deleted file mode 100644 index ce13d98..0000000 --- a/addons/gut/utils.gd +++ /dev/null @@ -1,344 +0,0 @@ -# ############################################################################## -#(G)odot (U)nit (T)est class -# -# ############################################################################## -# The MIT License (MIT) -# ===================== -# -# Copyright (c) 2020 Tom "Butch" Wesley -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ############################################################################## -# Description -# ----------- -# This class is a PSUEDO SINGLETON. You should not make instances of it but use -# the get_instance static method. -# ############################################################################## -extends Node - -# ------------------------------------------------------------------------------ -# The instance name as a function since you can't have static variables. -# ------------------------------------------------------------------------------ -static func INSTANCE_NAME(): - return '__GutUtilsInstName__' - -# ------------------------------------------------------------------------------ -# Gets the root node without having to be in the tree and pushing out an error -# if we don't have a main loop ready to go yet. -# ------------------------------------------------------------------------------ -static func get_root_node(): - var to_return = null - var main_loop = Engine.get_main_loop() - if(main_loop != null): - return main_loop.root - else: - push_error('No Main Loop Yet') - return null - -# ------------------------------------------------------------------------------ -# Get the ONE instance of utils -# ------------------------------------------------------------------------------ -static func get_instance(): - var the_root = get_root_node() - var inst = null - if(the_root.has_node(INSTANCE_NAME())): - inst = the_root.get_node(INSTANCE_NAME()) - else: - inst = load('res://addons/gut/utils.gd').new() - inst.set_name(INSTANCE_NAME()) - the_root.add_child(inst) - return inst - -var Logger = load('res://addons/gut/logger.gd') # everything should use get_logger -var _lgr = null - -var _test_mode = false - -var AutoFree = load('res://addons/gut/autofree.gd') -var Comparator = load('res://addons/gut/comparator.gd') -var CompareResult = load('res://addons/gut/compare_result.gd') -var DiffTool = load('res://addons/gut/diff_tool.gd') -var Doubler = load('res://addons/gut/doubler.gd') -var Gut = load('res://addons/gut/gut.gd') -var HookScript = load('res://addons/gut/hook_script.gd') -var MethodMaker = load('res://addons/gut/method_maker.gd') -var OneToMany = load('res://addons/gut/one_to_many.gd') -var OrphanCounter = load('res://addons/gut/orphan_counter.gd') -var ParameterFactory = load('res://addons/gut/parameter_factory.gd') -var ParameterHandler = load('res://addons/gut/parameter_handler.gd') -var Printers = load('res://addons/gut/printers.gd') -var Spy = load('res://addons/gut/spy.gd') -var Strutils = load('res://addons/gut/strutils.gd') -var Stubber = load('res://addons/gut/stubber.gd') -var StubParams = load('res://addons/gut/stub_params.gd') -var Summary = load('res://addons/gut/summary.gd') -var Test = load('res://addons/gut/test.gd') -var TestCollector = load('res://addons/gut/test_collector.gd') -var ThingCounter = load('res://addons/gut/thing_counter.gd') - -# Source of truth for the GUT version -var version = '7.1.0' -# The required Godot version as an array. -var req_godot = [3, 2, 0] -# Used for doing file manipulation stuff so as to not keep making File instances. -# could be a bit of overkill but who cares. -var _file_checker = File.new() - -const GUT_METADATA = '__gut_metadata_' - -enum DOUBLE_STRATEGY{ - FULL, - PARTIAL -} - -enum DIFF { - DEEP, - SHALLOW, - SIMPLE -} - -# ------------------------------------------------------------------------------ -# Blurb of text with GUT and Godot versions. -# ------------------------------------------------------------------------------ -func get_version_text(): - var v_info = Engine.get_version_info() - var gut_version_info = str('GUT version: ', version) - var godot_version_info = str('Godot version: ', v_info.major, '.', v_info.minor, '.', v_info.patch) - return godot_version_info + "\n" + gut_version_info - - -# ------------------------------------------------------------------------------ -# Returns a nice string for erroring out when we have a bad Godot version. -# ------------------------------------------------------------------------------ -func get_bad_version_text(): - var ver = join_array(req_godot, '.') - var info = Engine.get_version_info() - var gd_version = str(info.major, '.', info.minor, '.', info.patch) - return 'GUT ' + version + ' requires Godot ' + ver + ' or greater. Godot version is ' + gd_version - - -# ------------------------------------------------------------------------------ -# Checks the Godot version against req_godot array. -# ------------------------------------------------------------------------------ -func is_version_ok(engine_info=Engine.get_version_info(),required=req_godot): - var is_ok = null - var engine_array = [engine_info.major, engine_info.minor, engine_info.patch] - - var idx = 0 - while(is_ok == null and idx < engine_array.size()): - if(int(engine_array[idx]) > int(required[idx])): - is_ok = true - elif(int(engine_array[idx]) < int(required[idx])): - is_ok = false - - idx += 1 - - # still null means each index was the same. - return nvl(is_ok, true) - - -# ------------------------------------------------------------------------------ -# Everything should get a logger through this. -# -# When running in test mode this will always return a new logger so that errors -# are not caused by getting bad warn/error/etc counts. -# ------------------------------------------------------------------------------ -func get_logger(): - if(_test_mode): - return Logger.new() - else: - if(_lgr == null): - _lgr = Logger.new() - return _lgr - - -# ------------------------------------------------------------------------------ -# Returns an array created by splitting the string by the delimiter -# ------------------------------------------------------------------------------ -func split_string(to_split, delim): - var to_return = [] - - var loc = to_split.find(delim) - while(loc != -1): - to_return.append(to_split.substr(0, loc)) - to_split = to_split.substr(loc + 1, to_split.length() - loc) - loc = to_split.find(delim) - to_return.append(to_split) - return to_return - - -# ------------------------------------------------------------------------------ -# Returns a string containing all the elements in the array separated by delim -# ------------------------------------------------------------------------------ -func join_array(a, delim): - var to_return = '' - for i in range(a.size()): - to_return += str(a[i]) - if(i != a.size() -1): - to_return += str(delim) - return to_return - - -# ------------------------------------------------------------------------------ -# return if_null if value is null otherwise return value -# ------------------------------------------------------------------------------ -func nvl(value, if_null): - if(value == null): - return if_null - else: - return value - - -# ------------------------------------------------------------------------------ -# returns true if the object has been freed, false if not -# -# From what i've read, the weakref approach should work. It seems to work most -# of the time but sometimes it does not catch it. The str comparison seems to -# fill in the gaps. I've not seen any errors after adding that check. -# ------------------------------------------------------------------------------ -func is_freed(obj): - var wr = weakref(obj) - return !(wr.get_ref() and str(obj) != '[Deleted Object]') - - -# ------------------------------------------------------------------------------ -# Pretty self explanitory. -# ------------------------------------------------------------------------------ -func is_not_freed(obj): - return !is_freed(obj) - - -# ------------------------------------------------------------------------------ -# Checks if the passed in object is a GUT Double or Partial Double. -# ------------------------------------------------------------------------------ -func is_double(obj): - var to_return = false - if(typeof(obj) == TYPE_OBJECT and is_instance_valid(obj)): - to_return = obj.has_method('__gut_instance_from_id') - return to_return - - -# ------------------------------------------------------------------------------ -# Checks if the passed in is an instance of a class -# ------------------------------------------------------------------------------ -func is_instance(obj): - return typeof(obj) == TYPE_OBJECT and !obj.has_method('new') and !obj.has_method('instance') - - -# ------------------------------------------------------------------------------ -# Returns an array of values by calling get(property) on each element in source -# ------------------------------------------------------------------------------ -func extract_property_from_array(source, property): - var to_return = [] - for i in (source.size()): - to_return.append(source[i].get(property)) - return to_return - - -# ------------------------------------------------------------------------------ -# true if file exists, false if not. -# ------------------------------------------------------------------------------ -func file_exists(path): - return _file_checker.file_exists(path) - - -# ------------------------------------------------------------------------------ -# Write a file. -# ------------------------------------------------------------------------------ -func write_file(path, content): - var f = File.new() - var result = f.open(path, f.WRITE) - if(result == OK): - f.store_string(content) - f.close() - - -# ------------------------------------------------------------------------------ -# true if what is passed in is null or an empty string. -# ------------------------------------------------------------------------------ -func is_null_or_empty(text): - return text == null or text == '' - - -# ------------------------------------------------------------------------------ -# Get the name of a native class or null if the object passed in is not a -# native class. -# ------------------------------------------------------------------------------ -func get_native_class_name(thing): - var to_return = null - if(is_native_class(thing)): - var newone = thing.new() - to_return = newone.get_class() - newone.free() - return to_return - - -# ------------------------------------------------------------------------------ -# Checks an object to see if it is a GDScriptNativeClass -# ------------------------------------------------------------------------------ -func is_native_class(thing): - var it_is = false - if(typeof(thing) == TYPE_OBJECT): - it_is = str(thing).begins_with("[GDScriptNativeClass:") - return it_is - - -# ------------------------------------------------------------------------------ -# Returns the text of a file or an empty string if the file could not be opened. -# ------------------------------------------------------------------------------ -func get_file_as_text(path): - var to_return = '' - var f = File.new() - var result = f.open(path, f.READ) - if(result == OK): - to_return = f.get_as_text() - f.close() - return to_return - - -# ------------------------------------------------------------------------------ -# Loops through an array of things and calls a method or checks a property on -# each element until it finds the returned value. The item in the array is -# returned or null if it is not found. -# ------------------------------------------------------------------------------ -func search_array(ar, prop_method, value): - var found = false - var idx = 0 - - while(idx < ar.size() and !found): - var item = ar[idx] - if(item.get(prop_method) != null): - if(item.get(prop_method) == value): - found = true - elif(item.has_method(prop_method)): - if(item.call(prop_method) == value): - found = true - - if(!found): - idx += 1 - - if(found): - return ar[idx] - else: - return null - - -func are_datatypes_same(got, expected): - return !(typeof(got) != typeof(expected) and got != null and expected != null) diff --git a/addons/silicon.util.custom_docs/class_doc_generator.gd b/addons/silicon.util.custom_docs/class_doc_generator.gd deleted file mode 100644 index e703056..0000000 --- a/addons/silicon.util.custom_docs/class_doc_generator.gd +++ /dev/null @@ -1,342 +0,0 @@ -tool -extends Reference - - -var _pending_docs := {} -var _docs_queue := [] - -const _GD_TYPES = [ - "", "bool", "int", "float", - "String", "Vector2", "Rect2", "Vector3", - "Transform2D", "Plane", "Quat", "AABB", - "Basis", "Transform", "Color", "NodePath", - "RID", "Object", "Dictionary", "Array", - "PoolByteArray", "PoolIntArray", "PoolRealArray", "PoolStringArray", - "PoolVector2Array", "PoolVector3Array", "PoolColorArray" -] - -var plugin: EditorPlugin - - -func _update() -> void: - var time := OS.get_ticks_msec() - while not _docs_queue.empty() and OS.get_ticks_msec() - time < 5: - var name: String = _docs_queue.pop_front() - var doc: ClassDocItem = _pending_docs[name] - var should_first_gen := _generate(doc) - - if should_first_gen.empty(): - _pending_docs.erase(name) - else: - _docs_queue.push_back(name) - _docs_queue.erase(should_first_gen) - _docs_queue.push_front(should_first_gen) - - -func generate(name: String, base: String, script_path: String) -> ClassDocItem: - if name in _pending_docs: - return _pending_docs[name] - - var doc := ClassDocItem.new({ - name = name, - base = base, - path = script_path - }) - - _pending_docs[name] = doc - _docs_queue.append(name) - return doc - - -func _generate(doc: ClassDocItem) -> String: - var script: GDScript = load(doc.path) - var code_lines := script.source_code.split("\n") - - var inherits := doc.base - var parent_props := [] - var parent_methods := [] - var parent_constants := [] - while inherits != "" and inherits in plugin.class_docs: - if inherits in _pending_docs: - return inherits - - for prop in plugin.class_docs[inherits].properties: - parent_props.append(prop.name) - for method in plugin.class_docs[inherits].methods: - parent_methods.append(method.name) - for constant in plugin.class_docs[inherits].constants: - parent_constants.append(constant.name) - inherits = plugin.get_parent_class(inherits) - - for method in script.get_script_method_list(): - if method.name.begins_with("_") or method.name in parent_methods: - continue - doc.methods.append(_create_method_doc(method.name, script, method)) - - for property in script.get_script_property_list(): - if property.name.begins_with("_") or property.name in parent_props: - continue - doc.properties.append(_create_property_doc(property.name, script, property)) - - for _signal in script.get_script_signal_list(): - var signal_doc := SignalDocItem.new({ - "name": _signal.name - }) - doc.signals.append(signal_doc) - - for arg in _signal.args: - signal_doc.args.append(ArgumentDocItem.new({ - "name": arg.name, - "type": _type_string( - arg.type, - arg["class_name"] - ) if arg.type != TYPE_NIL else "Variant" - })) - - for constant in script.get_script_constant_map(): - if constant.begins_with("_") or constant in parent_constants: - continue - var value = script.get_script_constant_map()[constant] - - # Check if constant is an enumerator. - var is_enum := false - if typeof(value) == TYPE_DICTIONARY: - is_enum = true - for i in value.size(): - if typeof(value.keys()[i]) != TYPE_STRING or typeof(value.values()[i]) != TYPE_INT: - is_enum = false - break - - if is_enum: - for _enum in value: - doc.constants.append(ConstantDocItem.new({ - "name": _enum, - "value": value[_enum], - "enumeration": constant - })) - else: - doc.constants.append(ConstantDocItem.new({ - "name": constant, - "value": value - })) - - var comment_block := "" - var annotations := {} - var reading_block := false - var enum_block := false - for line in code_lines: - var indented: bool = line.begins_with(" ") or line.begins_with("\t") - if line.begins_with("##"): - reading_block = true - else: - reading_block = false - comment_block = comment_block.trim_suffix("\n") - - if line.begins_with("enum"): - enum_block = true - if line.find("}") != -1 and enum_block: - enum_block = false - - if line.find("##") != -1 and not reading_block: - var offset := 3 if line.find("## ") != -1 else 2 - comment_block = line.right(line.find("##") + offset) - - if reading_block: - if line.begins_with("## "): - line = line.trim_prefix("## ") - else: - line = line.trim_prefix("##") - if line.begins_with("@"): - var annote: Array = line.split(" ", true, 1) - if annote[0] == "@tutorial" and annote.size() == 2: - if annotations.has("@tutorial"): - annotations["@tutorial"].append(annote[1]) - annote[1] = annotations["@tutorial"] - else: - annote[1] = [annote[1]] - annotations[annote[0]] = null if annote.size() == 1 else annote[1] - else: - comment_block += line + "\n" - - elif not comment_block.empty(): - var doc_item: DocItem - - # Class document - if line.begins_with("extends") or line.begins_with("tool") or line.begins_with("class_name"): - if annotations.has("@doc-ignore"): - return "" - if annotations.has("@contribute"): - doc.contriute_url = annotations["@contribute"] - if annotations.has("@tutorial"): - doc.tutorials = annotations["@tutorial"] - var doc_split = comment_block.split("\n", true, 1) - doc.brief = doc_split[0] - if doc_split.size() == 2: - doc.description = doc_split[1] - doc_item = doc - - # Method document - elif line.find("func ") != -1 and not indented: - var regex := RegEx.new() - regex.compile("func ([a-zA-Z0-9_]+)") - var method := regex.search(line).get_string(1) - var method_doc := doc.get_method_doc(method) - - if not method_doc and method: - method_doc = _create_method_doc(method, script) - if method_doc: - doc.methods.append(method_doc) - - if method_doc: - if annotations.has("@args"): - var params = annotations["@args"].split(",") - for i in min(params.size(), method_doc.args.size()): - method_doc.args[i].name = params[i].strip_edges() - if annotations.has("@arg-defaults"): - var params = annotations["@arg-defaults"].split(",") - for i in min(params.size(), method_doc.args.size()): - method_doc.args[i].default = params[i].strip_edges().replace(";", ",") - if annotations.has("@arg-types"): - var params = annotations["@arg-types"].split(",") - for i in min(params.size(), method_doc.args.size()): - method_doc.args[i].type = params[i].strip_edges() - if annotations.has("@arg-enums"): - var params = annotations["@arg-enums"].split(",") - for i in min(params.size(), method_doc.args.size()): - method_doc.args[i].enumeration = params[i].strip_edges() - if annotations.has("@return"): - method_doc.return_type = annotations["@return"] - if annotations.has("@return-enum"): - method_doc.return_enum = annotations["@return-enum"] - method_doc.is_virtual = annotations.has("@virtual") - method_doc.description = comment_block - doc_item = method_doc - - # Property document - elif line.find("var ") != -1 and not indented: - var regex := RegEx.new() - regex.compile("var ([a-zA-Z0-9_]+)") - var prop := regex.search(line).get_string(1) - var prop_doc := doc.get_property_doc(prop) - if not prop_doc and prop: - prop_doc = _create_property_doc(prop, script) - if prop_doc: - doc.properties.append(prop_doc) - - if prop_doc: - if annotations.has("@type"): - prop_doc.type = annotations["@type"] - if annotations.has("@default"): - prop_doc.default = annotations["@default"] - if annotations.has("@enum"): - prop_doc.enumeration = annotations["@enum"] - if annotations.has("@setter"): - prop_doc.setter = annotations["@setter"] - if annotations.has("@getter"): - prop_doc.getter = annotations["@getter"] - prop_doc.description = comment_block - doc_item = prop_doc - - # Signal document - elif line.find("signal") != -1 and not indented: - var regex := RegEx.new() - regex.compile("signal ([a-zA-Z0-9_]+)") - var signl := regex.search(line).get_string(1) - var signal_doc := doc.get_signal_doc(signl) - if signal_doc: - if annotations.has("@arg-types"): - var params = annotations["@arg-types"].split(",") - for i in min(params.size(), signal_doc.args.size()): - signal_doc.args[i].type = params[i].strip_edges() - if annotations.has("@arg-enums"): - var params = annotations["@arg-enums"].split(",") - for i in min(params.size(), signal_doc.args.size()): - signal_doc.args[i].enumeration = params[i].strip_edges() - signal_doc.description = comment_block - doc_item = signal_doc - - # Constant document - elif line.find("const") != -1 and not indented: - var regex := RegEx.new() - regex.compile("const ([a-zA-Z0-9_]+)") - var constant := regex.search(line).get_string(1) - var const_doc := doc.get_constant_doc(constant) - if const_doc: - const_doc.description = comment_block - doc_item = const_doc - - # Enumerator document - elif enum_block: # Handle enumerators - for enum_doc in doc.constants: - if line.find(enum_doc.name) != -1: - enum_doc.description = comment_block - doc_item = enum_doc - break - - # Meta annotations - if doc_item: - for annote in annotations: - if annote.find("@meta-") == 0: - var key: String = annote.right("@meta-".length()) - doc_item.meta[key] = annotations[annote].strip_edges() - - comment_block = "" - annotations.clear() - return "" - - -func _create_method_doc(name: String, script: Script = null, method := {}) -> MethodDocItem: - if method.empty(): - var methods := script.get_script_method_list() - for m in methods: - if m.name == name: - method = m - break - - if not method.has("name"): - return null - - var method_doc := MethodDocItem.new({ - "name": method.name, - "return_type": _type_string( - method["return"]["type"], - method["return"]["class_name"] - ) if method["return"]["type"] != TYPE_NIL else "void", - }) - for arg in method.args: - method_doc.args.append(ArgumentDocItem.new({ - "name": arg.name, - "type": _type_string( - arg.type, - arg["class_name"] - ) if arg.type != TYPE_NIL else "Variant" - })) - return method_doc - - -func _create_property_doc(name: String, script: Script = null, property := {}) -> PropertyDocItem: - if property.empty(): - var properties := script.get_script_property_list() - for p in properties: - if p.name == name: - property = p - break - - if not property.has("name"): - return null - - var property_doc := PropertyDocItem.new({ - "name": name, - "type": _type_string( - property.type, - property["class_name"] - ) if property.type != TYPE_NIL else "Variant" - }) - return property_doc - - -func _type_string(type: int, _class_name: String) -> String: - if type == TYPE_OBJECT: - return _class_name - else: - return _GD_TYPES[type] diff --git a/addons/silicon.util.custom_docs/doc_exporter/doc_exporter.gd b/addons/silicon.util.custom_docs/doc_exporter/doc_exporter.gd deleted file mode 100644 index 6591e71..0000000 --- a/addons/silicon.util.custom_docs/doc_exporter/doc_exporter.gd +++ /dev/null @@ -1,13 +0,0 @@ -## The base class for every document exporter. -## @contribute https://placeholder_contribute.com -tool -class_name DocExporter -extends Reference - - -## @virtual -## @args doc -## @arg-types ClassDocItem -## This function gets called to generate a document string from a [ClassDocItem]. -func _generate(doc: ClassDocItem) -> String: - return "" diff --git a/addons/silicon.util.custom_docs/doc_exporter/editor_help_doc_exporter.gd b/addons/silicon.util.custom_docs/doc_exporter/editor_help_doc_exporter.gd deleted file mode 100644 index 260f87c..0000000 --- a/addons/silicon.util.custom_docs/doc_exporter/editor_help_doc_exporter.gd +++ /dev/null @@ -1,1046 +0,0 @@ -tool -extends DocExporter - -var plugin: EditorPlugin - -var label: RichTextLabel -var class_docs: Dictionary - -var editor_settings: EditorSettings -var theme: Theme -var class_list := Array(ClassDB.get_class_list()) + [ - "Variant", "bool", "int", "float", - "String", "Vector2", "Rect2", "Vector3", - "Transform2D", "Plane", "Quat", "AABB", - "Basis", "Transform", "Color", "NodePath", - "RID", "Object", "Dictionary", "Array", - "PoolByteArray", "PoolIntArray", "PoolRealArray", - "PoolStringArray", "PoolVector2Array", - "PoolVector3Array", "PoolColorArray" -] - -var section_lines := [] -var description_line := 0 -var signal_line := {} -var method_line := {} -var property_line := {} -var enum_line := {} -var constant_line := {} - - -var doc_font: Font -var doc_bold_font: Font -var doc_title_font: Font -var doc_code_font: Font - -var title_color: Color -var text_color: Color -var headline_color: Color -var base_type_color: Color -var comment_color: Color -var symbol_color: Color -var value_color: Color -var qualifier_color: Color -var type_color: Color - - -func _generate(doc: ClassDocItem) -> String: - var is_current: bool = label.is_visible_in_tree() - var link_color_text := title_color.to_html() - section_lines.clear() - if is_current: - signal_line.clear() - method_line.clear() - property_line.clear() - enum_line.clear() - constant_line.clear() - - label.visible = true - label.clear() - - # Class Name - if is_current: - section_lines.append(["Top", 0]) - label.push_font(doc_title_font) - label.push_color(title_color) - label.add_text("Class: ") - label.push_color(headline_color) - add_text(doc.name) - label.pop() - label.pop() - label.pop() - label.add_text("\n") - - # Ascendance - if doc.base != "": - label.push_color(title_color) - label.push_font(doc_font) - label.add_text("Inherits: ") - label.pop() - - var inherits = doc.base - - while inherits != "": - add_type(inherits, "") - inherits = plugin.get_parent_class(inherits) - - if inherits != "": - label.add_text(" < ") - - label.pop() - label.add_text("\n") - - # Descendents - var found := false - var prev := false - for name in class_docs: - if class_docs[name].base == doc.name: - if not found: - label.push_color(title_color) - label.push_font(doc_font) - label.add_text("Inherited by: ") - label.pop() - found = true - - if prev: - label.add_text(" , ") - - add_type(name, "") - prev = true - if found: - label.pop() - label.add_text("\n") - - label.add_text("\n") - label.add_text("\n") - - # Brief description - if doc.brief != "": - label.push_color(text_color) - label.push_font(doc_bold_font) - label.push_indent(1) - add_text(doc.brief) - label.pop() - label.pop() - label.pop() - label.add_text("\n") - label.add_text("\n") - label.add_text("\n") - - if doc.description != "": - if is_current: - section_lines.append(["Description", label.get_line_count() - 2]) - description_line = label.get_line_count() - 2 - label.push_color(title_color) - label.push_font(doc_title_font) - label.add_text("Description") - label.pop() - label.pop() - - label.add_text("\n") - label.add_text("\n") - label.push_color(text_color) - label.push_font(doc_font) - label.push_indent(1) - add_text(doc.description) - label.pop() - label.pop() - label.pop() - label.add_text("\n") - label.add_text("\n") - label.add_text("\n") - - # Online tutorials - if doc.tutorials.size(): - label.push_color(title_color) - label.push_font(doc_title_font) - label.add_text("Online Tutorials") - label.pop() - label.pop() - - label.push_indent(1) - label.push_font(doc_code_font) - label.add_text("\n") - - for tutorial in doc.tutorials: - var link: String = tutorial - var linktxt: String = tutorial - var seppos := link.find("//") - if seppos != -1: - link = link.right(seppos + 2) - - label.push_color(symbol_color) - label.append_bbcode("[url=" + linktxt + "]" + link + "[/url]") - label.pop() - label.add_text("\n") - - label.pop() - label.pop() - label.add_text("\n") - label.add_text("\n") - - # Properties overview - var skip_methods := [] - var property_descr := false - - if doc.properties.size(): - if is_current: - section_lines.append(["Properties", label.get_line_count() - 2]) - label.push_color(title_color) - label.push_font(doc_title_font) - label.add_text("Properties") - label.pop() - label.pop() - - label.add_text("\n") - label.push_font(doc_code_font) - label.push_indent(1) - label.push_table(2) - label.set_table_column_expand(1, true, 1) - - for property in doc.properties: - property_line[property.name] = label.get_line_count() - 2 #gets overridden if description - label.push_cell() - label.push_align(RichTextLabel.ALIGN_RIGHT) - label.push_font(doc_code_font) - add_type(property.type, property.enumeration) - label.pop() - label.pop() - label.pop() - - var describe := false - - if property.setter != "": - skip_methods.append(property.setter) - describe = true - if property.getter != "": - skip_methods.append(property.getter) - describe = true - - if property.description != "": - describe = true - - label.push_cell() - label.push_font(doc_code_font) - label.push_color(headline_color) - - if describe: - label.push_meta("@member " + property.name) - - add_text(property.name) - - if describe: - label.pop() - property_descr = true - - if property.default != "": - label.push_color(symbol_color) - label.add_text(" [default: ") - label.pop() - label.push_color(value_color) - add_text(property.default) - label.pop() - label.push_color(symbol_color) - label.add_text("]") - label.pop() - - label.pop() - label.pop() - label.pop() - - label.pop() #table - label.pop() - label.pop() # font - label.add_text("\n") - label.add_text("\n") - - # Methods overview - var method_descr := false - var sort_methods: bool = editor_settings.get("text_editor/help/sort_functions_alphabetically") - var methods := [] - - for method in doc.methods: - if skip_methods.has(method.name): - if method.args.size() == 0 or (method.args.size() == 1 and method.return_type == "void"): - continue - methods.append(method) - - if methods.size(): - if sort_methods: - methods.sort_custom(self, "sort_methods") - if is_current: - section_lines.append(["Methods", label.get_line_count() - 2]) - label.push_color(title_color) - label.push_font(doc_title_font) - label.add_text("Methods") - label.pop() - label.pop() - - label.add_text("\n") - label.push_font(doc_code_font) - label.push_indent(1) - label.push_table(2) - label.set_table_column_expand(1, true, 1) - - var any_previous := false - for _pass in 2: - var m := [] - for method in methods: - if (_pass == 0 and method.is_virtual) or (_pass == 1 and not method.is_virtual): - m.append(method) - - if any_previous and not m.empty(): - label.push_cell() - label.pop() #cell - label.push_cell() - label.pop() #cell - - var group_prefix := "" - for i in m.size(): - var new_prefix: String = m[i].name.substr(0, 3) - var is_new_group := false - - if i < m.size() - 1 and new_prefix == m[i + 1].name.substr(0, 3) and new_prefix != group_prefix: - is_new_group = i > 0 - group_prefix = new_prefix - elif group_prefix != "" and new_prefix != group_prefix: - is_new_group = true - group_prefix = "" - - if is_new_group and _pass == 1: - label.push_cell() - label.pop() #cell - label.push_cell() - label.pop() #cell - - if m[i].description != "": - method_descr = true - - add_method(m[i], true) - - any_previous = !m.empty() - - label.pop() #table - label.pop() - label.pop() # font - label.add_text("\n") - label.add_text("\n") - - # Theme properties -# if doc.theme_properties.size(): -# -# section_line.append(Pair(TTR("Theme Properties"), label.get_line_count() - 2)) -# label.push_color(title_color) -# label.push_font(doc_title_font) -# label.add_text(TTR("Theme Properties")) -# label.pop() -# label.pop() -# -# label.push_indent(1) -# label.push_table(2) -# label.set_table_column_expand(1, 1) -# -# for int i = 0 i < doc.theme_properties.size() i++: -# -# theme_property_line[doc.theme_properties[i].name] = label.get_line_count() - 2 #gets overridden if description -# -# label.push_cell() -# label.push_align(RichTextLabel.ALIGN_RIGHT) -# label.push_font(doc_code_font) -# //add_type(doc.theme_properties[i].type) -# label.pop() -# label.pop() -# label.pop() -# -# label.push_cell() -# label.push_font(doc_code_font) -# label.push_color(headline_color) -# //add_text(doc.theme_properties[i].name) -# label.pop() -# -# if doc.theme_properties[i].default != "": -# label.push_color(symbol_color) -# label.add_text(" [" + TTR("default:") + " ") -# label.pop() -# label.push_color(value_color) -# //add_text(_fix_constant(doc.theme_properties[i].default)) -# label.pop() -# label.push_color(symbol_color) -# label.add_text("]") -# label.pop() -# } -# -# label.pop() -# -# if doc.theme_properties[i].description != "": -# label.push_font(doc_font) -# label.add_text(" ") -# label.push_color(comment_color) -# //add_text(doc.theme_properties[i].description) -# label.pop() -# label.pop() -# } -# label.pop() # cell -# } -# -# label.pop() # table -# label.pop() -# label.add_text("\n") -# label.add_text("\n") -# } -# - # Signals - var signals := doc.signals.duplicate() - if signals.size(): - if sort_methods: - signals.sort() - if is_current: - section_lines.append(["Signals", label.get_line_count() - 2]) - - label.push_color(title_color) - label.push_font(doc_title_font) - label.add_text("Signals") - label.pop() - label.pop() - - label.add_text("\n") - label.add_text("\n") - - label.push_indent(1) - - for _signal in signals: - signal_line[_signal.name] = label.get_line_count() - 2 #gets overridden if description - label.push_font(doc_code_font) # monofont - label.push_color(headline_color) - add_text(_signal.name) - label.pop() - label.push_color(symbol_color) - label.add_text("(") - label.pop() - for j in _signal.args.size(): - label.push_color(text_color) - if j > 0: - label.add_text(", ") - - add_text(_signal.args[j].name) - label.add_text(": ") - add_type(_signal.args[j].type, _signal.args[j].enumeration) - if _signal.args[j].default != "": - label.push_color(symbol_color) - label.add_text(" = ") - label.pop() - add_text(_signal.args[j].default) - - label.pop() - - label.push_color(symbol_color) - label.add_text(")") - label.pop() - label.pop() # end monofont - if _signal.description != "": - label.push_font(doc_font) - label.push_color(comment_color) - label.push_indent(1) - add_text(_signal.description) - label.pop() # indent - label.pop() - label.pop() # font - label.add_text("\n") - label.add_text("\n") - - label.pop() - label.add_text("\n") - - # Constants and enums - if doc.constants.size(): - var enums := {} - var constants := [] - for i in doc.constants.size(): - if doc.constants[i].enumeration != "": - if not enums.has(doc.constants[i].enumeration): - enums[doc.constants[i].enumeration] = [] - enums[doc.constants[i].enumeration].append(doc.constants[i]) - else: - constants.append(doc.constants[i]) - - # Enums - if enums.size(): - section_lines.append(["Enumerations", label.get_line_count() - 2]) - label.push_color(title_color) - label.push_font(doc_title_font) - label.add_text("Enumerations") - label.pop() - label.pop() - label.push_indent(1) - - label.add_text("\n") - - for e in enums: - enum_line[e] = label.get_line_count() - 2 - - label.push_color(title_color) - label.add_text("enum ") - label.pop() - label.push_font(doc_code_font) - if (e.split(".").size() > 1) and (e.split(".")[0] == doc.name): - e = e.split(".")[1] - - label.push_color(headline_color) - label.add_text(e) - label.pop() - label.pop() - label.push_color(symbol_color) - label.add_text(":") - label.pop() - label.add_text("\n") - - label.push_indent(1) - var enum_list: Array = enums[e] - - for _enum in enum_list: - # Add the enum constant line to the constant_line map so we can locate it as a constant - constant_line[_enum.name] = label.get_line_count() - 2 - - label.push_font(doc_code_font) - label.push_color(headline_color) - add_text(_enum.name) - label.pop() - label.push_color(symbol_color) - label.add_text(" = ") - label.pop() - label.push_color(value_color) - add_text(_enum.value) - label.pop() - label.pop() - if _enum.description != "": - label.push_font(doc_font) - #label.add_text(" ") - label.push_indent(1) - label.push_color(comment_color) - add_text(_enum.description) - label.pop() - label.pop() - label.pop() # indent - label.add_text("\n") - label.add_text("\n") - - label.pop() - label.add_text("\n") - - label.pop() - label.add_text("\n") - - # Constants - if constants.size(): - section_lines.append(["Constants", label.get_line_count() - 2]) - label.push_color(title_color) - label.push_font(doc_title_font) - label.add_text("Constants") - label.pop() - label.pop() - label.push_indent(1) - - label.add_text("\n") - - for i in constants.size(): -# constant_line[constants[i].name] = label.get_line_count() - 2 - label.push_font(doc_code_font) - - if constants[i].value.begins_with("Color(") and constants[i].value.ends_with(")"): - var stripped: String = constants[i].value.replace(" ", "").replace("Color(", "").replace(")", "") - var color := stripped.split_floats(",") - if color.size() >= 3: - label.push_color(Color(color[0], color[1], color[2])) - var prefix := [0x25CF, ' ', 0] - label.add_text(String(prefix)) - label.pop() - - label.push_color(headline_color) - add_text(constants[i].name) - label.pop() - label.push_color(symbol_color) - label.add_text(" = ") - label.pop() - label.push_color(value_color) - add_text(constants[i].value) - label.pop() - - label.pop() - if constants[i].description != "": - label.push_font(doc_font) - label.push_indent(1) - label.push_color(comment_color) - add_text(constants[i].description) - label.pop() - label.pop() - label.pop() # indent - label.add_text("\n") - - label.add_text("\n") - - label.pop() - label.add_text("\n") - - # Property descriptions - if property_descr: - section_lines.append(["Property Descriptions", label.get_line_count() - 2]) - label.push_color(title_color) - label.push_font(doc_title_font) - label.add_text("Property Descriptions") - label.pop() - label.pop() - - label.add_text("\n") - label.add_text("\n") - - for prop in doc.properties: -# if doc.properties[i].overridden: -# continue - - property_line[prop.name] = label.get_line_count() - 2 - label.push_table(2) - label.set_table_column_expand(1, true, 1) - - label.push_cell() - label.push_font(doc_code_font) - add_type(prop.type, prop.enumeration) - label.add_text(" ") - label.pop() # font - label.pop() # cell - - label.push_cell() - label.push_font(doc_code_font) - label.push_color(headline_color) - add_text(prop.name) - label.pop() # color - - if prop.default != "": - label.push_color(symbol_color) - label.add_text(" [default: ") - label.pop() # color - - label.push_color(value_color) - add_text(prop.default) - label.pop() # color - - label.push_color(symbol_color) - label.add_text("]") - label.pop() # color - - label.pop() # font - label.pop() # cell - - var method_map := {} - for method in methods: - method_map[method.name] = method - - if prop.setter != "": - label.push_cell() - label.pop() # cell - - label.push_cell() - label.push_font(doc_code_font) - label.push_color(text_color) - if method_map.has(prop.setter) and method_map[prop.setter].args.size() > 1: - # Setters with additional args are exposed in the method list, so we link them here for quick access. - label.push_meta("@method " + prop.setter) - label.add_text(prop.setter + "(value)") - label.pop() - else: - label.add_text(prop.setter + "(value)") - label.pop() # color - label.push_color(comment_color) - label.add_text(" setter") - label.pop() # color - label.pop() # font - label.pop() # cell - method_line[prop.setter] = property_line[prop.name] - - if prop.getter != "": - label.push_cell() - label.pop() # cell - - label.push_cell() - label.push_font(doc_code_font) - label.push_color(text_color) - if method_map.has(prop.getter) and method_map[prop.getter].args.size() > 0: - # Getters with additional args are exposed in the method list, so we link them here for quick access. - label.push_meta("@method " + prop.getter) - label.add_text(prop.getter + "()") - label.pop() - else: - label.add_text(prop.getter + "()") - label.pop() #color - label.push_color(comment_color) - label.add_text(" getter") - label.pop() #color - label.pop() #font - label.pop() #cell - method_line[prop.getter] = property_line[prop.name] - - label.pop() # table - - label.add_text("\n") - label.add_text("\n") - - label.push_color(text_color) - label.push_font(doc_font) - label.push_indent(1) - if prop.description.strip_edges() != "": - add_text(prop.description) - else: - label.add_image(theme.get_icon("Error", "EditorIcons")) - label.add_text(" ") - label.push_color(comment_color) - label.append_bbcode("There is currently no description for this property. Please help us by [color=$color][url=$url]contributing one[/url][/color]!".replace("$url", doc.contriute_url).replace("$color", link_color_text)) - label.pop() - label.pop() - label.pop() - label.pop() - label.add_text("\n") - label.add_text("\n") - label.add_text("\n") - - # Method descriptions - if method_descr: -# section_line.append(Pair(TTR("Method Descriptions"), label.get_line_count() - 2)) - label.push_color(title_color) - label.push_font(doc_title_font) - label.add_text("Method Descriptions") - label.pop() - label.pop() - label.add_text("\n") - label.add_text("\n") - - for _pass in 2: - var methods_filtered := [] - for method in methods: - if (_pass == 0 and method.is_virtual) or (_pass == 1 and not method.is_virtual): - methods_filtered.append(method) - - for i in methods_filtered.size(): - label.push_font(doc_code_font) - add_method(methods_filtered[i], false) - label.pop() - - label.add_text("\n") - label.add_text("\n") - - label.push_color(text_color) - label.push_font(doc_font) - label.push_indent(1) - if methods_filtered[i].description.strip_edges() != "": - add_text(methods_filtered[i].description) - else: - label.add_image(theme.get_icon("Error", "EditorIcons")) - label.add_text(" ") - label.push_color(comment_color) - label.append_bbcode("There is currently no description for this method. Please help us by [color=$color][url=$url]contributing one[/url][/color]!".replace("$url", doc.contriute_url).replace("$color", link_color_text)) - label.pop() - - label.pop() - label.pop() - label.pop() - label.add_text("\n") - label.add_text("\n") - label.add_text("\n") - - return str(is_current) - - -func update_theme_vars() -> void: - doc_font = theme.get_font("doc", "EditorFonts") - doc_bold_font = theme.get_font("doc_bold", "EditorFonts") - doc_title_font = theme.get_font("doc_title", "EditorFonts") - doc_code_font = theme.get_font("doc_source", "EditorFonts") - - title_color = theme.get_color("accent_color", "Editor") - text_color = theme.get_color("default_color", "RichTextLabel") - headline_color = theme.get_color("headline_color", "EditorHelp") - base_type_color = title_color.linear_interpolate(text_color, 0.5) - comment_color = text_color * Color(1, 1, 1, 0.6) - symbol_color = comment_color - value_color = text_color * Color(1, 1, 1, 0.6) - qualifier_color = text_color * Color(1, 1, 1, 0.8) - type_color = theme.get_color("accent_color", "Editor").linear_interpolate(text_color, 0.5) - - -func add_type(type: String, _enum: String): - var t := type - if t.empty(): - t = "void" - var can_ref := (t != "void") or not _enum.empty() - - if not _enum.empty(): - if _enum.split(".").size() > 1: - t = _enum.split(".")[1] - else: - t = _enum.split(".")[0] - - var text_color := label.get_color("default_color", "RichTextLabel") - var type_color := label.get_color("accent_color", "Editor").linear_interpolate(text_color, 0.5) - label.push_color(type_color) - if can_ref: - if _enum.empty(): - label.push_meta("#" + t) #class - else: - label.push_meta("$" + _enum) #class - label.add_text(t) - if can_ref: - label.pop() - label.pop() - - -func add_method(method: MethodDocItem, overview: bool) -> void: - method_line[method.name] = label.get_line_count() - 2 # gets overridden if description - if overview: - label.push_cell() - label.push_align(RichTextLabel.ALIGN_RIGHT) - - add_type(method.return_type, method.return_enum) - - if overview: - label.pop() #align - label.pop() #cell - label.push_cell() - else: - label.add_text(" ") - - if overview and method.description != "": - label.push_meta("@method " + method.name) - - label.push_color(headline_color) - add_text(method.name) - label.pop() - - if overview and method.description != "": - label.pop() #meta - - label.push_color(symbol_color) - label.add_text("(") - label.pop() - - for j in method.args.size(): - label.push_color(text_color) - if j > 0: - label.add_text(", ") - - add_text(method.args[j].name) - label.add_text(": ") - add_type(method.args[j].type, method.args[j].enumeration) - if method.args[j].default != "": - label.push_color(symbol_color) - label.add_text(" = ") - label.pop() - label.push_color(value_color) - add_text(method.args[j].default) - label.pop() - label.pop() - - label.push_color(symbol_color) - label.add_text(")") - label.pop() - if method.is_virtual: - label.push_color(qualifier_color) - label.add_text(" ") - add_text("virtual") - label.pop() - - if overview: - label.pop() #cell - - -func add_text(bbcode: String) -> void: - var base_path: String - - var doc_font := label.get_font("doc", "EditorFonts") - var doc_bold_font := label.get_font("doc_bold", "EditorFonts") - var doc_code_font := label.get_font("doc_source", "EditorFonts") - var doc_kbd_font := label.get_font("doc_keyboard", "EditorFonts") - - var headline_color := label.get_color("headline_color", "EditorHelp") - var accent_color := label.get_color("accent_color", "Editor") - var property_color := label.get_color("property_color", "Editor") - var link_color := accent_color.linear_interpolate(headline_color, 0.8) - var code_color := accent_color.linear_interpolate(headline_color, 0.6) - var kbd_color := accent_color.linear_interpolate(property_color, 0.6) - - bbcode = bbcode.dedent().replace("\t", "").replace("\r", "").strip_edges() - - bbcode = bbcode.replace("[csharp]", "[b]C#:[/b]\n[codeblock]") - bbcode = bbcode.replace("[gdscript]", "[b]GDScript:[/b]\n[codeblock]") - bbcode = bbcode.replace("[/csharp]", "[/codeblock]") - bbcode = bbcode.replace("[/gdscript]", "[/codeblock]") - - # Remove codeblocks (they would be printed otherwise) - bbcode = bbcode.replace("[codeblocks]\n", "") - bbcode = bbcode.replace("\n[/codeblocks]", "") - bbcode = bbcode.replace("[codeblocks]", "") - bbcode = bbcode.replace("[/codeblocks]", "") - - # remove extra new lines around code blocks - bbcode = bbcode.replace("[codeblock]\n", "[codeblock]") - bbcode = bbcode.replace("\n[/codeblock]", "[/codeblock]") - - var tag_stack := [] - var code_tag := false - - var pos := 0 - while pos < bbcode.length(): - var brk_pos := bbcode.find("[", pos) - if brk_pos < 0: - brk_pos = bbcode.length() - - if brk_pos > pos: - var text := bbcode.substr(pos, brk_pos - pos) -# if not code_tag: -# text = text.replace("\n", "\n\n") - label.add_text(text) - - if brk_pos == bbcode.length(): - break #nothing else to add - - var brk_end := bbcode.find("]", brk_pos + 1) - - if brk_end == -1: - var text := bbcode.substr(brk_pos, bbcode.length() - brk_pos) - if not code_tag: - text = text.replace("\n", "\n\n") - label.add_text(text) - break - - var tag := bbcode.substr(brk_pos + 1, brk_end - brk_pos - 1) - - if tag.begins_with("/"): - var tag_ok = tag_stack.size() and tag_stack[0] == tag.substr(1, tag.length()) - if not tag_ok: - label.add_text("[") - pos = brk_pos + 1 - continue - - tag_stack.pop_front() - pos = brk_end + 1 - if tag != "/img": - label.pop() - if code_tag: - label.pop() - code_tag = false - - elif code_tag: - label.add_text("[") - pos = brk_pos + 1 - - elif tag.begins_with("method ") || tag.begins_with("member ") || tag.begins_with("signal ") || tag.begins_with("enum ") || tag.begins_with("constant "): - var tag_end := tag.find(" ") - var link_tag := tag.substr(0, tag_end) - var link_target := tag.substr(tag_end + 1, tag.length()).lstrip(" ") - - label.push_color(link_color) - label.push_meta("@" + link_tag + " " + link_target) - label.add_text(link_target + ("()" if tag.begins_with("method ") else "")) - label.pop() - label.pop() - pos = brk_end + 1 - - elif class_list.has(tag): - label.push_color(link_color) - label.push_meta("#" + tag) - label.add_text(tag) - label.pop() - label.pop() - pos = brk_end + 1 - - elif tag == "b": - #use bold font - label.push_font(doc_bold_font) - pos = brk_end + 1 - tag_stack.push_front(tag) - elif tag == "i": - #use italics font - label.push_color(headline_color) - pos = brk_end + 1 - tag_stack.push_front(tag) - elif tag == "code" || tag == "codeblock": - #use monospace font - label.push_font(doc_code_font) - label.push_color(code_color) - code_tag = true - pos = brk_end + 1 - tag_stack.push_front(tag) - elif tag == "kbd": - #use keyboard font with custom color - label.push_font(doc_kbd_font) - label.push_color(kbd_color) - code_tag = true # though not strictly a code tag, logic is similar - pos = brk_end + 1 - tag_stack.push_front(tag) - elif tag == "center": - #align to center - label.push_paragraph(RichTextLabel.ALIGN_CENTER, Control.TEXT_DIRECTION_AUTO, "") - pos = brk_end + 1 - tag_stack.push_front(tag) - elif tag == "br": - #force a line break - label.add_newline() - pos = brk_end + 1 - elif tag == "u": - #use underline - label.push_underline() - pos = brk_end + 1 - tag_stack.push_front(tag) - elif tag == "s": - #use strikethrough - label.push_strikethrough() - pos = brk_end + 1 - tag_stack.push_front(tag) - elif tag == "url": - var end := bbcode.find("[", brk_end) - if end == -1: - end = bbcode.length() - var url = bbcode.substr(brk_end + 1, end - brk_end - 1) - label.push_meta(url) - - pos = brk_end + 1 - tag_stack.push_front(tag) - elif tag.begins_with("url="): - var url := tag.substr(4, tag.length()) - label.push_meta(url) - pos = brk_end + 1 - tag_stack.push_front("url") - elif tag == "img": - var end := bbcode.find("[", brk_end) - if end == -1: - end = bbcode.length() - var image := bbcode.substr(brk_end + 1, end - brk_end - 1) - var texture := load(base_path.plus_file(image)) as Texture - if texture: - label.add_image(texture) - - pos = end - tag_stack.push_front(tag) - elif tag.begins_with("color="): - var col := tag.substr(6, tag.length()) - var color := Color(col) - label.push_color(color) - pos = brk_end + 1 - tag_stack.push_front("color") - - elif tag.begins_with("font="): - var fnt := tag.substr(5, tag.length()) - var font := load(base_path.plus_file(fnt)) as Font - if font.is_valid(): - label.push_font(font) - else: - label.push_font(doc_font) - - pos = brk_end + 1 - tag_stack.push_front("font") - - else: - label.add_text("[") #ignore - pos = brk_pos + 1 - - -func sort_methods(a: Dictionary, b: Dictionary) -> bool: - return a.name < b.name - diff --git a/addons/silicon.util.custom_docs/doc_item/argument_doc_item.gd b/addons/silicon.util.custom_docs/doc_item/argument_doc_item.gd deleted file mode 100644 index 5107bf1..0000000 --- a/addons/silicon.util.custom_docs/doc_item/argument_doc_item.gd +++ /dev/null @@ -1,27 +0,0 @@ -## An object that contains documentation data about an argument of a signal or method. -## @contribute https://placeholder_contribute.com -tool -class_name ArgumentDocItem -extends DocItem - - -## @default "" -## The default value of the argument. -var default := "" - -## @default "" -## The enumeration of [member default]. -var enumeration := "" - -## @default "" -## The class/built-in type of [member default]. -var type := "" - - -func _init(args := {}) -> void: - for arg in args: - set(arg, args[arg]) - - -func _to_string() -> String: - return "[Argument doc: " + name + "]" diff --git a/addons/silicon.util.custom_docs/doc_item/class_doc_item.gd b/addons/silicon.util.custom_docs/doc_item/class_doc_item.gd deleted file mode 100644 index 6293708..0000000 --- a/addons/silicon.util.custom_docs/doc_item/class_doc_item.gd +++ /dev/null @@ -1,77 +0,0 @@ -## An object that contains documentation data about a class. -## @contribute https://placeholder_contribute.com -tool -class_name ClassDocItem -extends DocItem - - -var base := "" ## The base class this class extends from. -var path := "" ## The file location of this class' script. - -var brief := "" ## A brief description of the class. -var description := "" ## A full description of the class. - -var methods := [] ## A list of method documents. -var properties := [] ## A list of property documents. -var signals := [] ## A list of signal documents. -var constants := [] ## A list of constant documents, including enumerators. - -var tutorials := [] ## A list of tutorials that helps to understand this class. - -## @default "" -## A link to where the user can contribute to the class' documentation. -var contriute_url := "" - -## @default false -## Whether the class is a singleton. -var is_singleton := false -var icon := "" ## A path to the class icon if any. - - -func _init(args := {}) -> void: - for arg in args: - set(arg, args[arg]) - - -## @args name -## @return MethodDocItem -## Gets a method document called [code]name[/code]. -func get_method_doc(name: String) -> MethodDocItem: - for doc in methods: - if doc.name == name: - return doc - return null - - -## @args name -## @return PropertyDocItem -## Gets a signal document called [code]name[/code]. -func get_property_doc(name: String) -> PropertyDocItem: - for doc in properties: - if doc.name == name: - return doc - return null - - -## @args name -## @return SignalDocItem -## Gets a signal document called [code]name[/code]. -func get_signal_doc(name: String) -> SignalDocItem: - for doc in signals: - if doc.name == name: - return doc - return null - - -## @args name -## @return ConstantlDocItem -## Gets a signal document called [code]name[/code]. -func get_constant_doc(name: String) -> ConstantDocItem: - for doc in constants: - if doc.name == name: - return doc - return null - - -func _to_string() -> String: - return "[Class doc: " + name + "]" diff --git a/addons/silicon.util.custom_docs/doc_item/constant_doc_item.gd b/addons/silicon.util.custom_docs/doc_item/constant_doc_item.gd deleted file mode 100644 index 5ff678e..0000000 --- a/addons/silicon.util.custom_docs/doc_item/constant_doc_item.gd +++ /dev/null @@ -1,27 +0,0 @@ -## An object that contains documentation data about a constant. -## @contribute https://placeholder_contribute.com -tool -class_name ConstantDocItem -extends DocItem - - -## @default "" -## A description of the constant. -var description := "" - -## @default "" -## The value of the constant in a string form. -var value := "" - -## @default "" -## The [member value]'s enumeration. -var enumeration := "" - - -func _init(args := {}) -> void: - for arg in args: - set(arg, args[arg]) - - -func _to_string() -> String: - return "[Constant doc: " + name + "]" diff --git a/addons/silicon.util.custom_docs/doc_item/doc_item.gd b/addons/silicon.util.custom_docs/doc_item/doc_item.gd deleted file mode 100644 index c1c035a..0000000 --- a/addons/silicon.util.custom_docs/doc_item/doc_item.gd +++ /dev/null @@ -1,13 +0,0 @@ -## The base class for all documentation items. -tool -class_name DocItem -extends Reference - - -## @default "" -## The name of the documentation item. -var name := "" - -## @default {} -## The item's metadata. -var meta := {} diff --git a/addons/silicon.util.custom_docs/doc_item/method_doc_item.gd b/addons/silicon.util.custom_docs/doc_item/method_doc_item.gd deleted file mode 100644 index 756ac4b..0000000 --- a/addons/silicon.util.custom_docs/doc_item/method_doc_item.gd +++ /dev/null @@ -1,33 +0,0 @@ -tool -class_name MethodDocItem -extends DocItem - - -## @default "" -## A description of the method. -var description := "" - -## @default "" -## The return type of the method. -var return_type := "" - -## @default "" -## The enumerator of [member return_type]. -var return_enum := "" - -## @default [] -## A list of arguments the method takes in. -var args := [] - -## @default false -## Whether the method is to be overriden in an extended class, similar to [Node._ready]. -var is_virtual := false - - -func _init(args := {}) -> void: - for arg in args: - set(arg, args[arg]) - - -func _to_string() -> String: - return "[Method doc: " + name + "]" diff --git a/addons/silicon.util.custom_docs/doc_item/property_doc_item.gd b/addons/silicon.util.custom_docs/doc_item/property_doc_item.gd deleted file mode 100644 index d6da050..0000000 --- a/addons/silicon.util.custom_docs/doc_item/property_doc_item.gd +++ /dev/null @@ -1,39 +0,0 @@ -## An object that contains documentation data about a property. -## @contribute https://placeholder_contribute.com -tool -class_name PropertyDocItem -extends DocItem - - -## @default "" -## A description of the property. -var description := "" - -## @default "" -## The default of the property in string form. -var default := "" - -## @default "" -## The enumeration of [member default]. -var enumeration := "" - -## @default "" -## The class/built-in type of [member default]. -var type := "" - -## @default "" -## The setter method of the property. -var setter := "" - -## @default "" -## The getter method of the property. -var getter := "" - - -func _init(args := {}) -> void: - for arg in args: - set(arg, args[arg]) - - -func _to_string() -> String: - return "[Property doc: " + name + "]" diff --git a/addons/silicon.util.custom_docs/doc_item/signal_doc_item.gd b/addons/silicon.util.custom_docs/doc_item/signal_doc_item.gd deleted file mode 100644 index ec775bb..0000000 --- a/addons/silicon.util.custom_docs/doc_item/signal_doc_item.gd +++ /dev/null @@ -1,19 +0,0 @@ -## An object that contains documentation data about a signal. -## @contribute https://placeholder_contribute.com -tool -class_name SignalDocItem -extends DocItem - - -## @default "" -## A description of the signal. -var description := "" - -## @default [] -## A list of arguments the signal carries. -var args := [] - - -func _init(args := {}) -> void: - for arg in args: - set(arg, args[arg]) diff --git a/addons/silicon.util.custom_docs/plugin.cfg b/addons/silicon.util.custom_docs/plugin.cfg deleted file mode 100644 index 37ee41c..0000000 --- a/addons/silicon.util.custom_docs/plugin.cfg +++ /dev/null @@ -1,7 +0,0 @@ -[plugin] - -name="Custom Docs" -description="This plugin allows you to view Custom classe documentation in the same place where the builtin class documentation is!" -author="SIsilicon" -version="1.0" -script="plugin.gd" diff --git a/addons/silicon.util.custom_docs/plugin.gd b/addons/silicon.util.custom_docs/plugin.gd deleted file mode 100644 index 27f0d17..0000000 --- a/addons/silicon.util.custom_docs/plugin.gd +++ /dev/null @@ -1,630 +0,0 @@ -tool -extends EditorPlugin - -# enum { -# SEARCH_CLASS = 1, -# SEARCH_METHOD = 2, -# SEARCH_SIGNAL = 4, -# SEARCH_CONSTANT = 8, -# SEARCH_PROPERTY = 16, -# SEARCH_THEME = 32, -# SEARCH_CASE = 64, -# SEARCH_TREE = 128 -# } - -# enum { -# ITEM_CLASS, -# ITEM_METHOD, -# ITEM_SIGNAL, -# ITEM_CONSTANT, -# ITEM_PROPERTY -# } - -# var doc_generator := preload("class_doc_generator.gd").new() -# var doc_exporter := preload("doc_exporter/editor_help_doc_exporter.gd").new() - -# var script_editor: ScriptEditor - -# var search_help: AcceptDialog -# var search_controls: HBoxContainer -# var search_term: String -# var search_flags: int -# var tree: Tree - -# var script_list: ItemList -# var script_tabs: TabContainer -# var section_list: ItemList - -# var class_docs := {} -# var doc_items := {} -# var current_label: RichTextLabel setget set_current_label - -# var theme: Theme -# var disabled_color: Color - -# var doc_timer: Timer - -# func _enter_tree() -> void: -# theme = get_editor_interface().get_base_control().theme -# disabled_color = theme.get_color("disabled_font_color", "Editor") - -# script_editor = get_editor_interface().get_script_editor() -# script_list = find_node_by_class(script_editor, "ItemList") -# script_tabs = get_child_chain(script_editor, [0, 1, 1]) -# search_help = find_node_by_class(script_editor, "EditorHelpSearch") -# search_controls = find_node_by_class(search_help, "LineEdit").get_parent() -# tree = find_node_by_class(search_help, "Tree") - -# if not search_help.is_connected("go_to_help", self, "_on_SearchHelp_go_to_help"): -# search_help.connect("go_to_help", self, "_on_SearchHelp_go_to_help", [], CONNECT_DEFERRED) - -# section_list = ItemList.new() -# section_list.allow_reselect = true -# section_list.size_flags_vertical = Control.SIZE_EXPAND_FILL -# get_child_chain(script_editor, [0, 1, 0, 1]).add_child(section_list) - -# if not section_list.is_connected("item_selected", self, "_on_SectionList_item_selected"): -# section_list.connect("item_selected", self, "_on_SectionList_item_selected") - -# doc_exporter.plugin = self -# doc_exporter.theme = theme -# doc_exporter.editor_settings = get_editor_interface().get_editor_settings() -# doc_exporter.class_docs = class_docs -# doc_exporter.update_theme_vars() -# doc_generator.plugin = self - -# doc_timer = Timer.new() -# doc_timer.wait_time = 0.5 -# add_child(doc_timer) -# doc_timer.start() -# if not doc_timer.is_connected("timeout", self, "_on_DocTimer_timeout"): -# doc_timer.connect("timeout", self, "_on_DocTimer_timeout") - -# # Load opened custom docs from last session. -# var settings_path := get_editor_interface().get_editor_settings().get_project_settings_dir() + "/opened_custom_docs.json" -# var file := File.new() -# if not file.open(settings_path, File.READ): -# var opened_tabs := [] -# for i in script_list.get_item_count(): -# opened_tabs.append(script_tabs.get_child(script_list.get_item_metadata(i)).name) - -# var selected := script_list.get_selected_items() -# var list: Array = JSON.parse(file.get_as_text()).result -# for item in list: -# if not item in opened_tabs: -# search_help.call_deferred("emit_signal", "go_to_help", "class_name:" + item) - -# if not selected.empty(): -# script_list.call_deferred("select", selected[0]) -# script_list.call_deferred("emit_signal", "item_selected", selected[0]) -# file.close() - - -# func _exit_tree() -> void: -# # Save opened custom docs for next session. -# var settings_path := get_editor_interface().get_editor_settings().get_project_settings_dir() + "/opened_custom_docs.json" -# var file := File.new() -# if not file.open(settings_path, File.WRITE): -# var opened_docs := [] -# for i in script_tabs.get_children(): -# if i.name in class_docs: -# opened_docs.append(i.name) -# file.store_string(JSON.print(opened_docs)) -# file.close() - -# section_list.queue_free() - - -# func _process(_delta := 0.0) -> void: -# if not tree: -# _enter_tree() -# if not tree: -# return - -# doc_generator._update() - -# # Update search help tree items -# if tree.get_root(): -# search_flags = search_controls.get_child(3).get_item_id(search_controls.get_child(3).selected) -# search_flags |= SEARCH_CASE * int(search_controls.get_child(1).pressed) -# search_flags |= SEARCH_TREE * int(search_controls.get_child(2).pressed) -# search_term = search_controls.get_child(0).text - -# for name in class_docs: -# if fits_search(name, ITEM_CLASS): -# process_custom_item(name, ITEM_CLASS) - -# for method in class_docs[name].methods: -# if fits_search(method.name, ITEM_METHOD): -# process_custom_item(name + "." + method.name, ITEM_METHOD) - -# for _signal in class_docs[name].signals: -# if fits_search(_signal.name, ITEM_SIGNAL): -# process_custom_item(name + "." + _signal.name, ITEM_SIGNAL) - -# for constant in class_docs[name].constants: -# if fits_search(constant.name, ITEM_CONSTANT): -# process_custom_item(name + "." + constant.name, ITEM_CONSTANT) - -# for property in class_docs[name].properties: -# if fits_search(property.name, ITEM_PROPERTY): -# process_custom_item(name + "." + property.name, ITEM_PROPERTY) - -# var custom_doc_open := false -# var doc_open := false -# for i in script_list.get_item_count(): -# var icon := script_list.get_item_icon(i) -# var text := script_list.get_item_text(i) - -# var editor_help = script_tabs.get_child(script_list.get_item_metadata(i)) -# if icon == theme.get_icon("Help", "EditorIcons"): -# if script_list.get_selected_items()[0] == i: -# doc_open = true -# if editor_help.name != text: -# text = editor_help.name -# script_list.set_item_tooltip(i, text + " Class Reference") - -# if script_list.get_selected_items()[0] == i and text in class_docs: -# custom_doc_open = true -# set_current_label(editor_help.get_child(0)) -# # else: -# # set_current_label(null) - -# script_list.call_deferred("set_item_text", i, text) - -# if custom_doc_open: -# section_list.get_parent().get_child(3).set_deferred("visible", false) -# section_list.visible = true -# else: -# section_list.get_parent().get_child(3).set_deferred("visible", doc_open) -# section_list.visible = false - - -# func get_parent_class(_class: String) -> String: -# if class_docs.has(_class): -# return class_docs[_class].base -# return ClassDB.get_parent_class(_class) - - -# func fits_search(name: String, type: int) -> bool: -# if type == ITEM_CLASS and not (search_flags & SEARCH_CLASS): -# return false -# elif type == ITEM_METHOD and not (search_flags & SEARCH_METHOD) or search_term.empty(): -# return false -# elif type == ITEM_SIGNAL and not (search_flags & SEARCH_SIGNAL) or search_term.empty(): -# return false -# elif type == ITEM_CONSTANT and not (search_flags & SEARCH_CONSTANT) or search_term.empty(): -# return false -# elif type == ITEM_PROPERTY and not (search_flags & SEARCH_PROPERTY) or search_term.empty(): -# return false - -# if not search_term.empty(): -# if (search_flags & SEARCH_CASE) and name.find(search_term) == -1: -# return false -# elif ~(search_flags & SEARCH_CASE) and name.findn(search_term) == -1: -# return false - -# return true - - -# func update_doc(label: RichTextLabel) -> void: -# doc_exporter.label = label -# doc_exporter._generate(class_docs[label.get_parent().name]) - -# var section_lines := doc_exporter.section_lines -# section_list.clear() -# for i in len(section_lines): -# section_list.add_item(section_lines[i][0]) -# section_list.set_item_metadata(i, section_lines[i][1]) - - -# func process_custom_item(name: String, type := ITEM_CLASS) -> TreeItem: -# # Create tree item if it's not their. -# if weakref(doc_items.get(name + str(type))).get_ref(): -# doc_items[name + str(type)].clear_custom_color(0) -# doc_items[name + str(type)].clear_custom_color(1) -# return doc_items[name + str(type)] - -# var parent := tree.get_root() -# var sub_name: String -# if name.find(".") != -1: -# var split := name.split(".") -# name = split[0] -# sub_name = split[1] - -# var doc: DocItem = class_docs[name] - -# if search_flags & SEARCH_TREE: -# # Get inheritance chain of the class. -# var inherit_chain = [doc.base] -# while not inherit_chain[-1].empty(): -# inherit_chain.append(get_parent_class(inherit_chain[-1])) -# inherit_chain.pop_back() -# inherit_chain.invert() -# if not sub_name.empty(): -# inherit_chain.append(name) - -# # Find the tree item the class should be under. -# for inherit in inherit_chain: -# var failed := true -# var child := parent.get_children() -# while child and child.get_parent() == parent: -# if child.get_text(0) == inherit: -# parent = child -# failed = false -# break -# child = child.get_next() - -# if failed: -# var new_parent: TreeItem -# if inherit in class_docs: -# new_parent = process_custom_item(inherit) -# if not new_parent: -# new_parent = tree.create_item(parent) -# new_parent.set_text(0, inherit) -# new_parent.set_text(1, "Class") -# new_parent.set_icon(0, get_class_icon(inherit)) -# new_parent.set_metadata(0, "class_name:" + inherit) -# new_parent.set_custom_color(0, disabled_color) -# new_parent.set_custom_color(1, disabled_color) -# parent = new_parent - -# var item := tree.create_item(parent) -# if not sub_name.empty(): -# name += "." + sub_name -# var display_name := sub_name if search_flags & SEARCH_TREE else name -# match type: -# ITEM_CLASS: -# item.set_text(0, name) -# item.set_text(1, "Class") -# item.set_tooltip(0, doc.brief) -# item.set_tooltip(1, doc.brief) -# item.set_metadata(0, "class_name:" + name) -# item.set_icon(0, get_class_icon("Object")) - -# ITEM_METHOD: -# doc = doc.get_method_doc(sub_name) -# item.set_text(0, display_name) -# item.set_text(1, "Method") -# item.set_tooltip(0, doc.return_type + " " + name + "()") -# item.set_tooltip(1, item.get_tooltip(0)) -# item.set_metadata(0, "class_method:" + name.replace(".", ":")) -# item.set_icon(0, theme.get_icon("MemberMethod", "EditorIcons")) - -# ITEM_SIGNAL: -# doc = doc.get_signal_doc(sub_name) -# item.set_text(0, display_name) -# item.set_text(1, "Signal") -# item.set_tooltip(0, name + "()") -# item.set_tooltip(1, item.get_tooltip(0)) -# item.set_metadata(0, "class_signal:" + name.replace(".", ":")) -# item.set_icon(0, theme.get_icon("MemberSignal", "EditorIcons")) - -# ITEM_CONSTANT: -# doc = doc.get_constant_doc(sub_name) -# item.set_text(0, display_name) -# item.set_text(1, "Constant") -# item.set_tooltip(0, name) -# item.set_tooltip(1, item.get_tooltip(0)) -# item.set_metadata(0, "class_constant:" + name.replace(".", ":")) -# item.set_icon(0, theme.get_icon("MemberConstant", "EditorIcons")) - -# ITEM_PROPERTY: -# doc = doc.get_property_doc(sub_name) -# item.set_text(0, display_name) -# item.set_text(1, "Property") -# item.set_tooltip(0, doc.type + " " + name) -# item.set_tooltip(1, item.get_tooltip(0)) -# item.set_metadata(0, "class_property:" + name.replace(".", ":")) -# item.set_icon(0, theme.get_icon("MemberProperty", "EditorIcons")) - -# var tooltip = item.get_tooltip(0) -# for key in doc.meta: -# tooltip += "\n" + snakekebab2pascal(key) + ": " + doc.meta[key] -# item.set_tooltip(0, tooltip) - -# doc_items[name + str(type)] = item -# return item - - -# func snakekebab2pascal(string: String) -> String: -# var result := PoolStringArray() -# var prev_is_underscore := true # Make false for camelCase -# for ch in string: -# if ch == "_" or ch == "-": -# prev_is_underscore = true -# else: -# if prev_is_underscore: -# result.append(ch.to_upper()) -# else: -# result.append(ch) -# prev_is_underscore = false - -# return result.join("") - - -# func purge_duplicate_tabs() -> void: -# var selected_duplicate := "" -# var i := 0 -# while i < script_list.get_item_count(): -# if script_list.get_item_icon(i) != theme.get_icon("Help", "EditorIcons"): -# i += 1 -# continue - -# var text := script_tabs.get_child(script_list.get_item_metadata(i)).name -# # Possible duplicate -# var is_duplicate := false -# if text[-1].is_valid_integer(): -# for doc in class_docs: -# if text.find(doc) != -1 and text.right(len(doc)).is_valid_integer(): -# text = doc -# is_duplicate = true -# break - -# if is_duplicate: -# # HACK: Creating a couple input events to simulate deleting the duplicate tab -# if script_list.is_visible_in_tree(): -# var prev_count := script_list.get_item_count() - -# if script_list.is_selected(i): -# selected_duplicate = text - -# script_list.select(i) -# var event := InputEventKey.new() -# event.scancode = KEY_W -# event.control = true -# event.pressed = true -# get_tree().input_event(event) -# event = event.duplicate() -# event.pressed = false -# get_tree().input_event(event) - -# # Makes sure that we don't run into an infinite loop. -# i -= prev_count - script_list.get_item_count() -# i += 1 - -# if not selected_duplicate.empty(): -# for j in script_list.get_item_count(): -# var editor_help := script_tabs.get_child(script_list.get_item_metadata(j)) -# if editor_help.name == selected_duplicate: -# script_list.select(j) -# script_list.emit_signal("item_selected", j) -# set_current_label(editor_help.get_child(0)) -# break - - -# func set_current_label(label: RichTextLabel) -> void: -# if current_label != label: -# if is_instance_valid(current_label): -# current_label.disconnect("meta_clicked", self, "_on_EditorHelpLabel_meta_clicked") - -# if is_instance_valid(label): -# update_doc(label) -# current_label = label -# current_label.connect("meta_clicked", self, "_on_EditorHelpLabel_meta_clicked", [current_label], CONNECT_DEFERRED) - - -# func get_class_icon(_class: String) -> Texture: -# if theme.has_icon(_class, "EditorIcons"): -# return theme.get_icon(_class, "EditorIcons") -# elif _class in class_docs: -# var path: String = class_docs[_class].icon -# if not path.empty() and load(path) is Texture: -# return load(path) as Texture -# return get_class_icon("Object") - - -# func get_child_chain(node: Node, indices: Array) -> Node: -# var child := node -# for index in indices: -# child = child.get_child(index) -# if not child: -# return null -# return child - - -# func find_node_by_class(node: Node, _class: String) -> Node: -# if node.is_class(_class): -# return node - -# for child in node.get_children(): -# var result = find_node_by_class(child, _class) -# if result: -# return result - -# return null - - -# func _on_DocTimer_timeout() -> void: -# doc_exporter.plugin = self -# doc_exporter.theme = theme -# doc_exporter.editor_settings = get_editor_interface().get_editor_settings() -# doc_exporter.class_docs = class_docs -# doc_exporter.update_theme_vars() -# doc_generator.plugin = self - -# var classes: Array = ProjectSettings.get("_global_script_classes") -# var class_icons: Dictionary = ProjectSettings.get("_global_script_class_icons") - -# # Include autoloads -# var file := File.new() -# while not file.open("res://project.godot", File.READ): -# var project_string := file.get_as_text() -# var autoload_loc := project_string.find("[autoload]\n") -# if autoload_loc == -1: -# break -# autoload_loc += len("[autoload]\n\n") - -# var list := project_string.right(autoload_loc).split("\n") -# for i in len(list): -# var line := list[i] -# if line.empty(): -# continue -# if line.begins_with("["): -# break - -# var entry := line.split("=") -# # An asterisk indicates that the singleton's enabled. -# if entry[1][1] != "*": -# continue - -# # Only gdscript and scenes are supported. -# var type := "other" -# var base := "" -# var path := entry[1].trim_prefix("\"*").trim_suffix("\"") -# var script: GDScript - -# if path.ends_with(".tscn") or path.ends_with(".scn"): -# type = "scene" -# elif type.ends_with(".gd"): -# type = "script" - -# if type == "other": -# continue -# elif type == "scene": -# script = load(path).instance().get_script() -# else: -# script = load(path) - -# if not script: -# continue -# if script.resource_path.empty(): -# continue -# else: -# path = script.resource_path -# base = script.get_instance_base_type() - -# classes.append({ -# "base": base, -# "class": entry[0], -# "language": "GDScript" if path.find(".gd") != -1 else "Other", -# "path": path, -# "is_autoload": true -# }) - -# break -# file.close() - -# var docs := {} -# for _class in classes: -# if _class["language"] != "GDScript": -# continue - -# # TODO: Add file path to class document item -# var doc := doc_generator.generate(_class["class"], _class["base"], _class["path"]) -# if not doc: -# continue - -# doc.icon = class_icons.get(doc.name, "") -# doc.is_singleton = _class.has("is_autoload") -# docs[doc.name] = doc -# class_docs[doc.name] = doc -# if not doc.name in doc_exporter.class_list: -# doc_exporter.class_list.append(doc.name) - -# # Periodically clean up tree items -# for name in doc_items: -# if not doc_items[name]: -# doc_items.erase(name) - -# for _class in class_docs: -# if not docs.has(_class): -# doc_exporter.class_list.erase(_class) -# class_docs.erase(_class) - - -# func _on_EditorHelpLabel_meta_clicked(meta: String, label: RichTextLabel) -> void: -# if meta.begins_with("$"): -# var select := meta.substr(1, len(meta)) -# var _class_name := "" -# if select.find(".") != -1: -# _class_name = select.split(".")[0] -# else: -# _class_name = "@GlobalScope" -# search_help.emit_signal("go_to_help", "class_enum:" + _class_name + ":" + select) -# elif meta.begins_with("#"): -# search_help.emit_signal("go_to_help", "class_name:" + meta.substr(1, len(meta))) -# elif meta.begins_with("@"): -# var tag_end := meta.find(" ") -# var tag := meta.substr(1, tag_end - 1) -# var link := meta.substr(tag_end + 1, meta.length()).lstrip(" ") - -# var topic := "" -# var table: Dictionary - -# if tag == "method": -# topic = "class_method" -# table = doc_exporter.method_line -# elif tag == "member": -# topic = "class_property" -# table = doc_exporter.property_line -# elif tag == "enum": -# topic = "class_enum" -# table = doc_exporter.enum_line -# elif tag == "signal": -# topic = "class_signal" -# table = doc_exporter.signal_line -# elif tag == "constant": -# topic = "class_constant" -# table = doc_exporter.constant_line -# else: -# return - -# if link.find(".") != -1: -# search_help.emit_signal("go_to_help", topic + ":" + link.split(".")[0] + ":" + link.split(".")[1]) -# else: -# if table.has(link): -# # Found in the current page -# current_label.scroll_to_line(table[link]) -# else: -# pass # Oh well. - - -# func _on_SearchHelp_go_to_help(tag: String) -> void: -# purge_duplicate_tabs() -# var editor_help := script_tabs.get_child(script_list.get_selected_items()[0]) -# if editor_help.name in class_docs.keys(): -# set_current_label(editor_help.get_child(0)) - -# var what := tag.split(":")[0] -# var clss := tag.split(":")[1] -# var name := "" -# if len(tag.split(":")) == 3: -# name = tag.split(":")[2] - -# var de := doc_exporter -# var line := 0 -# if what == "class_desc": -# line = de.description_line -# elif what == "class_signal": -# if de.signal_line.has(name): -# line = de.signal_line[name] -# elif what == "class_method" or what == "class_method_desc": -# if de.method_line.has(name): -# line = de.method_line[name] -# elif what == "class_property": -# if de.property_line.has(name): -# line = de.property_line[name] -# elif what == "class_enum": -# if de.enum_line.has(name): -# line = de.enum_line[name] -# # elif what == "class_theme_item": -# # if (theme_property_line.has(name)) -# # line = theme_property_line[name] -# elif what == "class_constant": -# if de.constant_line.has(name): -# line = de.constant_line[name] -# elif what == "class_name": -# pass -# else: -# printerr("Could not go to help: " + tag) - -# current_label.call_deferred("scroll_to_line", line) - - -# func _on_SectionList_item_selected(index: int) -> void: -# if not current_label: -# return -# current_label.scroll_to_line(section_list.get_item_metadata(index)) - - From 130d724c195b3c86346c493b877215863638a791 Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Sat, 4 Feb 2023 09:29:17 -0500 Subject: [PATCH 02/49] Remove remaining GUT references --- .gutconfig.json | 16 -- test/TestUtils.gd | 8 - test/firestore_test.gd | 31 ---- test/firestore_test.tscn | 6 - test/storage_stress_test.gd | 54 ------ test/storage_stress_test.tscn | 6 - test/unit/test_FirebaseDatabaseStore.gd | 221 ------------------------ test/unit/test_FirestoreDocument.gd | 48 ----- 8 files changed, 390 deletions(-) delete mode 100644 .gutconfig.json delete mode 100644 test/TestUtils.gd delete mode 100644 test/firestore_test.gd delete mode 100644 test/firestore_test.tscn delete mode 100644 test/storage_stress_test.gd delete mode 100644 test/storage_stress_test.tscn delete mode 100644 test/unit/test_FirebaseDatabaseStore.gd delete mode 100644 test/unit/test_FirestoreDocument.gd diff --git a/.gutconfig.json b/.gutconfig.json deleted file mode 100644 index 10b60bf..0000000 --- a/.gutconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "dirs":["res://test/unit/"], - "double_strategy":"partial", - "ignore_pause":false, - "include_subdirs":true, - "inner_class":"", - "log_level":3, - "opacity":100, - "prefix":"test_", - "selected":"", - "should_exit":true, - "should_maximize":true, - "suffix":".gd", - "tests": [], - "unit_test_name": "" -} diff --git a/test/TestUtils.gd b/test/TestUtils.gd deleted file mode 100644 index a109892..0000000 --- a/test/TestUtils.gd +++ /dev/null @@ -1,8 +0,0 @@ -class_name TestUtils -extends Object - - -static func instantiate(script: Script) -> Node: - var o = Node.new() - o.set_script(script) - return o diff --git a/test/firestore_test.gd b/test/firestore_test.gd deleted file mode 100644 index 2e22a48..0000000 --- a/test/firestore_test.gd +++ /dev/null @@ -1,31 +0,0 @@ -extends Node2D - -export var email := "" -export var password := "" - -func _ready() -> void: - Firebase.Auth.login_with_email_and_password(email, password) - yield(Firebase.Auth, "login_succeeded") - print("Logged in!") - - var task: FirestoreTask - - Firebase.Firestore.disable_networking() - - task = Firebase.Firestore.list("test_collection", 5, "", "number") - print(yield(task, "listed_documents")) - - var test : FirestoreCollection = Firebase.Firestore.collection("test_collection") - - for i in 5: - var name = "some_document_%d" % hash(str(i)) - task = test.delete(name) - task = test.update(name, {"number": i + 10}) - - var document = yield(task, "task_finished") - - Firebase.Firestore.enable_networking() - - task = test.get("some_document_%d" % hash(str(4))) - document = yield(task, "task_finished") - print(document) diff --git a/test/firestore_test.tscn b/test/firestore_test.tscn deleted file mode 100644 index 98660a8..0000000 --- a/test/firestore_test.tscn +++ /dev/null @@ -1,6 +0,0 @@ -[gd_scene load_steps=2 format=2] - -[ext_resource path="res://test/firestore_test.gd" type="Script" id=1] - -[node name="Node2D" type="Node2D"] -script = ExtResource( 1 ) diff --git a/test/storage_stress_test.gd b/test/storage_stress_test.gd deleted file mode 100644 index 3594a37..0000000 --- a/test/storage_stress_test.gd +++ /dev/null @@ -1,54 +0,0 @@ -extends Node2D - - -var offset := 0 - -export var email := "" -export var password := "" - - -func _ready() -> void: - Firebase.Auth.login_with_email_and_password(email, password) - yield(Firebase.Auth, "login_succeeded") - print("Logged in!") - - var ref = Firebase.Storage.ref("test/test_image0.png") - var task = ref.put_file("res://icon.png") - task.connect("task_finished", self, "_on_task_finished", [task]) - - for i in range(10): - task = ref.get_data() - task.connect("task_finished", self, "_on_task_finished", [task]) - - task = ref.delete() - task.connect("task_finished", self, "_on_task_finished", [task]) - - -func _on_task_finished(task: StorageTask) -> void: - if task.result or task.response_code >= 400: - if typeof(task.data) == TYPE_DICTIONARY: - printerr(task.data) - else: - printerr(JSON.parse(task.data.get_string_from_utf8()).result) - return - - match task.action: - StorageTask.Task.TASK_UPLOAD: - print("%s uploaded!" % task.ref) - - StorageTask.Task.TASK_DOWNLOAD: - var image := Image.new() - image.load_png_from_buffer(task.data) - var tex := ImageTexture.new() - tex.create_from_image(image) - - var sprite := Sprite.new() - sprite.scale *= 0.7 - sprite.centered = false - sprite.texture = tex - sprite.position.x = offset - add_child(sprite) - offset += 100 - - StorageTask.Task.TASK_DELETE: - print("%s deleted!" % task.ref) diff --git a/test/storage_stress_test.tscn b/test/storage_stress_test.tscn deleted file mode 100644 index dd8a279..0000000 --- a/test/storage_stress_test.tscn +++ /dev/null @@ -1,6 +0,0 @@ -[gd_scene load_steps=2 format=2] - -[ext_resource path="res://test/storage_stress_test.gd" type="Script" id=1] - -[node name="Node2D" type="Node2D"] -script = ExtResource( 1 ) diff --git a/test/unit/test_FirebaseDatabaseStore.gd b/test/unit/test_FirebaseDatabaseStore.gd deleted file mode 100644 index 6278b63..0000000 --- a/test/unit/test_FirebaseDatabaseStore.gd +++ /dev/null @@ -1,221 +0,0 @@ -extends "res://addons/gut/test.gd" - - -const FirebaseDatabaseStore = preload("res://addons/godot-firebase/database/database_store.gd") -const TestKey = "-MPrgu_F8OXiL-VpRxjq" -const TestObject = { - "I": "Some Value", - "II": "Some Other Value", - "III": [111, 222, 333, 444, 555], - "IV": { - "a": "Another Value", - "b": "Yet Another Value" - } -} -const TestObjectOther = { - "a": "A Different Value", - "b": "Another One", - "c": "A New Value" -} -const TestArray = [666, 777, 888, 999] -const TestValue = 12345.6789 - -class TestPutOperations: - extends "res://addons/gut/test.gd" - - - func test_put_object(): - var store = TestUtils.instantiate(FirebaseDatabaseStore) - - store.put(TestKey, TestObject) - - var store_data: Dictionary = store.get_data() - var store_object = store_data[TestKey] - - assert_eq_deep(store_object, TestObject) - - store.queue_free() - - - func test_put_nested_object(): - var store = TestUtils.instantiate(FirebaseDatabaseStore) - - store.put(TestKey, TestObject) - store.put(TestKey + "/V", TestObjectOther) - - var store_data: Dictionary = store.get_data() - var store_object = store_data[TestKey]["V"] - - assert_eq_deep(store_object, TestObjectOther) - - store.queue_free() - - - func test_put_array_value(): - var store = TestUtils.instantiate(FirebaseDatabaseStore) - - store.put(TestKey, TestObject) - store.put(TestKey + "/III", TestArray) - - var store_data: Dictionary = store.get_data() - var store_object = store_data[TestKey]["III"] - - assert_eq_deep(store_object, TestArray) - - store.queue_free() - - - func test_put_normal_value(): - var store = TestUtils.instantiate(FirebaseDatabaseStore) - - store.put(TestKey, TestObject) - store.put(TestKey + "/II", TestValue) - - var store_data: Dictionary = store.get_data() - var store_object = store_data[TestKey]["II"] - - assert_eq_deep(store_object, TestValue) - - store.queue_free() - - - func test_put_deleted_value(): - # NOTE: Firebase Realtime Database sets values to null to indicate that they have been - # deleted. - var store = TestUtils.instantiate(FirebaseDatabaseStore) - - store.put(TestKey, TestObject) - store.put(TestKey + "/II", null) - - var store_data: Dictionary = store.get_data() - var store_object = store_data[TestKey] - - assert_false(store_object.has("II"), "The value should have been deleted, but was not.") - - store.queue_free() - - - func test_put_new_object(): - var store = TestUtils.instantiate(FirebaseDatabaseStore) - - store.put(TestKey, TestObject) - - var store_data: Dictionary = store.get_data() - var store_object = store_data[TestKey] - - assert_eq_deep(store_object, TestObject) - - store.queue_free() - - - func test_put_new_nested_object(): - var store = TestUtils.instantiate(FirebaseDatabaseStore) - - store.put(TestKey + "/V", TestObjectOther) - - var store_data: Dictionary = store.get_data() - var store_object = store_data[TestKey]["V"] - - assert_eq_deep(store_object, TestObjectOther) - - store.queue_free() - - - func test_put_new_array_value(): - var store = TestUtils.instantiate(FirebaseDatabaseStore) - - store.put(TestKey + "/III", TestArray) - - var store_data: Dictionary = store.get_data() - var store_object = store_data[TestKey]["III"] - - assert_eq_deep(store_object, TestArray) - - store.queue_free() - - - func test_put_new_normal_value(): - var store = TestUtils.instantiate(FirebaseDatabaseStore) - - store.put(TestKey + "/II", TestValue) - - var store_data: Dictionary = store.get_data() - var store_object = store_data[TestKey]["II"] - - assert_eq_deep(store_object, TestValue) - - store.queue_free() - -class TestPatchOperations: - extends "res://addons/gut/test.gd" - - - func test_patch_object(): - var store = TestUtils.instantiate(FirebaseDatabaseStore) - - store.patch(TestKey, TestObject) - - var store_data: Dictionary = store.get_data() - var store_object = store_data[TestKey] - - assert_eq_deep(store_object, TestObject) - - store.queue_free() - - - func test_patch_nested_object(): - var store = TestUtils.instantiate(FirebaseDatabaseStore) - - store.put(TestKey, TestObject) - store.patch(TestKey + "/V", TestObjectOther) - - var store_data: Dictionary = store.get_data() - var store_object = store_data[TestKey]["V"] - - assert_eq_deep(store_object, TestObjectOther) - - store.queue_free() - - - func test_patch_array_value(): - var store = TestUtils.instantiate(FirebaseDatabaseStore) - - store.put(TestKey, TestObject) - store.patch(TestKey + "/III", TestArray) - - var store_data: Dictionary = store.get_data() - var store_object = store_data[TestKey]["III"] - - assert_eq_deep(store_object, TestArray) - - store.queue_free() - - - func test_patch_normal_value(): - var store = TestUtils.instantiate(FirebaseDatabaseStore) - - store.put(TestKey, TestObject) - store.patch(TestKey + "/II", TestValue) - - var store_data: Dictionary = store.get_data() - var store_object = store_data[TestKey]["II"] - - assert_eq_deep(store_object, TestValue) - - store.queue_free() - - - func test_patch_deleted_value(): - # NOTE: Firebase Realtime Database sets values to null to indicate that they have been - # deleted. - var store = TestUtils.instantiate(FirebaseDatabaseStore) - - store.put(TestKey, TestObject) - store.patch(TestKey + "/II", null) - - var store_data: Dictionary = store.get_data() - var store_object = store_data[TestKey] - - assert_false(store_object.has("II"), "The value should have been deleted, but was not.") - - store.queue_free() diff --git a/test/unit/test_FirestoreDocument.gd b/test/unit/test_FirestoreDocument.gd deleted file mode 100644 index 3313c35..0000000 --- a/test/unit/test_FirestoreDocument.gd +++ /dev/null @@ -1,48 +0,0 @@ -extends "res://addons/gut/test.gd" - - -const FirestoreDocument = preload("res://addons/godot-firebase/firestore/firestore_document.gd") - -class TestDeserialization: - extends "res://addons/gut/test.gd" - - - func test_deserialize_array_of_dicts(): - var doc_infos: Dictionary = { - "name": "projects/godot-firebase/databases/(default)/documents/rooms/EUZT", - "fields": { - "code": {"stringValue": "EUZT"}, - "players": { - "arrayValue": { - "values": [ - {"mapValue": {"fields": {"name": {"stringValue": "Hello"}}}}, - {"mapValue": {"fields": {"name": {"stringValue": "Test"}}}} - ] - } - } - }, - "createTime": "2021-02-16T07:24:11.106522Z", - "updateTime": "2021-02-16T08:21:32.131028Z" - } - var expected_doc_fields: Dictionary = { - "code": "EUZT", "players": [{"name": "Hello"}, {"name": "Test"}] - } - var firestore_document: FirestoreDocument = FirestoreDocument.new(doc_infos) - - assert_eq_deep(firestore_document.doc_fields, expected_doc_fields) - - - func test_deserialize_array_of_strings(): - var doc_infos: Dictionary = { - "name": "projects/godot-firebase/databases/(default)/documents/rooms/EUZT", - "fields": { - "code": {"stringValue": "EUZT"}, - "things": {"arrayValue": {"values": [{"stringValue": "first"}, {"stringValue": "second"}]}} - }, - "createTime": "2021-02-16T07:24:11.106522Z", - "updateTime": "2021-02-16T08:21:32.131028Z" - } - var expected_doc_fields: Dictionary = {"code": "EUZT", "things": ["first", "second"]} - var firestore_document: FirestoreDocument = FirestoreDocument.new(doc_infos) - - assert_eq_deep(firestore_document.doc_fields, expected_doc_fields) From 5fb709ad279f217505771705e25a966f667ede70 Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Sat, 4 Feb 2023 14:15:11 -0500 Subject: [PATCH 03/49] Initial commit - DO NOT USE --- addons/godot-firebase/Utilies.gd | 10 + addons/godot-firebase/auth/auth.gd | 454 ++++++++++-------- addons/godot-firebase/auth/auth_provider.gd | 9 +- .../godot-firebase/auth/providers/facebook.gd | 11 +- .../godot-firebase/auth/providers/github.gd | 7 +- .../godot-firebase/auth/providers/google.gd | 3 +- .../godot-firebase/auth/providers/twitter.gd | 38 +- addons/godot-firebase/auth/user_data.gd | 32 +- addons/godot-firebase/database/database.gd | 34 +- .../godot-firebase/database/database_store.gd | 8 +- addons/godot-firebase/database/reference.gd | 181 ++++--- addons/godot-firebase/database/resource.gd | 9 +- .../dynamiclinks/dynamiclinks.gd | 72 ++- addons/godot-firebase/firebase/firebase.gd | 105 ++-- addons/godot-firebase/firebase/firebase.tscn | 2 +- addons/godot-firebase/firestore/firestore.gd | 126 ++--- .../firestore/firestore_collection.gd | 54 +-- .../firestore/firestore_document.gd | 118 +++-- .../firestore/firestore_query.gd | 11 +- .../firestore/firestore_task.gd | 256 ++-------- .../godot-firebase/functions/function_task.gd | 59 +-- addons/godot-firebase/functions/functions.gd | 41 +- addons/godot-firebase/icon.svg.import | 32 +- addons/godot-firebase/plugin.cfg | 6 +- addons/godot-firebase/plugin.gd | 4 +- addons/godot-firebase/storage/storage.gd | 160 +++--- .../storage/storage_reference.gd | 82 ++-- addons/godot-firebase/storage/storage_task.gd | 36 +- addons/http-sse-client/HTTPSSEClient.gd | 54 +-- .../http-sse-client/httpsseclient_plugin.gd | 4 +- 30 files changed, 863 insertions(+), 1155 deletions(-) create mode 100644 addons/godot-firebase/Utilies.gd diff --git a/addons/godot-firebase/Utilies.gd b/addons/godot-firebase/Utilies.gd new file mode 100644 index 0000000..1e66a0c --- /dev/null +++ b/addons/godot-firebase/Utilies.gd @@ -0,0 +1,10 @@ +extends Node +class_name Utilities + +static func get_json_data(value): + var json = JSON.new() + var json_parse_result = json.parse(value) + if json_parse_result == OK: + return json.data + + return null diff --git a/addons/godot-firebase/auth/auth.gd b/addons/godot-firebase/auth/auth.gd index bf3d98b..55dd786 100644 --- a/addons/godot-firebase/auth/auth.gd +++ b/addons/godot-firebase/auth/auth.gd @@ -2,13 +2,12 @@ ## @meta-version 2.5 ## The authentication API for Firebase. ## Documentation TODO. -tool +@tool class_name FirebaseAuth extends HTTPRequest - -const _API_VERSION: String = "v1" -const _INAPP_PLUGIN: String = "GodotSvc" +const _API_VERSION : String = "v1" +const _INAPP_PLUGIN : String = "GodotSvc" # Emitted for each Auth request issued. # `result_code` -> Either `1` if auth succeeded or `error_code` if unsuccessful auth request @@ -24,50 +23,51 @@ signal token_exchanged(successful) signal token_refresh_succeeded(auth_result) signal logged_out() -const RESPONSE_SIGNUP: String = "identitytoolkit#SignupNewUserResponse" -const RESPONSE_SIGNIN: String = "identitytoolkit#VerifyPasswordResponse" -const RESPONSE_ASSERTION: String = "identitytoolkit#VerifyAssertionResponse" -const RESPONSE_USERDATA: String = "identitytoolkit#GetAccountInfoResponse" -const RESPONSE_CUSTOM_TOKEN: String = "identitytoolkit#VerifyCustomTokenResponse" +const RESPONSE_SIGNUP : String = "identitytoolkit#SignupNewUserResponse" +const RESPONSE_SIGNIN : String = "identitytoolkit#VerifyPasswordResponse" +const RESPONSE_ASSERTION : String = "identitytoolkit#VerifyAssertionResponse" +const RESPONSE_USERDATA : String = "identitytoolkit#GetAccountInfoResponse" +const RESPONSE_CUSTOM_TOKEN : String = "identitytoolkit#VerifyCustomTokenResponse" -var _base_url: String = "" +var _base_url : String = "" var _refresh_request_base_url = "" -var _signup_request_url: String = "accounts:signUp?key=%s" -var _signin_with_oauth_request_url: String = "accounts:signInWithIdp?key=%s" -var _signin_request_url: String = "accounts:signInWithPassword?key=%s" -var _signin_custom_token_url: String = "accounts:signInWithCustomToken?key=%s" -var _userdata_request_url: String = "accounts:lookup?key=%s" -var _oobcode_request_url: String = "accounts:sendOobCode?key=%s" -var _delete_account_request_url: String = "accounts:delete?key=%s" -var _update_account_request_url: String = "accounts:update?key=%s" - -var _refresh_request_url: String = "/v1/token?key=%s" -var _google_auth_request_url: String = "https://accounts.google.com/o/oauth2/v2/auth?" - -var _config: Dictionary = {} -var auth: Dictionary = {} -var _needs_refresh: bool = false -var is_busy: bool = false -var has_child: bool = false - -var tcp_server: TCP_Server = TCP_Server.new() -var tcp_timer: Timer = Timer.new() -var tcp_timeout: float = 0.5 - -var _headers: PoolStringArray = [ +var _signup_request_url : String = "accounts:signUp?key=%s" +var _signin_with_oauth_request_url : String = "accounts:signInWithIdp?key=%s" +var _signin_request_url : String = "accounts:signInWithPassword?key=%s" +var _signin_custom_token_url : String = "accounts:signInWithCustomToken?key=%s" +var _userdata_request_url : String = "accounts:lookup?key=%s" +var _oobcode_request_url : String = "accounts:sendOobCode?key=%s" +var _delete_account_request_url : String = "accounts:delete?key=%s" +var _update_account_request_url : String = "accounts:update?key=%s" + +var _refresh_request_url : String = "/v1/token?key=%s" +var _google_auth_request_url : String = "https://accounts.google.com/o/oauth2/v2/auth?" + +var _config : Dictionary = {} +var auth : Dictionary = {} +var _needs_refresh : bool = false +var is_busy : bool = false +var has_child : bool = false + + +var tcp_server : TCPServer = TCPServer.new() +var tcp_timer : Timer = Timer.new() +var tcp_timeout : float = 0.5 + +var _headers : PackedStringArray = [ "Content-Type: application/json", "Accept: application/json", ] -var requesting: int = -1 +var requesting : int = -1 enum Requests { NONE = -1, EXCHANGE_TOKEN, - LOGIN_WITH_OAUTH, + LOGIN_WITH_OAUTH } -var auth_request_type: int = -1 +var auth_request_type : int = -1 enum Auth_Type { NONE = -1, @@ -75,82 +75,85 @@ enum Auth_Type { LOGIN_ANON, LOGIN_CT, LOGIN_OAUTH, - SIGNUP_EP, + SIGNUP_EP } -var _login_request_body: Dictionary = { - "email": "", - "password": "", +var _login_request_body : Dictionary = { + "email":"", + "password":"", "returnSecureToken": true, } -var _oauth_login_request_body: Dictionary = { - "postBody": "", - "requestUri": "", - "returnIdpCredential": false, - "returnSecureToken": true, +var _oauth_login_request_body : Dictionary = { + "postBody":"", + "requestUri":"", + "returnIdpCredential":false, + "returnSecureToken":true } -var _anonymous_login_request_body: Dictionary = { - "returnSecureToken": true, +var _anonymous_login_request_body : Dictionary = { + "returnSecureToken":true } -var _refresh_request_body: Dictionary = { - "grant_type": "refresh_token", - "refresh_token": "", +var _refresh_request_body : Dictionary = { + "grant_type":"refresh_token", + "refresh_token":"", } -var _custom_token_body: Dictionary = { - "token": "", - "returnSecureToken": true, +var _custom_token_body : Dictionary = { + "token":"", + "returnSecureToken":true } -var _password_reset_body: Dictionary = { - "requestType": "password_reset", - "email": "", +var _password_reset_body : Dictionary = { + "requestType":"password_reset", + "email":"", } -var _change_email_body: Dictionary = { - "idToken": "", - "email": "", + +var _change_email_body : Dictionary = { + "idToken":"", + "email":"", "returnSecureToken": true, } -var _change_password_body: Dictionary = { - "idToken": "", - "password": "", + +var _change_password_body : Dictionary = { + "idToken":"", + "password":"", "returnSecureToken": true, } -var _account_verification_body: Dictionary = { - "requestType": "verify_email", - "idToken": "", -} -var _update_profile_body: Dictionary = { - "idToken": "", - "displayName": "", - "photoUrl": "", - "deleteAttribute": "", - "returnSecureToken": true, +var _account_verification_body : Dictionary = { + "requestType":"verify_email", + "idToken":"", } -var _local_port: int = 8060 -var _local_uri: String = "http://localhost:%s/" % _local_port -var _local_provider: AuthProvider = AuthProvider.new() +var _update_profile_body : Dictionary = { + "idToken":"", + "displayName":"", + "photoUrl":"", + "deleteAttribute":"", + "returnSecureToken":true +} + +var _local_port : int = 8060 +var _local_uri : String = "http://localhost:%s/"%_local_port +var _local_provider : AuthProvider = AuthProvider.new() func _ready() -> void: tcp_timer.wait_time = tcp_timeout - tcp_timer.connect("timeout", self, "_tcp_stream_timer") - + tcp_timer.timeout.connect(_tcp_stream_timer) + if OS.get_name() == "HTML5": _local_uri += "tmp_js_export.html" # Sets the configuration needed for the plugin to talk to Firebase # These settings come from the Firebase.gd script automatically -func _set_config(config_json: Dictionary) -> void: +func _set_config(config_json : Dictionary) -> void: _config = config_json _signup_request_url %= _config.apiKey _signin_request_url %= _config.apiKey @@ -162,21 +165,21 @@ func _set_config(config_json: Dictionary) -> void: _delete_account_request_url %= _config.apiKey _update_account_request_url %= _config.apiKey - connect("request_completed", self, "_on_FirebaseAuth_request_completed") + request_completed.connect(_on_FirebaseAuth_request_completed) _check_emulating() -func _check_emulating() -> void: +func _check_emulating() -> void : ## Check emulating if not Firebase.emulating: - _base_url = "https://identitytoolkit.googleapis.com/{version}/".format({version = _API_VERSION}) + _base_url = "https://identitytoolkit.googleapis.com/{version}/".format({ version = _API_VERSION }) _refresh_request_base_url = "https://securetoken.googleapis.com" else: - var port: String = _config.emulators.ports.authentication + var port : String = _config.emulators.ports.authentication if port == "": Firebase._printerr("You are in 'emulated' mode, but the port for Authentication has not been configured.") else: - _base_url = "http://localhost:{port}/identitytoolkit.googleapis.com/{version}/".format({version = _API_VERSION, port = port}) + _base_url = "http://localhost:{port}/identitytoolkit.googleapis.com/{version}/".format({ version = _API_VERSION ,port = port }) _refresh_request_base_url = "http://localhost:{port}/securetoken.googleapis.com".format({port = port}) @@ -189,15 +192,13 @@ func _is_ready() -> bool: else: return true - # Function cleans the URI and replaces spaces with %20 # As of right now we only replace spaces -# We may need to decide to use the percent_encode() String function +# We may need to decide to use the uri_encode() String function func _clean_url(_url): - _url = _url.replace(" ", "%20") + _url = _url.replace(' ','%20') return _url - # Synchronous call to check if any user is already logged in. func is_logged_in() -> bool: return auth != null and auth.has("idtoken") @@ -205,13 +206,18 @@ func is_logged_in() -> bool: # Called with Firebase.Auth.signup_with_email_and_password(email, password) # You must pass in the email and password to this function for it to work correctly -func signup_with_email_and_password(email: String, password: String) -> void: +func signup_with_email_and_password(email : String, password : String) -> void: if _is_ready(): is_busy = true _login_request_body.email = email _login_request_body.password = password auth_request_type = Auth_Type.SIGNUP_EP - request(_base_url + _signup_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_login_request_body)) + var err = request(_base_url + _signup_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_login_request_body)) + _login_request_body.email = "" + _login_request_body.password = "" + if err != OK: + is_busy = false + Firebase._printerr("Error signing up with password and email: %s" % err) # Called with Firebase.Auth.anonymous_login() @@ -222,37 +228,45 @@ func login_anonymous() -> void: if _is_ready(): is_busy = true auth_request_type = Auth_Type.LOGIN_ANON - request(_base_url + _signup_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_anonymous_login_request_body)) - + var err = request(_base_url + _signup_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_anonymous_login_request_body)) + if err != OK: + is_busy = false + Firebase._printerr("Error logging in as anonymous: %s" % err) # Called with Firebase.Auth.login_with_email_and_password(email, password) # You must pass in the email and password to this function for it to work correctly # If the login fails it will return an error code through the function _on_FirebaseAuth_request_completed -func login_with_email_and_password(email: String, password: String) -> void: +func login_with_email_and_password(email : String, password : String) -> void: if _is_ready(): is_busy = true _login_request_body.email = email _login_request_body.password = password auth_request_type = Auth_Type.LOGIN_EP - request(_base_url + _signin_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_login_request_body)) - + var err = request(_base_url + _signin_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_login_request_body)) + _login_request_body.email = "" + _login_request_body.password = "" + if err != OK: + is_busy = false + Firebase._printerr("Error logging in with password and email: %s" % err) # Login with a custom valid token # The token needs to be generated using an external service/function -func login_with_custom_token(token: String) -> void: +func login_with_custom_token(token : String) -> void: if _is_ready(): is_busy = true _custom_token_body.token = token auth_request_type = Auth_Type.LOGIN_CT - request(_base_url + _signin_custom_token_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_custom_token_body)) - + var err = request(_base_url + _signin_custom_token_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_custom_token_body)) + if err != OK: + is_busy = false + Firebase._printerr("Error logging in with custom token: %s" % err) # Open a web page in browser redirecting to Google oAuth2 page for the current project # Once given user's authorization, a token will be generated. # NOTE** the generated token will be automatically captured and a login request will be made if the token is correct -func get_auth_localhost(provider: AuthProvider = get_GoogleProvider(), port: int = _local_port): +func get_auth_localhost(provider: AuthProvider = get_GoogleProvider(), port : int = _local_port): get_auth_with_redirect(provider) - yield(get_tree().create_timer(0.5),"timeout") + await get_tree().create_timer(0.5).timeout if has_child == false: add_child(tcp_timer) has_child = true @@ -263,13 +277,13 @@ func get_auth_localhost(provider: AuthProvider = get_GoogleProvider(), port: int func get_auth_with_redirect(provider: AuthProvider) -> void: var url_endpoint: String = provider.redirect_uri for key in provider.params.keys(): - url_endpoint += key + "=" + provider.params[key] + "&" - url_endpoint += provider.params.redirect_type + "=" + _local_uri + url_endpoint+=key+"="+provider.params[key]+"&" + url_endpoint += provider.params.redirect_type+"="+_local_uri url_endpoint = _clean_url(url_endpoint) if OS.get_name() == "HTML5" and OS.has_feature("JavaScript"): - JavaScript.eval('window.location.replace("' + url_endpoint + '")') + JavaScriptBridge.eval('window.location.replace("' + url_endpoint + '")') elif Engine.has_singleton(_INAPP_PLUGIN) and OS.get_name() == "iOS": - # in app for ios if the iOS plugin exists + #in app for ios if the iOS plugin exists set_local_provider(provider) Engine.get_singleton(_INAPP_PLUGIN).popup(url_endpoint) else: @@ -282,27 +296,32 @@ func get_auth_with_redirect(provider: AuthProvider) -> void: # A token is automatically obtained using an authorization code using @get_google_auth() # @provider_id and @request_uri can be changed func login_with_oauth(_token: String, provider: AuthProvider) -> void: - var token: String = _token.percent_decode() - print(token) - var is_successful: bool = true - if provider.should_exchange: - exchange_token(token, _local_uri, provider.access_token_uri, provider.get_client_id(), provider.get_client_secret()) - is_successful = yield(self, "token_exchanged") - token = auth.accesstoken - if is_successful and _is_ready(): - is_busy = true - _oauth_login_request_body.postBody = "access_token=" + token + "&providerId=" + provider.provider_id - _oauth_login_request_body.requestUri = _local_uri - requesting = Requests.LOGIN_WITH_OAUTH - auth_request_type = Auth_Type.LOGIN_OAUTH - request(_base_url + _signin_with_oauth_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_oauth_login_request_body)) - + if _token: + var token : String = _token.uri_decode() + print(token) + var is_successful: bool = true + if provider.should_exchange: + exchange_token(token, _local_uri, provider.access_token_uri, provider.get_client_id(), provider.get_client_secret()) + is_successful = await self.token_exchanged + token = auth.accesstoken + if is_successful and _is_ready(): + is_busy = true + _oauth_login_request_body.postBody = "access_token="+token+"&providerId="+provider.provider_id + _oauth_login_request_body.requestUri = _local_uri + requesting = Requests.LOGIN_WITH_OAUTH + auth_request_type = Auth_Type.LOGIN_OAUTH + var err = request(_base_url + _signin_with_oauth_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_oauth_login_request_body)) + _oauth_login_request_body.postBody = "" + _oauth_login_request_body.requestUri = "" + if err != OK: + is_busy = false + Firebase._printerr("Error logging in with oauth: %s" % err) # Exchange the authorization oAuth2 code obtained from browser with a proper access id_token -func exchange_token(code: String, redirect_uri: String, request_url: String, _client_id: String, _client_secret: String) -> void: +func exchange_token(code : String, redirect_uri : String, request_url: String, _client_id: String, _client_secret: String) -> void: if _is_ready(): is_busy = true - var exchange_token_body: Dictionary = { + var exchange_token_body : Dictionary = { code = code, redirect_uri = redirect_uri, client_id = _client_id, @@ -310,9 +329,11 @@ func exchange_token(code: String, redirect_uri: String, request_url: String, _cl grant_type = "authorization_code", } requesting = Requests.EXCHANGE_TOKEN - request(request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(exchange_token_body)) - - + var err = request(request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(exchange_token_body)) + if err != OK: + is_busy = false + Firebase._printerr("Error exchanging tokens: %s" % err) + # Open a web page in browser redirecting to Google oAuth2 page for the current project # Once given user's authorization, a token will be generated. # NOTE** with this method, the authorization process will be copy-pasted @@ -320,44 +341,46 @@ func get_google_auth_manual(provider: AuthProvider = _local_provider) -> void: provider.params.redirect_uri = "urn:ietf:wg:oauth:2.0:oob" get_auth_with_redirect(provider) - -# A timer used to listen through TCP on the redirect uri of the request +# A timer used to listen through TCP checked the redirect uri of the request func _tcp_stream_timer() -> void: var peer : StreamPeer = tcp_server.take_connection() if peer != null: - var raw_result: String = peer.get_utf8_string(400) + var raw_result : String = peer.get_utf8_string(400) if raw_result != "" and raw_result.begins_with("GET"): tcp_timer.stop() remove_child(tcp_timer) has_child = false - var token: String = "" + var token : String = "" for value in raw_result.split(" ")[1].lstrip("/?").split("&"): - var splitted: PoolStringArray = value.split("=") + var splitted: PackedStringArray = value.split("=") if _local_provider.params.response_type in splitted[0]: token = splitted[1] break + if token == "": - emit_signal("login_failed") + login_failed.emit() peer.disconnect_from_host() tcp_server.stop() - var data : PoolByteArray = '

🔥 You can close this window now. 🔥

'.to_ascii() - peer.put_data(("HTTP/1.1 200 OK\n").to_ascii()) - peer.put_data(("Server: Godot Firebase SDK\n").to_ascii()) - peer.put_data(("Content-Length: %d\n" % data.size()).to_ascii()) - peer.put_data("Connection: close\n".to_ascii()) - peer.put_data(("Content-Type: text/html; charset=UTF-8\n\n").to_ascii()) + return + + var data : PackedByteArray = '

🔥 You can close this window now. 🔥

'.to_ascii_buffer() + peer.put_data(("HTTP/1.1 200 OK\n").to_ascii_buffer()) + peer.put_data(("Server: Godot Firebase SDK\n").to_ascii_buffer()) + peer.put_data(("Content-Length: %d\n" % data.size()).to_ascii_buffer()) + peer.put_data("Connection: close\n".to_ascii_buffer()) + peer.put_data(("Content-Type: text/html; charset=UTF-8\n\n").to_ascii_buffer()) peer.put_data(data) login_with_oauth(token, _local_provider) - yield(self, "login_succeeded") + await self.login_succeeded peer.disconnect_from_host() tcp_server.stop() + - -# Function used to logout of the system, this will also remove the local encrypted auth file if there is one +# Function used to logout of the system, this will also remove_at the local encrypted auth file if there is one func logout() -> void: auth = {} remove_auth() - emit_signal("logged_out") + logged_out.emit() # Function is called when requesting a manual token refresh @@ -371,13 +394,16 @@ func manual_token_refresh(auth_data): refresh_token = auth.refresh_token _needs_refresh = true _refresh_request_body.refresh_token = refresh_token - request(_refresh_request_base_url + _refresh_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_refresh_request_body)) + var err = request(_refresh_request_base_url + _refresh_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_refresh_request_body)) + if err != OK: + is_busy = false + Firebase._printerr("Error manually refreshing token: %s" % err) # This function is called whenever there is an authentication request to Firebase # On an error, this function with emit the signal 'login_failed' and print the error to the console -func _on_FirebaseAuth_request_completed(result: int, response_code: int, headers: PoolStringArray, body: PoolByteArray) -> void: - print_debug(JSON.parse(body.get_string_from_utf8()).result) +func _on_FirebaseAuth_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: + var json = Utilities.get_json_data(body) is_busy = false var res if response_code == 0: @@ -388,81 +414,82 @@ func _on_FirebaseAuth_request_completed(result: int, response_code: int, headers "code": "Connection error", "message": "Error connecting to auth service"}} else: - var bod = body.get_string_from_utf8() - var json_result = JSON.parse(bod) - if json_result.error != OK: + if json == null: Firebase._printerr("Error while parsing auth body json") - emit_signal("auth_request", ERR_PARSE_ERROR, "Error while parsing auth body json") + auth_request.emit(ERR_PARSE_ERROR, "Error while parsing auth body json") return - res = json_result.result + + res = json if response_code == HTTPClient.RESPONSE_OK: if not res.has("kind"): auth = get_clean_keys(res) match requesting: Requests.EXCHANGE_TOKEN: - emit_signal("token_exchanged", true) + token_exchanged.emit(true) begin_refresh_countdown() # Refresh token countdown - emit_signal("auth_request", 1, auth) + auth_request.emit(1, auth) else: match res.kind: RESPONSE_SIGNUP: auth = get_clean_keys(res) - emit_signal("signup_succeeded", auth) + signup_succeeded.emit(auth) begin_refresh_countdown() RESPONSE_SIGNIN, RESPONSE_ASSERTION, RESPONSE_CUSTOM_TOKEN: auth = get_clean_keys(res) - emit_signal("login_succeeded", auth) + login_succeeded.emit(auth) begin_refresh_countdown() RESPONSE_USERDATA: var userdata = FirebaseUserData.new(res.users[0]) - emit_signal("userdata_received", userdata) - emit_signal("auth_request", 1, auth) + userdata_received.emit(userdata) + auth_request.emit(1, auth) else: # error message would be INVALID_EMAIL, EMAIL_NOT_FOUND, INVALID_PASSWORD, USER_DISABLED or WEAK_PASSWORD if requesting == Requests.EXCHANGE_TOKEN: - emit_signal("token_exchanged", false) - emit_signal("login_failed", res.error, res.error_description) - emit_signal("auth_request", res.error, res.error_description) + token_exchanged.emit(false) + login_failed.emit(res.error, res.error_description) + auth_request.emit(res.error, res.error_description) else: - var sig = "signup_failed" if auth_request_type == Auth_Type.SIGNUP_EP else "login_failed" - emit_signal(sig, res.error.code, res.error.message) - emit_signal("auth_request", res.error.code, res.error.message) + var sig = signup_failed if auth_request_type == Auth_Type.SIGNUP_EP else login_failed + sig.emit(res.error.code, res.error.message) + auth_request.emit(res.error.code, res.error.message) requesting = Requests.NONE auth_request_type = Auth_Type.NONE # Function used to save the auth data provided by Firebase into an encrypted file # Note this does not work in HTML5 or UWP -func save_auth(auth: Dictionary) -> void: - var encrypted_file = File.new() - var err = encrypted_file.open_encrypted_with_pass("user://user.auth", File.WRITE, _config.apiKey) - if err != OK: - Firebase._printerr("Error Opening File. Error Code: " + String(err)) +func save_auth(auth : Dictionary) -> void: + var encrypted_file = FileAccess.open_encrypted_with_pass("user://user.auth", FileAccess.WRITE, _config.apiKey) + var err = encrypted_file == null + if err: + Firebase._printerr("Error Opening File. Error Code: " + str(FileAccess.get_open_error())) else: - encrypted_file.store_line(to_json(auth)) + encrypted_file.store_line(JSON.stringify(auth)) encrypted_file.close() # Function used to load the auth data file that has been stored locally # Note this does not work in HTML5 or UWP func load_auth() -> void: - var encrypted_file = File.new() - var err = encrypted_file.open_encrypted_with_pass("user://user.auth", File.READ, _config.apiKey) - if err != OK: - Firebase._printerr("Error Opening Firebase Auth File. Error Code: " + str(err)) - emit_signal("auth_request", err, "Error Opening Firebase Auth File.") + var encrypted_file = FileAccess.open_encrypted_with_pass("user://user.auth", FileAccess.WRITE, _config.apiKey) + var err = encrypted_file == null + if err: + Firebase._printerr("Error Opening Firebase Auth File. Error Code: " + str(FileAccess.get_open_error())) + auth_request.emit(err, "Error Opening Firebase Auth File.") else: - var encrypted_file_data = parse_json(encrypted_file.get_line()) - manual_token_refresh(encrypted_file_data) + var json = JSON.new() + var json_parse_result = json.parse(encrypted_file.get_line()) + if json_parse_result == OK: + var encrypted_file_data = json.data + manual_token_refresh(encrypted_file_data) -# Function used to remove the local encrypted auth file +# Function used to remove_at the local encrypted auth file func remove_auth() -> void: - var dir = Directory.new() - if dir.file_exists("user://user.auth"): - dir.remove("user://user.auth") + if (FileAccess.file_exists("user://user.auth")): + DirAccess.remove_absolute("user://user.auth") else: Firebase._printerr("No encrypted auth file exists") @@ -470,35 +497,40 @@ func remove_auth() -> void: # Function to check if there is an encrypted auth data file # If there is, the game will load it and refresh the token func check_auth_file() -> void: - var dir = Directory.new() - if dir.file_exists("user://user.auth"): + if (FileAccess.file_exists("user://user.auth")): # Will ensure "auth_request" emitted load_auth() else: Firebase._printerr("Encrypted Firebase Auth file does not exist") - emit_signal("auth_request", ERR_DOES_NOT_EXIST, "Encrypted Firebase Auth file does not exist") + auth_request.emit(ERR_DOES_NOT_EXIST, "Encrypted Firebase Auth file does not exist") # Function used to change the email account for the currently logged in user -func change_user_email(email: String) -> void: +func change_user_email(email : String) -> void: if _is_ready(): is_busy = true _change_email_body.email = email _change_email_body.idToken = auth.idtoken - request(_base_url + _update_account_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_change_email_body)) + var err = request(_base_url + _update_account_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_change_email_body)) + if err != OK: + is_busy = false + Firebase._printerr("Error changing user email: %s" % err) # Function used to change the password for the currently logged in user -func change_user_password(password: String) -> void: +func change_user_password(password : String) -> void: if _is_ready(): is_busy = true _change_password_body.password = password _change_password_body.idToken = auth.idtoken - request(_base_url + _update_account_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_change_password_body)) + var err = request(_base_url + _update_account_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_change_password_body)) + if err != OK: + is_busy = false + Firebase._printerr("Error changing user password: %s" % err) # User Profile handlers -func update_account(idToken: String, displayName: String, photoUrl: String, deleteAttribute: PoolStringArray, returnSecureToken: bool) -> void: +func update_account(idToken : String, displayName : String, photoUrl : String, deleteAttribute : PackedStringArray, returnSecureToken : bool) -> void: if _is_ready(): is_busy = true _update_profile_body.idToken = idToken @@ -506,7 +538,10 @@ func update_account(idToken: String, displayName: String, photoUrl: String, dele _update_profile_body.photoUrl = photoUrl _update_profile_body.deleteAttribute = deleteAttribute _update_profile_body.returnSecureToken = returnSecureToken - request(_base_url + _update_account_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_update_profile_body)) + var err = request(_base_url + _update_account_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_update_profile_body)) + if err != OK: + is_busy = false + Firebase._printerr("Error updating account: %s" % err) # Function to send a account verification email @@ -514,16 +549,22 @@ func send_account_verification_email() -> void: if _is_ready(): is_busy = true _account_verification_body.idToken = auth.idtoken - request(_base_url + _oobcode_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_account_verification_body)) + var err = request(_base_url + _oobcode_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_account_verification_body)) + if err != OK: + is_busy = false + Firebase._printerr("Error sending account verification email: %s" % err) # Function used to reset the password for a user who has forgotten in. # This will send the users account an email with a password reset link -func send_password_reset_email(email: String) -> void: +func send_password_reset_email(email : String) -> void: if _is_ready(): is_busy = true _password_reset_body.email = email - request(_base_url + _oobcode_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_password_reset_body)) + var err = request(_base_url + _oobcode_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_password_reset_body)) + if err != OK: + is_busy = false + Firebase._printerr("Error sending password reset email: %s" % err) # Function called to get all @@ -535,14 +576,20 @@ func get_user_data() -> void: is_busy = false return - request(_base_url + _userdata_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print({"idToken": auth.idtoken})) + var err = request(_base_url + _userdata_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify({"idToken":auth.idtoken})) + if err != OK: + is_busy = false + Firebase._printerr("Error getting user data: %s" % err) # Function used to delete the account of the currently authenticated user func delete_user_account() -> void: if _is_ready(): is_busy = true - request(_base_url + _delete_account_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print({"idToken": auth.idtoken})) + var err = request(_base_url + _delete_account_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify({"idToken":auth.idtoken})) + if err != OK: + is_busy = false + Firebase._printerr("Error deleting user: %s" % err) # Function is called when a new token is issued to a user. The function will yield until the token has expired, and then request a new one. @@ -559,43 +606,43 @@ func begin_refresh_countdown() -> void: if auth.has("userid"): auth["localid"] = auth.userid _needs_refresh = true - emit_signal("token_refresh_succeeded", auth) - yield(get_tree().create_timer(float(expires_in)), "timeout") + token_refresh_succeeded.emit(auth) + await get_tree().create_timer(float(expires_in)).timeout _refresh_request_body.refresh_token = refresh_token - request(_refresh_request_base_url + _refresh_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_refresh_request_body)) + var err = request(_refresh_request_base_url + _refresh_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_refresh_request_body)) + if err != OK: + is_busy = false + Firebase._printerr("Error refreshing via countdown: %s" % err) func get_token_from_url(provider: AuthProvider): var token_type: String = provider.params.response_type if provider.params.response_type == "code" else "access_token" if OS.has_feature('JavaScript'): - var token = JavaScript.eval(""" + var token = JavaScriptBridge.eval(""" var url_string = window.location.href.replaceAll('?#', '?'); var url = new URL(url_string); url.searchParams.get('"""+token_type+"""'); """) - JavaScript.eval("""window.history.pushState({}, null, location.href.split('?')[0]);""") + JavaScriptBridge.eval("""window.history.pushState({}, null, location.href.split('?')[0]);""") return token return null -func set_redirect_uri(redirect_uri: String) -> void: +func set_redirect_uri(redirect_uri : String) -> void: self._local_uri = redirect_uri - -func set_local_provider(provider: AuthProvider) -> void: +func set_local_provider(provider : AuthProvider) -> void: self._local_provider = provider - # This function is used to make all keys lowercase -# This is only used to cut down on processing errors from Firebase +# This is only used to cut down checked processing errors from Firebase # This is due to Google have inconsistencies in the API that we are trying to fix -func get_clean_keys(auth_result: Dictionary) -> Dictionary: +func get_clean_keys(auth_result : Dictionary) -> Dictionary: var cleaned = {} for key in auth_result.keys(): cleaned[key.replace("_", "").to_lower()] = auth_result[key] return cleaned - # -------------------- # PROVIDERS # -------------------- @@ -603,14 +650,11 @@ func get_clean_keys(auth_result: Dictionary) -> Dictionary: func get_GoogleProvider() -> GoogleProvider: return GoogleProvider.new(_config.clientId, _config.clientSecret) - func get_FacebookProvider() -> FacebookProvider: return FacebookProvider.new(_config.auth_providers.facebook_id, _config.auth_providers.facebook_secret) - func get_GitHubProvider() -> GitHubProvider: return GitHubProvider.new(_config.auth_providers.github_id, _config.auth_providers.github_secret) - func get_TwitterProvider() -> TwitterProvider: return TwitterProvider.new(_config.auth_providers.twitter_id, _config.auth_providers.twitter_secret) diff --git a/addons/godot-firebase/auth/auth_provider.gd b/addons/godot-firebase/auth/auth_provider.gd index 2f0fd0b..288cf2d 100644 --- a/addons/godot-firebase/auth/auth_provider.gd +++ b/addons/godot-firebase/auth/auth_provider.gd @@ -1,7 +1,6 @@ -tool +@tool class_name AuthProvider -extends Reference - +extends RefCounted var redirect_uri: String = "" var access_token_uri: String = "" @@ -20,18 +19,14 @@ var should_exchange: bool = false func set_client_id(client_id: String) -> void: self.params.client_id = client_id - func set_client_secret(client_secret: String) -> void: self.client_secret = client_secret - func get_client_id() -> String: return self.params.client_id - func get_client_secret() -> String: return self.client_secret - func get_oauth_params() -> String: return "" diff --git a/addons/godot-firebase/auth/providers/facebook.gd b/addons/godot-firebase/auth/providers/facebook.gd index b5e59fb..9dc4b1d 100644 --- a/addons/godot-firebase/auth/providers/facebook.gd +++ b/addons/godot-firebase/auth/providers/facebook.gd @@ -1,20 +1,21 @@ -class_name FacebookProvider +class_name FacebookProvider extends AuthProvider - -func _init(client_id: String, client_secret: String) -> void: +func _init(client_id: String,client_secret: String): randomize() set_client_id(client_id) set_client_secret(client_secret) - + self.redirect_uri = "https://www.facebook.com/v13.0/dialog/oauth?" self.access_token_uri = "https://graph.facebook.com/v13.0/oauth/access_token" self.provider_id = "facebook.com" self.params.scope = "public_profile" - self.params.state = str(rand_range(0, 1)) + self.params.state = str(randf_range(0, 1)) if OS.get_name() == "HTML5": self.should_exchange = false self.params.response_type = "token" else: self.should_exchange = true self.params.response_type = "code" + + diff --git a/addons/godot-firebase/auth/providers/github.gd b/addons/godot-firebase/auth/providers/github.gd index 0c77272..bab073a 100644 --- a/addons/godot-firebase/auth/providers/github.gd +++ b/addons/godot-firebase/auth/providers/github.gd @@ -1,8 +1,7 @@ -class_name GitHubProvider +class_name GitHubProvider extends AuthProvider - -func _init(client_id: String, client_secret: String) -> void: +func _init(client_id: String,client_secret: String): randomize() set_client_id(client_id) set_client_secret(client_secret) @@ -11,5 +10,5 @@ func _init(client_id: String, client_secret: String) -> void: self.access_token_uri = "https://github.com/login/oauth/access_token" self.provider_id = "github.com" self.params.scope = "user:read" - self.params.state = str(rand_range(0, 1)) + self.params.state = str(randf_range(0, 1)) self.params.response_type = "code" diff --git a/addons/godot-firebase/auth/providers/google.gd b/addons/godot-firebase/auth/providers/google.gd index 15ac362..2ea88cf 100644 --- a/addons/godot-firebase/auth/providers/google.gd +++ b/addons/godot-firebase/auth/providers/google.gd @@ -1,8 +1,7 @@ class_name GoogleProvider extends AuthProvider - -func _init(client_id: String, client_secret: String) -> void: +func _init(client_id: String,client_secret: String): set_client_id(client_id) set_client_secret(client_secret) self.should_exchange = true diff --git a/addons/godot-firebase/auth/providers/twitter.gd b/addons/godot-firebase/auth/providers/twitter.gd index b7675c9..1ec11cf 100644 --- a/addons/godot-firebase/auth/providers/twitter.gd +++ b/addons/godot-firebase/auth/providers/twitter.gd @@ -1,29 +1,28 @@ -class_name TwitterProvider +class_name TwitterProvider extends AuthProvider - var request_token_endpoint: String = "https://api.twitter.com/oauth/access_token?oauth_callback=" var oauth_header: Dictionary = { - oauth_callback = "", - oauth_consumer_key = "", - oauth_nonce = "", - oauth_signature = "", - oauth_signature_method = "HMAC-SHA1", - oauth_timestamp = "", - oauth_version = "1.0" + oauth_callback="", + oauth_consumer_key="", + oauth_nonce="", + oauth_signature="", + oauth_signature_method="HMAC-SHA1", + oauth_timestamp="", + oauth_version="1.0" } - -func _init(client_id: String, client_secret: String) -> void: +func _init(client_id: String,client_secret: String): randomize() set_client_id(client_id) set_client_secret(client_secret) - + self.oauth_header.oauth_consumer_key = client_id - self.oauth_header.oauth_nonce = OS.get_ticks_usec() - self.oauth_header.oauth_timestamp = OS.get_ticks_msec() - + self.oauth_header.oauth_nonce = Time.get_ticks_usec() + self.oauth_header.oauth_timestamp = Time.get_ticks_msec() + + self.should_exchange = true self.redirect_uri = "https://twitter.com/i/oauth2/authorize?" self.access_token_uri = "https://api.twitter.com/2/oauth2/token" @@ -31,11 +30,10 @@ func _init(client_id: String, client_secret: String) -> void: self.params.redirect_type = "redirect_uri" self.params.response_type = "code" self.params.scope = "users.read" - self.params.state = str(rand_range(0, 1)) - + self.params.state = str(randf_range(0, 1)) func get_oauth_params() -> String: - var params: PoolStringArray = [] + var params: PackedStringArray = [] for key in self.oauth.keys(): - params.append(key + "=" + self.oauth.get(key)) - return params.join("&") + params.append(key+"="+self.oauth.get(key)) + return "&".join(params) diff --git a/addons/godot-firebase/auth/user_data.gd b/addons/godot-firebase/auth/user_data.gd index 62e0d57..c76e515 100644 --- a/addons/godot-firebase/auth/user_data.gd +++ b/addons/godot-firebase/auth/user_data.gd @@ -2,25 +2,23 @@ ## @meta-version 2.3 ## Authentication user data. ## Documentation TODO. -tool +@tool class_name FirebaseUserData -extends Reference +extends RefCounted +var local_id : String = "" # The uid of the current user. +var email : String = "" +var email_verified := false # Whether or not the account's email has been verified. +var password_updated_at : float = 0 # The timestamp, in milliseconds, that the account password was last changed. +var last_login_at : float = 0 # The timestamp, in milliseconds, that the account last logged in at. +var created_at : float = 0 # The timestamp, in milliseconds, that the account was created at. +var provider_user_info : Array = [] -var local_id: String = "" # The uid of the current user. -var email: String = "" -var email_verified := false # Whether or not the account's email has been verified. -var password_updated_at: float = 0 # The timestamp, in milliseconds, that the account password was last changed. -var last_login_at: float = 0 # The timestamp, in milliseconds, that the account last logged in at. -var created_at: float = 0 # The timestamp, in milliseconds, that the account was created at. -var provider_user_info: Array = [] +var provider_id : String = "" +var display_name : String = "" +var photo_url : String = "" -var provider_id: String = "" -var display_name: String = "" -var photo_url: String = "" - - -func _init(p_userdata: Dictionary) -> void: +func _init(p_userdata : Dictionary): local_id = p_userdata.get("localId", "") email = p_userdata.get("email", "") email_verified = p_userdata.get("emailVerified", false) @@ -29,16 +27,14 @@ func _init(p_userdata: Dictionary) -> void: password_updated_at = float(p_userdata.get("passwordUpdatedAt", 0)) display_name = p_userdata.get("displayName", "") provider_user_info = p_userdata.get("providerUserInfo", []) - if not provider_user_info.empty(): + if not provider_user_info.is_empty(): provider_id = provider_user_info[0].get("providerId", "") photo_url = provider_user_info[0].get("photoUrl", "") display_name = provider_user_info[0].get("displayName", "") - func as_text() -> String: return _to_string() - func _to_string() -> String: var txt = "local_id : %s\n" % local_id txt += "email : %s\n" % email diff --git a/addons/godot-firebase/database/database.gd b/addons/godot-firebase/database/database.gd index de736be..430b33a 100644 --- a/addons/godot-firebase/database/database.gd +++ b/addons/godot-firebase/database/database.gd @@ -2,53 +2,49 @@ ## @meta-version 2.2 ## The Realtime Database API for Firebase. ## Documentation TODO. -tool +@tool class_name FirebaseDatabase extends Node +var _base_url : String = "" -var _base_url: String = "" +var _config : Dictionary = {} -var _config: Dictionary = {} +var _auth : Dictionary = {} -var _auth: Dictionary = {} - - -func _set_config(config_json: Dictionary) -> void: +func _set_config(config_json : Dictionary) -> void: _config = config_json _check_emulating() -func _check_emulating() -> void: +func _check_emulating() -> void : ## Check emulating if not Firebase.emulating: _base_url = _config.databaseURL else: - var port: String = _config.emulators.ports.realtimeDatabase + var port : String = _config.emulators.ports.realtimeDatabase if port == "": Firebase._printerr("You are in 'emulated' mode, but the port for Realtime Database has not been configured.") else: _base_url = "http://localhost" -func _on_FirebaseAuth_login_succeeded(auth_result: Dictionary) -> void: - _auth = auth_result - -func _on_FirebaseAuth_token_refresh_succeeded(auth_result: Dictionary) -> void: +func _on_FirebaseAuth_login_succeeded(auth_result : Dictionary) -> void: _auth = auth_result +func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void: + _auth = auth_result func _on_FirebaseAuth_logout() -> void: _auth = {} - -func get_database_reference(path: String, filter: Dictionary = {}) -> FirebaseDatabaseReference: - var firebase_reference: FirebaseDatabaseReference = FirebaseDatabaseReference.new() - var pusher: HTTPRequest = HTTPRequest.new() - var listener: Node = Node.new() +func get_database_reference(path : String, filter : Dictionary = {}) -> FirebaseDatabaseReference: + var firebase_reference : FirebaseDatabaseReference = FirebaseDatabaseReference.new() + var pusher : HTTPRequest = HTTPRequest.new() + var listener : Node = Node.new() listener.set_script(load("res://addons/http-sse-client/HTTPSSEClient.gd")) - var store: FirebaseDatabaseStore = FirebaseDatabaseStore.new() + var store : FirebaseDatabaseStore = FirebaseDatabaseStore.new() firebase_reference.set_db_path(path, filter) firebase_reference.set_auth_and_config(_auth, _config) firebase_reference.set_pusher(pusher) diff --git a/addons/godot-firebase/database/database_store.gd b/addons/godot-firebase/database/database_store.gd index 07b6753..e6ea028 100644 --- a/addons/godot-firebase/database/database_store.gd +++ b/addons/godot-firebase/database/database_store.gd @@ -1,8 +1,8 @@ ## @meta-authors TODO ## @meta-version 2.2 ## Data structure that holds the currently-known data at a given path (a.k.a. reference) in a Firebase Realtime Database. -## Can process both puts and patches into the data based on realtime events received from the service. -tool +## Can process both puts and patches into the data based checked realtime events received from the service. +@tool class_name FirebaseDatabaseStore extends Node @@ -54,14 +54,14 @@ func _update_data(path: String, payload, patch: bool) -> void: # Traverse the path. # var dict = _data - var keys = PoolStringArray([_ROOT]) + var keys = PackedStringArray([_ROOT]) keys.append_array(path.split(_DELIMITER, false)) var final_key_idx = (keys.size() - 1) var final_key = (keys[final_key_idx]) - keys.remove(final_key_idx) + keys.remove_at(final_key_idx) for key in keys: if !dict.has(key): diff --git a/addons/godot-firebase/database/reference.gd b/addons/godot-firebase/database/reference.gd index 464e5c1..0c485cd 100644 --- a/addons/godot-firebase/database/reference.gd +++ b/addons/godot-firebase/database/reference.gd @@ -2,11 +2,10 @@ ## @meta-version 2.3 ## A reference to a location in the Realtime Database. ## Documentation TODO. -tool +@tool class_name FirebaseDatabaseReference extends Node - signal new_data_update(data) signal patch_data_update(data) signal delete_data_update(data) @@ -14,66 +13,62 @@ signal delete_data_update(data) signal push_successful() signal push_failed() -const ORDER_BY: String = "orderBy" -const LIMIT_TO_FIRST: String = "limitToFirst" -const LIMIT_TO_LAST: String = "limitToLast" -const START_AT: String = "startAt" -const END_AT: String = "endAt" -const EQUAL_TO: String = "equalTo" - -var _pusher: HTTPRequest -var _listener: Node -var _store: FirebaseDatabaseStore -var _auth: Dictionary -var _config: Dictionary -var _filter_query: Dictionary -var _db_path: String -var _cached_filter: String -var _push_queue: Array = [] -var _update_queue: Array = [] -var _delete_queue: Array = [] -var _can_connect_to_host: bool = false - -const _put_tag: String = "put" -const _patch_tag: String = "patch" -const _delete_tag: String = "delete" -const _separator: String = "/" -const _json_list_tag: String = ".json" -const _query_tag: String = "?" -const _auth_tag: String = "auth=" -const _accept_header: String = "accept: text/event-stream" -const _auth_variable_begin: String = "[" -const _auth_variable_end: String = "]" -const _filter_tag: String = "&" -const _escaped_quote: String = '"' -const _equal_tag: String = "=" -const _key_filter_tag: String = "$key" - -var _headers: PoolStringArray = [] - - -func set_db_path(path: String, filter_query_dict: Dictionary) -> void: +const ORDER_BY : String = "orderBy" +const LIMIT_TO_FIRST : String = "limitToFirst" +const LIMIT_TO_LAST : String = "limitToLast" +const START_AT : String = "startAt" +const END_AT : String = "endAt" +const EQUAL_TO : String = "equalTo" + +var _pusher : HTTPRequest +var _listener : Node +var _store : FirebaseDatabaseStore +var _auth : Dictionary +var _config : Dictionary +var _filter_query : Dictionary +var _db_path : String +var _cached_filter : String +var _push_queue : Array = [] +var _update_queue : Array = [] +var _delete_queue : Array = [] +var _can_connect_to_host : bool = false + +const _put_tag : String = "put" +const _patch_tag : String = "patch" +const _delete_tag : String = "delete" +const _separator : String = "/" +const _json_list_tag : String = ".json" +const _query_tag : String = "?" +const _auth_tag : String = "auth=" +const _accept_header : String = "accept: text/event-stream" +const _auth_variable_begin : String = "[" +const _auth_variable_end : String = "]" +const _filter_tag : String = "&" +const _escaped_quote : String = '"' +const _equal_tag : String = "=" +const _key_filter_tag : String = "$key" + +var _headers : PackedStringArray = [] + +func set_db_path(path : String, filter_query_dict : Dictionary) -> void: _db_path = path _filter_query = filter_query_dict - -func set_auth_and_config(auth_ref: Dictionary, config_ref: Dictionary) -> void: +func set_auth_and_config(auth_ref : Dictionary, config_ref : Dictionary) -> void: _auth = auth_ref _config = config_ref - -func set_pusher(pusher_ref: HTTPRequest) -> void: - if not _pusher: +func set_pusher(pusher_ref : HTTPRequest) -> void: + if !_pusher: _pusher = pusher_ref add_child(_pusher) - _pusher.connect("request_completed", self, "on_push_request_complete") - + _pusher.request_completed.connect(on_push_request_complete) -func set_listener(listener_ref: Node) -> void: - if not _listener: +func set_listener(listener_ref : Node) -> void: + if !_listener: _listener = listener_ref add_child(_listener) - _listener.connect("new_sse_event", self, "on_new_sse_event") + _listener.new_sse_event.connect(on_new_sse_event) var base_url = _get_list_url(false).trim_suffix(_separator) var extended_url = _separator + _db_path + _get_remaining_path(false) var port = -1 @@ -81,8 +76,7 @@ func set_listener(listener_ref: Node) -> void: port = int(_config.emulators.ports.realtimeDatabase) _listener.connect_to_host(base_url, extended_url, port) - -func on_new_sse_event(headers: Dictionary, event: String, data: Dictionary) -> void: +func on_new_sse_event(headers : Dictionary, event : String, data : Dictionary) -> void: if data: var command = event if command and command != "keep-alive": @@ -90,76 +84,71 @@ func on_new_sse_event(headers: Dictionary, event: String, data: Dictionary) -> v if command == _put_tag: if data.path == _separator and data.data and data.data.keys().size() > 0: for key in data.data.keys(): - emit_signal("new_data_update", FirebaseResource.new(_separator + key, data.data[key])) + new_data_update.emit(FirebaseResource.new(_separator + key, data.data[key])) elif data.path != _separator: - emit_signal("new_data_update", FirebaseResource.new(data.path, data.data)) + new_data_update.emit(FirebaseResource.new(data.path, data.data)) elif command == _patch_tag: - emit_signal("patch_data_update", FirebaseResource.new(data.path, data.data)) + patch_data_update.emit(FirebaseResource.new(data.path, data.data)) elif command == _delete_tag: - emit_signal("delete_data_update", FirebaseResource.new(data.path, data.data)) + delete_data_update.emit(FirebaseResource.new(data.path, data.data)) pass - -func set_store(store_ref: FirebaseDatabaseStore) -> void: - if not _store: +func set_store(store_ref : FirebaseDatabaseStore) -> void: + if !_store: _store = store_ref add_child(_store) - -func update(path: String, data: Dictionary) -> void: +func update(path : String, data : Dictionary) -> void: path = path.strip_edges(true, true) if path == _separator: path = "" - var to_update = JSON.print(data) + var to_update = JSON.stringify(data) var status = _pusher.get_http_client_status() - if status == HTTPClient.STATUS_DISCONNECTED or status != HTTPClient.STATUS_REQUESTING: - var resolved_path = _get_list_url() + _db_path + "/" + path + _get_remaining_path() + if status == HTTPClient.STATUS_DISCONNECTED || status != HTTPClient.STATUS_REQUESTING: + var resolved_path = (_get_list_url() + _db_path + "/" + path + _get_remaining_path()) - _pusher.request(resolved_path, _headers, true, HTTPClient.METHOD_PATCH, to_update) + _pusher.request(resolved_path, _headers, HTTPClient.METHOD_PATCH, to_update) else: _update_queue.append({"path": path, "data": data}) - -func push(data: Dictionary) -> void: - var to_push = JSON.print(data) +func push(data : Dictionary) -> void: + var to_push = JSON.stringify(data) if _pusher.get_http_client_status() == HTTPClient.STATUS_DISCONNECTED: - _pusher.request(_get_list_url() + _db_path + _get_remaining_path(), _headers, true, HTTPClient.METHOD_POST, to_push) + _pusher.request(_get_list_url() + _db_path + _get_remaining_path(), _headers, HTTPClient.METHOD_POST, to_push) else: _push_queue.append(data) - -func delete(reference: String) -> void: +func delete(reference : String) -> void: if _pusher.get_http_client_status() == HTTPClient.STATUS_DISCONNECTED: - _pusher.request(_get_list_url() + _db_path + _separator + reference + _get_remaining_path(), _headers, true, HTTPClient.METHOD_DELETE, "") + _pusher.request(_get_list_url() + _db_path + _separator + reference + _get_remaining_path(), _headers, HTTPClient.METHOD_DELETE, "") else: _delete_queue.append(reference) - -## Returns a deep copy of the current local copy of the data stored at this reference in the Firebase -## Realtime Database. +# +# Returns a deep copy of the current local copy of the data stored at this reference in the Firebase +# Realtime Database. +# func get_data() -> Dictionary: if _store == null: - return {} + return { } return _store.get_data() - -func _get_remaining_path(is_push: bool = true) -> String: +func _get_remaining_path(is_push : bool = true) -> String: var remaining_path = "" - if not _filter_query or is_push: + if _filter_query_empty() or is_push: remaining_path = _json_list_tag + _query_tag + _auth_tag + Firebase.Auth.auth.idtoken else: remaining_path = _json_list_tag + _query_tag + _get_filter() + _filter_tag + _auth_tag + Firebase.Auth.auth.idtoken if Firebase.emulating: - remaining_path += "&ns=" + _config.projectId + "-default-rtdb" + remaining_path += "&ns="+_config.projectId+"-default-rtdb" return remaining_path - -func _get_list_url(with_port: bool = true) -> String: +func _get_list_url(with_port:bool = true) -> String: var url = Firebase.Database._base_url.trim_suffix(_separator) if with_port and Firebase.emulating: url += ":" + _config.emulators.ports.realtimeDatabase @@ -167,10 +156,10 @@ func _get_list_url(with_port: bool = true) -> String: func _get_filter(): - if not _filter_query: + if _filter_query_empty(): return "" # At the moment, this means you can't dynamically change your filter; I think it's okay to specify that in the rules. - if not _cached_filter: + if _cached_filter != "": _cached_filter = "" if _filter_query.has(ORDER_BY): _cached_filter += ORDER_BY + _equal_tag + _escaped_quote + _filter_query[ORDER_BY] + _escaped_quote @@ -182,10 +171,14 @@ func _get_filter(): return _cached_filter +func _filter_query_empty() -> bool: + return _filter_query == null or _filter_query.is_empty() -## Appropriately updates the current local copy of the data stored at this reference in the Firebase -## Realtime Database. -func _route_data(command: String, path: String, data) -> void: +# +# Appropriately updates the current local copy of the data stored at this reference in the Firebase +# Realtime Database. +# +func _route_data(command : String, path : String, data) -> void: if command == _put_tag: _store.put(path, data) elif command == _patch_tag: @@ -193,19 +186,21 @@ func _route_data(command: String, path: String, data) -> void: elif command == _delete_tag: _store.delete(path, data) - -func on_push_request_complete(result: int, response_code: int, headers: PoolStringArray, body: PoolByteArray) -> void: +func on_push_request_complete(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: if response_code == HTTPClient.RESPONSE_OK: - emit_signal("push_successful") + push_successful.emit() else: - emit_signal("push_failed") + push_failed.emit() + # handle queued operations if _push_queue.size() > 0: push(_push_queue.pop_front()) return + if _update_queue.size() > 0: var e = _update_queue.pop_front() - update(e["path"], e["data"]) + update(e['path'], e['data']) return + if _delete_queue.size() > 0: delete(_delete_queue.pop_front()) diff --git a/addons/godot-firebase/database/resource.gd b/addons/godot-firebase/database/resource.gd index 9b7e82d..ff5b985 100644 --- a/addons/godot-firebase/database/resource.gd +++ b/addons/godot-firebase/database/resource.gd @@ -1,19 +1,16 @@ ## @meta-authors SIsilicon, fenix-hub ## @meta-version 2.2 ## A generic resource used by Firebase Database. -tool +@tool class_name FirebaseResource extends Resource - -var key: String +var key : String var data - -func _init(key: String, data) -> void: +func _init(key : String,data): self.key = key.lstrip("/") self.data = data - func _to_string(): return "{ key:{key}, data:{data} }".format({key = key, data = data}) diff --git a/addons/godot-firebase/dynamiclinks/dynamiclinks.gd b/addons/godot-firebase/dynamiclinks/dynamiclinks.gd index d749458..c23e86c 100644 --- a/addons/godot-firebase/dynamiclinks/dynamiclinks.gd +++ b/addons/godot-firebase/dynamiclinks/dynamiclinks.gd @@ -3,56 +3,54 @@ ## @meta-version 1.1 ## The dynamic links API for Firebase ## Documentation TODO. -tool +@tool class_name FirebaseDynamicLinks extends Node - signal dynamic_link_generated(link_result) signal generate_dynamic_link_error(error) -const _AUTHORIZATION_HEADER: String = "Authorization: Bearer " -const _API_VERSION: String = "v1" +const _AUTHORIZATION_HEADER : String = "Authorization: Bearer " +const _API_VERSION : String = "v1" -var request: int = -1 +var request : int = -1 -var _base_url: String = "" +var _base_url : String = "" -var _config: Dictionary = {} +var _config : Dictionary = {} -var _auth: Dictionary -var _request_list_node: HTTPRequest +var _auth : Dictionary +var _request_list_node : HTTPRequest -var _headers: PoolStringArray = [] +var _headers : PackedStringArray = [] enum Requests { NONE = -1, GENERATE -} - + } -func _set_config(config_json: Dictionary) -> void: +func _set_config(config_json : Dictionary) -> void: _config = config_json _request_list_node = HTTPRequest.new() - _request_list_node.connect("request_completed", self, "_on_request_completed") + _request_list_node.request_completed.connect(_on_request_completed) add_child(_request_list_node) _check_emulating() -func _check_emulating() -> void: +func _check_emulating() -> void : ## Check emulating if not Firebase.emulating: _base_url = "https://firebasedynamiclinks.googleapis.com/v1/shortLinks?key=%s" _base_url %= _config.apiKey else: - var port: String = _config.emulators.ports.dynamicLinks + var port : String = _config.emulators.ports.dynamicLinks if port == "": Firebase._printerr("You are in 'emulated' mode, but the port for Dynamic Links has not been configured.") else: - _base_url = "http://localhost:{port}/{version}/".format({version = _API_VERSION, port = port}) + _base_url = "http://localhost:{port}/{version}/".format({ version = _API_VERSION, port = port }) -var _link_request_body: Dictionary = { +var _link_request_body : Dictionary = { "dynamicLinkInfo": { "domainUriPrefix": "", "link": "", @@ -62,20 +60,19 @@ var _link_request_body: Dictionary = { "iosInfo": { "iosBundleId": "" } - }, + }, "suffix": { "option": "" } } - ## @args log_link, APN, IBI, is_unguessable ## This function is used to generate a dynamic link using the Firebase REST API ## It will return a JSON with the shortened link -func generate_dynamic_link(long_link: String, APN: String, IBI: String, is_unguessable: bool) -> void: +func generate_dynamic_link(long_link : String, APN : String, IBI : String, is_unguessable : bool) -> void: if not _config.domainUriPrefix or _config.domainUriPrefix == "": - emit_signal("generate_dynamic_link_error", "You're missing the domainUriPrefix in config file! Error!") - Firebase._printerr("You're missing the domainUriPrefix in config file! Error!") + generate_dynamic_link_error.emit("Error: Missing domainUriPrefix in config file. Parameter is required.") + Firebase._printerr("Error: Missing domainUriPrefix in config file. Parameter is required.") return request = Requests.GENERATE @@ -87,28 +84,25 @@ func generate_dynamic_link(long_link: String, APN: String, IBI: String, is_ungue _link_request_body.suffix.option = "UNGUESSABLE" else: _link_request_body.suffix.option = "SHORT" - _request_list_node.request(_base_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_link_request_body)) - - -func _on_request_completed(result: int, response_code: int, headers: PoolStringArray, body: PoolByteArray) -> void: - var result_body = JSON.parse(body.get_string_from_utf8()) - if result_body.error: - emit_signal("generate_dynamic_link_error", result_body.error_string) - return + _request_list_node.request(_base_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_link_request_body)) + +func _on_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: + var json = JSON.new() + var json_parse_result = json.parse(body.get_string_from_utf8()) + if json_parse_result == OK: + var result_body = json.data.result # Check this + dynamic_link_generated.emit(result_body.shortLink) else: - result_body = result_body.result - - emit_signal("dynamic_link_generated", result_body.shortLink) + generate_dynamic_link_error.emit(json.get_error_message()) + # This used to return immediately when above, but it should still clear the request, so removing it + request = Requests.NONE - -func _on_FirebaseAuth_login_succeeded(auth_result: Dictionary) -> void: +func _on_FirebaseAuth_login_succeeded(auth_result : Dictionary) -> void: _auth = auth_result - -func _on_FirebaseAuth_token_refresh_succeeded(auth_result: Dictionary) -> void: +func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void: _auth = auth_result - func _on_FirebaseAuth_logout() -> void: _auth = {} diff --git a/addons/godot-firebase/firebase/firebase.gd b/addons/godot-firebase/firebase/firebase.gd index e8cd0ec..8c106b3 100644 --- a/addons/godot-firebase/firebase/firebase.gd +++ b/addons/godot-firebase/firebase/firebase.gd @@ -8,44 +8,43 @@ ## - [code]Storage[/code]: Gives access to Cloud Storage; perfect for storing files like images and other assets. ## ## @tutorial https://github.com/GodotNuts/GodotFirebase/wiki -tool +@tool extends Node - -const _ENVIRONMENT_VARIABLES: String = "firebase/environment_variables" -const _EMULATORS_PORTS: String = "firebase/emulators/ports" -const _AUTH_PROVIDERS: String = "firebase/auth_providers" +const _ENVIRONMENT_VARIABLES : String = "firebase/environment_variables" +const _EMULATORS_PORTS : String = "firebase/emulators/ports" +const _AUTH_PROVIDERS : String = "firebase/auth_providers" ## @type FirebaseAuth ## The Firebase Authentication API. -onready var Auth: FirebaseAuth = $Auth +@onready var Auth := $Auth ## @type FirebaseFirestore ## The Firebase Firestore API. -onready var Firestore: FirebaseFirestore = $Firestore +@onready var Firestore := $Firestore ## @type FirebaseDatabase ## The Firebase Realtime Database API. -onready var Database: FirebaseDatabase = $Database +@onready var Database := $Database ## @type FirebaseStorage ## The Firebase Storage API. -onready var Storage: FirebaseStorage = $Storage +@onready var Storage := $Storage ## @type FirebaseDynamicLinks ## The Firebase Dynamic Links API. -onready var DynamicLinks: FirebaseDynamicLinks = $DynamicLinks +@onready var DynamicLinks := $DynamicLinks ## @type FirebaseFunctions ## The Firebase Cloud Functions API -onready var Functions: FirebaseFunctions = $Functions +@onready var Functions := $Functions -export var emulating: bool = false +@export var emulating : bool = false # Configuration used by all files in this project # These values can be found in your Firebase Project -# See the README on Github for how to access -var _config: Dictionary = { +# See the README checked Github for how to access +var _config : Dictionary = { "apiKey": "", "authDomain": "", "databaseURL": "", @@ -55,43 +54,41 @@ var _config: Dictionary = { "appId": "", "measurementId": "", "clientId": "", - "clientSecret": "", - "domainUriPrefix": "", - "functionsGeoZone": "", - "cacheLocation": "user://.firebase_cache", + "clientSecret" : "", + "domainUriPrefix" : "", + "functionsGeoZone" : "", + "cacheLocation":"user://.firebase_cache", "emulators": { - "ports": { - "authentication": "", - "firestore": "", - "realtimeDatabase": "", - "functions": "", - "storage": "", - "dynamicLinks": "" + "ports" : { + "authentication" : "", + "firestore" : "", + "realtimeDatabase" : "", + "functions" : "", + "storage" : "", + "dynamicLinks" : "" } }, - "workarounds": { + "workarounds":{ "database_connection_closed_issue": false, # fixes https://github.com/firebase/firebase-tools/issues/3329 }, "auth_providers": { - "facebook_id": "", - "facebook_secret": "", - "github_id": "", - "github_secret": "", - "twitter_id": "", - "twitter_secret": "" + "facebook_id":"", + "facebook_secret":"", + "github_id":"", + "github_secret":"", + "twitter_id":"", + "twitter_secret":"" } } - func _ready() -> void: _load_config() -func set_emulated(emulating: bool = true) -> void: +func set_emulated(emulating : bool = true) -> void: self.emulating = emulating _check_emulating() - func _check_emulating() -> void: if emulating: print("[Firebase] You are now in 'emulated' mode: the services you are using will try to connect to your local emulators, if available.") @@ -99,23 +96,21 @@ func _check_emulating() -> void: if module.has_method("_check_emulating"): module._check_emulating() - func _load_config() -> void: - if _config.apiKey != "" and _config.authDomain != "": - pass - else: + if not (_config.apiKey != "" and _config.authDomain != ""): var env = ConfigFile.new() var err = env.load("res://addons/godot-firebase/.env") if err == OK: for key in _config.keys(): - if key == "emulators": - for port in _config[key]["ports"].keys(): - _config[key]["ports"][port] = env.get_value(_EMULATORS_PORTS, port, "") - if key == "auth_providers": - for provider in _config[key].keys(): - _config[key][provider] = env.get_value(_AUTH_PROVIDERS, provider) + var config_value = _config[key] + if key == "emulators" and config_value.has("ports"): + for port in config_value["ports"].keys(): + config_value["ports"][port] = env.get_value(_EMULATORS_PORTS, port, "") + if key == "auth_providers" and config_value.has("auth_providers"): + for provider in config_value.keys(): + config_value[provider] = env.get_value(_AUTH_PROVIDERS, provider) else: - var value: String = env.get_value(_ENVIRONMENT_VARIABLES, key, "") + var value : String = env.get_value(_ENVIRONMENT_VARIABLES, key, "") if value == "": _print("The value for `%s` is not configured. If you are not planning to use it, ignore this message." % key) else: @@ -125,23 +120,21 @@ func _load_config() -> void: _setup_modules() - func _setup_modules() -> void: for module in get_children(): module._set_config(_config) if not module.has_method("_on_FirebaseAuth_login_succeeded"): continue - Auth.connect("login_succeeded", module, "_on_FirebaseAuth_login_succeeded") - Auth.connect("signup_succeeded", module, "_on_FirebaseAuth_login_succeeded") - Auth.connect("token_refresh_succeeded", module, "_on_FirebaseAuth_token_refresh_succeeded") - Auth.connect("logged_out", module, "_on_FirebaseAuth_logout") + Auth.login_succeeded.connect(module._on_FirebaseAuth_login_succeeded) + Auth.signup_succeeded.connect(module._on_FirebaseAuth_login_succeeded) + Auth.token_refresh_succeeded.connect(module._on_FirebaseAuth_token_refresh_succeeded) + Auth.logged_out.connect(module._on_FirebaseAuth_logout) # ------------- -func _printerr(error: String) -> void: - printerr("[Firebase Error] >> " + error) - +func _printerr(error : String) -> void: + printerr("[Firebase Error] >> "+error) -func _print(msg: String) -> void: - print("[Firebase] >> " + msg) +func _print(msg : String) -> void: + print("[Firebase] >> "+msg) diff --git a/addons/godot-firebase/firebase/firebase.tscn b/addons/godot-firebase/firebase/firebase.tscn index 6b86131..3bc100d 100644 --- a/addons/godot-firebase/firebase/firebase.tscn +++ b/addons/godot-firebase/firebase/firebase.tscn @@ -9,7 +9,7 @@ [ext_resource path="res://addons/godot-firebase/functions/functions.gd" type="Script" id=7] [node name="Firebase" type="Node"] -pause_mode = 2 +process_mode = 2 script = ExtResource( 3 ) [node name="Auth" type="HTTPRequest" parent="."] diff --git a/addons/godot-firebase/firestore/firestore.gd b/addons/godot-firebase/firestore/firestore.gd index 156a645..4fe44d6 100644 --- a/addons/godot-firebase/firestore/firestore.gd +++ b/addons/godot-firebase/firestore/firestore.gd @@ -12,7 +12,7 @@ ## (source: [url=https://firebase.google.com/docs/firestore]Firestore[/url]) ## ## @tutorial https://github.com/GodotNuts/GodotFirebase/wiki/Firestore -tool +@tool class_name FirebaseFirestore extends Node @@ -31,8 +31,8 @@ signal task_error(code,status,message) enum Requests { NONE = -1, ## Firestore is not processing any request. - LIST, ## Firestore is processing a [code]list()[/code] request on a collection. - QUERY ## Firestore is processing a [code]query()[/code] request on a collection. + LIST, ## Firestore is processing a [code]list()[/code] request checked a collection. + QUERY ## Firestore is processing a [code]query()[/code] request checked a collection. } # TODO: Implement cache size limit @@ -56,7 +56,7 @@ var persistence_enabled : bool = true ## Whether an internet connection can be used. ## @default true -var networking: bool = true setget set_networking +var networking: bool = true : set = set_networking ## A Dictionary containing all collections currently referenced. ## @type Dictionary @@ -83,7 +83,7 @@ var _current_query : FirestoreQuery var _http_request_pool := [] -var _offline: bool = false setget _set_offline +var _offline: bool = false : set = _set_offline func _ready() -> void: pass @@ -95,14 +95,15 @@ func _process(delta : float) -> void: var lifetime: float = request.get_meta("lifetime") + delta if lifetime > _MAX_POOLED_REQUEST_AGE: request.queue_free() - _http_request_pool.remove(i) + _http_request_pool.remove_at(i) + continue # Just to skip set_meta on a queue_freed request request.set_meta("lifetime", lifetime) ## Returns a reference collection by its [i]path[/i]. ## ## The returned object will be of [code]FirestoreCollection[/code] type. -## If saved into a variable, it can be used to issue requests on the collection itself. +## If saved into a variable, it can be used to issue requests checked the collection itself. ## @args path ## @return FirestoreCollection func collection(path : String) -> FirestoreCollection: @@ -120,19 +121,19 @@ func collection(path : String) -> FirestoreCollection: return collections[path] -## Issue a query on your Firestore database. +## Issue a query checked your Firestore database. ## ## [b]Note:[/b] a [code]FirestoreQuery[/code] object needs to be created to issue the query. ## This method will return a [code]FirestoreTask[/code] object, representing a reference to the request issued. -## If saved into a variable, the [code]FirestoreTask[/code] object can be used to yield on the [code]result_query(result)[/code] signal, or the more generic [code]task_finished(result)[/code] signal. +## If saved into a variable, the [code]FirestoreTask[/code] object can be used to yield checked the [code]result_query(result)[/code] signal, or the more generic [code]task_finished(result)[/code] signal. ## ## ex. ## [code]var query_task : FirestoreTask = Firebase.Firestore.query(FirestoreQuery.new())[/code] -## [code]yield(query_task, "task_finished")[/code] +## [code]await query_task.task_finished[/code] ## Since the emitted signal is holding an argument, it can be directly retrieved as a return variable from the [code]yield()[/code] function. ## ## ex. -## [code]var result : Array = yield(query_task, "task_finished")[/code] +## [code]var result : Array = await query_task.task_finished[/code] ## ## [b]Warning:[/b] It currently does not work offline! ## @@ -141,28 +142,28 @@ func collection(path : String) -> FirestoreCollection: ## @return FirestoreTask func query(query : FirestoreQuery) -> FirestoreTask: var firestore_task : FirestoreTask = FirestoreTask.new() - firestore_task.connect("result_query", self, "_on_result_query") - firestore_task.connect("task_error", self, "_on_task_error") + firestore_task.result_query.connect(_on_result_query) # In theory, this and the following could be a CONNECT_ONE_SHOT, but I'm iffy on whether or not that might break any client code, so leaving this for now. + firestore_task.task_error.connect(_on_task_error) firestore_task.action = FirestoreTask.Task.TASK_QUERY var body : Dictionary = { structuredQuery = query.query } var url : String = _base_url + _extended_url + _query_suffix firestore_task.data = query - firestore_task._fields = JSON.print(body) + firestore_task._fields = JSON.stringify(body) firestore_task._url = url _pooled_request(firestore_task) return firestore_task -## Request a list of contents (documents and/or collections) inside a collection, specified by its [i]id[/i]. This method will return a [code]FirestoreTask[/code] object, representing a reference to the request issued. If saved into a variable, the [code]FirestoreTask[/code] object can be used to yield on the [code]result_query(result)[/code] signal, or the more generic [code]task_finished(result)[/code] signal. +## Request a list of contents (documents and/or collections) inside a collection, specified by its [i]id[/i]. This method will return a [code]FirestoreTask[/code] object, representing a reference to the request issued. If saved into a variable, the [code]FirestoreTask[/code] object can be used to yield checked the [code]result_query(result)[/code] signal, or the more generic [code]task_finished(result)[/code] signal. ## [b]Note:[/b] [code]order_by[/code] does not work in offline mode. ## ex. ## [code]var query_task : FirestoreTask = Firebase.Firestore.query(FirestoreQuery.new())[/code] -## [code]yield(query_task, "task_finished")[/code] +## [code]await query_task.task_finished[/code] ## Since the emitted signal is holding an argument, it can be directly retrieved as a return variable from the [code]yield()[/code] function. ## ## ex. -## [code]var result : Array = yield(query_task, "task_finished")[/code] +## [code]var result : Array = await query_task.task_finished[/code] ## ## @args collection_id, page_size, page_token, order_by ## @arg-types String, int, String, String @@ -170,8 +171,8 @@ func query(query : FirestoreQuery) -> FirestoreTask: ## @return FirestoreTask func list(path : String = "", page_size : int = 0, page_token : String = "", order_by : String = "") -> FirestoreTask: var firestore_task : FirestoreTask = FirestoreTask.new() - firestore_task.connect("listed_documents", self, "_on_listed_documents") - firestore_task.connect("task_error", self, "_on_task_error") + firestore_task.listed_documents.connect(_on_listed_documents) # Same as above with one shot connections + firestore_task.task_error.connect(_on_task_error) firestore_task.action = FirestoreTask.Task.TASK_LIST var url : String = _base_url + _extended_url + path if page_size != 0: @@ -214,63 +215,7 @@ func disable_networking() -> void: func _set_offline(value: bool) -> void: - if value == _offline: - return - - _offline = value - if not persistence_enabled: - return - - var event_record_path: String = _config["cacheLocation"].plus_file(_CACHE_RECORD_FILE) - if not value: - var offline_time := 2147483647 # Maximum signed 32-bit integer - var file := File.new() - if file.open_encrypted_with_pass(event_record_path, File.READ, _encrypt_key) == OK: - offline_time = int(file.get_buffer(file.get_len()).get_string_from_utf8()) - 2 - file.close() - - var cache_dir := Directory.new() - var cache_files := [] - if cache_dir.open(_cache_loc) == OK: - cache_dir.list_dir_begin(true) - var file_name = cache_dir.get_next() - while file_name != "": - if not cache_dir.current_is_dir() and file_name.ends_with(_CACHE_EXTENSION): - if file.get_modified_time(_cache_loc.plus_file(file_name)) >= offline_time: - cache_files.append(_cache_loc.plus_file(file_name)) -# else: -# print("%s is old! It's time is %d, but the time offline was %d." % [file_name, file.get_modified_time(_cache_loc.plus_file(file_name)), offline_time]) - file_name = cache_dir.get_next() - cache_dir.list_dir_end() - - cache_files.erase(event_record_path) - cache_dir.remove(event_record_path) - - for cache in cache_files: - var deleted := false - if file.open_encrypted_with_pass(cache, File.READ, _encrypt_key) == OK: - var name := file.get_line() - var content := file.get_line() - var collection_id := name.left(name.find_last("/")) - var document_id := name.right(name.find_last("/") + 1) - - var collection := collection(collection_id) - if content == "--deleted--": - collection.delete(document_id) - deleted = true - else: - collection.update(document_id, FirestoreDocument.fields2dict(JSON.parse(content).result)) - else: - Firebase._printerr("Failed to retrieve cache %s! Error code: %d" % [cache, file.get_error()]) - file.close() - if deleted: - cache_dir.remove(cache) - - else: - var file := File.new() - if file.open_encrypted_with_pass(event_record_path, File.WRITE, _encrypt_key) == OK: - file.store_buffer(str(OS.get_unix_time()).to_utf8()) - file.close() + return # Since caching is causing a lot of issues, I'm turning it off for now. We will revisit this in the future, once we have some time to investigate why the cache is being corrupted. func _set_config(config_json : Dictionary) -> void: @@ -278,11 +223,7 @@ func _set_config(config_json : Dictionary) -> void: _cache_loc = _config["cacheLocation"] _extended_url = _extended_url.replace("[PROJECT_ID]", _config.projectId) - var file := File.new() - if file.file_exists(_cache_loc.plus_file(_CACHE_RECORD_FILE)): - _offline = true - else: - _offline = false + # Since caching is causing a lot of issues, I'm removing this check for now. We will revisit this in the future, once we have some time to investigate why the cache is being corrupted. _check_emulating() @@ -300,19 +241,19 @@ func _check_emulating() -> void : func _pooled_request(task : FirestoreTask) -> void: if _offline: - task._on_request_completed(HTTPRequest.RESULT_CANT_CONNECT, 404, PoolStringArray(), PoolByteArray()) + task._on_request_completed(HTTPRequest.RESULT_CANT_CONNECT, 404, PackedStringArray(), PackedByteArray()) return - if not auth and not Firebase.emulating: + if (auth == null or auth.is_empty()) and not Firebase.emulating: Firebase._print("Unauthenticated request issued...") Firebase.Auth.login_anonymous() - var result : Array = yield(Firebase.Auth, "auth_request") + var result : Array = await Firebase.Auth.auth_request if result[0] != 1: _check_auth_error(result[0], result[1]) Firebase._print("Client connected as Anonymous") if not Firebase.emulating: - task._headers = PoolStringArray([_AUTHORIZATION_HEADER + auth.idtoken]) + task._headers = PackedStringArray([_AUTHORIZATION_HEADER + auth.idtoken]) var http_request : HTTPRequest for request in _http_request_pool: @@ -325,26 +266,25 @@ func _pooled_request(task : FirestoreTask) -> void: http_request.timeout = 5 _http_request_pool.append(http_request) add_child(http_request) - http_request.connect("request_completed", self, "_on_pooled_request_completed", [http_request]) + http_request.request_completed.connect(_on_pooled_request_completed.bind(http_request)) http_request.set_meta("requesting", true) http_request.set_meta("lifetime", 0.0) http_request.set_meta("task", task) - http_request.request(task._url, task._headers, !Firebase.emulating, task._method, task._fields) + http_request.request(task._url, task._headers, task._method, task._fields) # ------------- -func _on_listed_documents(listed_documents : Array): - emit_signal("listed_documents", listed_documents) - +func _on_listed_documents(_listed_documents : Array): + listed_documents.emit(_listed_documents) func _on_result_query(result : Array): - emit_signal("result_query", result) + result_query.emit(result) func _on_task_error(code : int, status : String, message : String, task : int): - emit_signal("task_error", code, status, message) + task_error.emit(code, status, message) Firebase._printerr(message) func _on_FirebaseAuth_login_succeeded(auth_result : Dictionary) -> void: @@ -359,7 +299,7 @@ func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void: collections[key].auth = auth -func _on_pooled_request_completed(result : int, response_code : int, headers : PoolStringArray, body : PoolByteArray, request : HTTPRequest) -> void: +func _on_pooled_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray, request : HTTPRequest) -> void: request.get_meta("task")._on_request_completed(result, response_code, headers, body) request.set_meta("requesting", false) diff --git a/addons/godot-firebase/firestore/firestore_collection.gd b/addons/godot-firebase/firestore/firestore_collection.gd index 7903318..8808112 100644 --- a/addons/godot-firebase/firestore/firestore_collection.gd +++ b/addons/godot-firebase/firestore/firestore_collection.gd @@ -3,9 +3,9 @@ ## @meta-version 2.3 ## A reference to a Firestore Collection. ## Documentation TODO. -tool +@tool class_name FirestoreCollection -extends Reference +extends RefCounted signal add_document(doc) signal get_document(doc) @@ -35,14 +35,14 @@ var _request_queues := {} ## @args document_id ## @return FirestoreTask ## used to GET a document from the collection, specify @document_id -func get(document_id : String) -> FirestoreTask: +func get_doc(document_id : String) -> FirestoreTask: var task : FirestoreTask = FirestoreTask.new() task.action = FirestoreTask.Task.TASK_GET task.data = collection_name + "/" + document_id var url = _get_request_url() + _separator + document_id.replace(" ", "%20") - task.connect("get_document", self, "_on_get_document") - task.connect("task_finished", self, "_on_task_finished", [document_id], CONNECT_DEFERRED) + task.get_document.connect(_on_get_document) + task.task_finished.connect(_on_task_finished.bind(document_id), CONNECT_DEFERRED) _process_request(task, document_id, url) return task @@ -56,9 +56,9 @@ func add(document_id : String, fields : Dictionary = {}) -> FirestoreTask: task.data = collection_name + "/" + document_id var url = _get_request_url() + _query_tag + _documentId_tag + document_id - task.connect("add_document", self, "_on_add_document") - task.connect("task_finished", self, "_on_task_finished", [document_id], CONNECT_DEFERRED) - _process_request(task, document_id, url, JSON.print(FirestoreDocument.dict2fields(fields))) + task.add_document.connect(_on_add_document) + task.task_finished.connect(_on_task_finished.bind(document_id), CONNECT_DEFERRED) + _process_request(task, document_id, url, JSON.stringify(FirestoreDocument.dict2fields(fields))) return task ## @args document_id, fields @@ -74,9 +74,9 @@ func update(document_id : String, fields : Dictionary = {}) -> FirestoreTask: url+="updateMask.fieldPaths={key}&".format({key = key}) url = url.rstrip("&") - task.connect("update_document", self, "_on_update_document") - task.connect("task_finished", self, "_on_task_finished", [document_id], CONNECT_DEFERRED) - _process_request(task, document_id, url, JSON.print(FirestoreDocument.dict2fields(fields))) + task.update_document.connect(_on_update_document) + task.task_finished.connect(_on_task_finished.bind(document_id), CONNECT_DEFERRED) + _process_request(task, document_id, url, JSON.stringify(FirestoreDocument.dict2fields(fields))) return task ## @args document_id @@ -88,8 +88,8 @@ func delete(document_id : String) -> FirestoreTask: task.data = collection_name + "/" + document_id var url = _get_request_url() + _separator + document_id.replace(" ", "%20") - task.connect("delete_document", self, "_on_delete_document") - task.connect("task_finished", self, "_on_task_finished", [document_id], CONNECT_DEFERRED) + task.delete_document.connect(_on_delete_document) + task.task_finished.connect(_on_task_finished.bind(document_id), CONNECT_DEFERRED) _process_request(task, document_id, url) return task @@ -99,46 +99,44 @@ func _get_request_url() -> String: func _process_request(task : FirestoreTask, document_id : String, url : String, fields := "") -> void: - task.connect("task_error", self, "_on_error") + if not task.task_error.is_connected(_on_error): + task.task_error.connect(_on_error) - if not auth: + if auth == null or auth.is_empty(): Firebase._print("Unauthenticated request issued...") Firebase.Auth.login_anonymous() - var result : Array = yield(Firebase.Auth, "auth_request") + var result : Array = await Firebase.Auth.auth_request if result[0] != 1: Firebase.Firestore._check_auth_error(result[0], result[1]) - return null + return Firebase._print("Client authenticated as Anonymous User.") task._url = url task._fields = fields - task._headers = PoolStringArray([_AUTHORIZATION_HEADER + auth.idtoken]) - if _request_queues.has(document_id) and not _request_queues[document_id].empty(): + task._headers = PackedStringArray([_AUTHORIZATION_HEADER + auth.idtoken]) + if _request_queues.has(document_id) and not _request_queues[document_id].is_empty(): _request_queues[document_id].append(task) else: _request_queues[document_id] = [] firestore._pooled_request(task) -# task._push_request(url, , fields) - func _on_task_finished(task : FirestoreTask, document_id : String) -> void: - if not _request_queues[document_id].empty(): + if not _request_queues[document_id].is_empty(): task._push_request(task._url, _AUTHORIZATION_HEADER + auth.idtoken, task._fields) - # -------------------- Higher level of communication with signals func _on_get_document(document : FirestoreDocument): - emit_signal("get_document", document ) + get_document.emit(document) func _on_add_document(document : FirestoreDocument): - emit_signal("add_document", document ) + add_document.emit(document) func _on_update_document(document : FirestoreDocument): - emit_signal("update_document", document ) + update_document.emit(document) func _on_delete_document(): - emit_signal("delete_document") + delete_document.emit() func _on_error(code, status, message, task): - emit_signal("error", code, status, message) + error.emit(code, status, message) Firebase._printerr(message) diff --git a/addons/godot-firebase/firestore/firestore_document.gd b/addons/godot-firebase/firestore/firestore_document.gd index 3411eba..12039b6 100644 --- a/addons/godot-firebase/firestore/firestore_document.gd +++ b/addons/godot-firebase/firestore/firestore_document.gd @@ -2,22 +2,21 @@ ## @meta-version 2.2 ## A reference to a Firestore Document. ## Documentation TODO. -tool +@tool class_name FirestoreDocument -extends Reference - +extends RefCounted # A FirestoreDocument objects that holds all important values for a Firestore Document, # @doc_name = name of the Firestore Document, which is the request PATH # @doc_fields = fields held by Firestore Document, in APIs format # created when requested from a `collection().get()` call -var document: Dictionary # the Document itself -var doc_fields: Dictionary # only .fields -var doc_name: String # only .name -var create_time: String # createTime +var document : Dictionary # the Document itself +var doc_fields : Dictionary # only .fields +var doc_name : String # only .name +var create_time : String # createTime -func _init(doc: Dictionary = {}, _doc_name: String = "", _doc_fields: Dictionary = {}) -> void: +func _init(doc : Dictionary = {},_doc_name : String = "",_doc_fields : Dictionary = {}): self.document = doc self.doc_name = doc.name if self.doc_name.count("/") > 2: @@ -25,26 +24,25 @@ func _init(doc: Dictionary = {}, _doc_name: String = "", _doc_fields: Dictionary self.doc_fields = fields2dict(self.document) self.create_time = doc.createTime - # Pass a dictionary { 'key' : 'value' } to format it in a APIs usable .fields -# Field Path using the "dot" (`.`) notation are supported: +# Field Path3D using the "dot" (`.`) notation are supported: # ex. { "PATH.TO.SUBKEY" : "VALUE" } ==> { "PATH" : { "TO" : { "SUBKEY" : "VALUE" } } } -static func dict2fields(dict: Dictionary) -> Dictionary: - var fields: Dictionary = {} - var var_type: String = "" +static func dict2fields(dict : Dictionary) -> Dictionary: + var fields : Dictionary = {} + var var_type : String = "" for field in dict.keys(): var field_value = dict[field] if "." in field: var keys: Array = field.split(".") field = keys.pop_front() - keys.invert() + keys.reverse() for key in keys: - field_value = {key: field_value} + field_value = { key : field_value } match typeof(field_value): TYPE_NIL: var_type = "nullValue" TYPE_BOOL: var_type = "booleanValue" TYPE_INT: var_type = "integerValue" - TYPE_REAL: var_type = "doubleValue" + TYPE_FLOAT: var_type = "doubleValue" TYPE_STRING: var_type = "stringValue" TYPE_DICTIONARY: if is_field_timestamp(field_value): @@ -61,38 +59,36 @@ static func dict2fields(dict: Dictionary) -> Dictionary: for key in field_value["fields"].keys(): fields[field]["mapValue"]["fields"][key] = field_value["fields"][key] else: - fields[field] = {var_type: field_value} - return {"fields": fields} - + fields[field] = { var_type : field_value } + return {'fields' : fields} # Pass the .fields inside a Firestore Document to print out the Dictionary { 'key' : 'value' } -static func fields2dict(doc: Dictionary) -> Dictionary: - var dict: Dictionary = {} +static func fields2dict(doc : Dictionary) -> Dictionary: + var dict : Dictionary = {} if doc.has("fields"): - for field in doc.fields.keys(): - if doc.fields[field].has("mapValue"): - dict[field] = fields2dict(doc.fields[field].mapValue) - elif doc.fields[field].has("timestampValue"): - dict[field] = timestamp2dict(doc.fields[field].timestampValue) - elif doc.fields[field].has("arrayValue"): - dict[field] = fields2array(doc.fields[field].arrayValue) - elif doc.fields[field].has("integerValue"): - dict[field] = doc.fields[field].values()[0] as int - elif doc.fields[field].has("doubleValue"): - dict[field] = doc.fields[field].values()[0] as float - elif doc.fields[field].has("booleanValue"): - dict[field] = doc.fields[field].values()[0] as bool - elif doc.fields[field].has("nullValue"): + for field in (doc.fields).keys(): + if (doc.fields)[field].has("mapValue"): + dict[field] = fields2dict((doc.fields)[field].mapValue) + elif (doc.fields)[field].has("timestampValue"): + dict[field] = timestamp2dict((doc.fields)[field].timestampValue) + elif (doc.fields)[field].has("arrayValue"): + dict[field] = fields2array((doc.fields)[field].arrayValue) + elif (doc.fields)[field].has("integerValue"): + dict[field] = (doc.fields)[field].values()[0] as int + elif (doc.fields)[field].has("doubleValue"): + dict[field] = (doc.fields)[field].values()[0] as float + elif (doc.fields)[field].has("booleanValue"): + dict[field] = (doc.fields)[field].values()[0] as bool + elif (doc.fields)[field].has("nullValue"): dict[field] = null else: - dict[field] = doc.fields[field].values()[0] + dict[field] = (doc.fields)[field].values()[0] return dict - # Pass an Array to parse it to a Firebase arrayValue -static func array2fields(array: Array) -> Array: - var fields: Array = [] - var var_type: String = "" +static func array2fields(array : Array) -> Array: + var fields : Array = [] + var var_type : String = "" for field in array: match typeof(field): TYPE_DICTIONARY: @@ -105,17 +101,16 @@ static func array2fields(array: Array) -> Array: TYPE_NIL: var_type = "nullValue" TYPE_BOOL: var_type = "booleanValue" TYPE_INT: var_type = "integerValue" - TYPE_REAL: var_type = "doubleValue" + TYPE_FLOAT: var_type = "doubleValue" TYPE_STRING: var_type = "stringValue" TYPE_ARRAY: var_type = "arrayValue" - fields.append({var_type: field}) + fields.append({ var_type : field }) return fields - # Pass a Firebase arrayValue Dictionary to convert it back to an Array -static func fields2array(array: Dictionary) -> Array: - var fields: Array = [] +static func fields2array(array : Dictionary) -> Array: + var fields : Array = [] if array.has("values"): for field in array.values: var item @@ -139,31 +134,28 @@ static func fields2array(array: Dictionary) -> Array: fields.append(item) return fields - -# Converts a gdscript Dictionary (most likely obtained with OS.get_datetime()) to a Firebase Timestamp -static func dict2timestamp(dict: Dictionary) -> String: - dict.erase("weekday") - dict.erase("dst") - var dict_values: Array = dict.values() +# Converts a gdscript Dictionary (most likely obtained with Time.get_datetime_dict_from_system()) to a Firebase Timestamp +static func dict2timestamp(dict : Dictionary) -> String: + dict.erase('weekday') + dict.erase('dst') + var dict_values : Array = dict.values() return "%04d-%02d-%02dT%02d:%02d:%02d.00Z" % dict_values - # Converts a Firebase Timestamp back to a gdscript Dictionary -static func timestamp2dict(timestamp: String) -> Dictionary: - var datetime: Dictionary = {year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0} - var dict: PoolStringArray = timestamp.split("T")[0].split("-") +static func timestamp2dict(timestamp : String) -> Dictionary: + var datetime : Dictionary = {year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0} + var dict : PackedStringArray = timestamp.split("T")[0].split("-") dict.append_array(timestamp.split("T")[1].split(":")) - for value in dict.size(): + for value in dict.size() : datetime[datetime.keys()[value]] = int(dict[value]) return datetime - -static func is_field_timestamp(field: Dictionary) -> bool: - return field.has_all(["year", "month", "day", "hour", "minute", "second"]) - +static func is_field_timestamp(field : Dictionary) -> bool: + return field.has_all(['year','month','day','hour','minute','second']) # Call print(document) to return directly this document formatted func _to_string() -> String: - return "doc_name: {doc_name}, \ndoc_fields: {doc_fields}, \ncreate_time: {create_time}\n".format( - {doc_name = self.doc_name, doc_fields = self.doc_fields, create_time = self.create_time} - ) + return ("doc_name: {doc_name}, \ndoc_fields: {doc_fields}, \ncreate_time: {create_time}\n").format( + {doc_name = self.doc_name, + doc_fields = self.doc_fields, + create_time = self.create_time}) diff --git a/addons/godot-firebase/firestore/firestore_query.gd b/addons/godot-firebase/firestore/firestore_query.gd index a75a692..6a22e81 100644 --- a/addons/godot-firebase/firestore/firestore_query.gd +++ b/addons/godot-firebase/firestore/firestore_query.gd @@ -2,8 +2,8 @@ ## @meta-version 1.4 ## A firestore query. ## Documentation TODO. -tool -extends Reference +@tool +extends RefCounted class_name FirestoreQuery class Order: @@ -13,7 +13,7 @@ class Cursor: var values : Array var before : bool - func _init(v : Array, b : bool): + func _init(v : Array,b : bool): values = v before = b @@ -63,9 +63,6 @@ enum DIRECTION { DESCENDING } -func _init(): - return self - # Select which fields you want to return as a reflection from your query. # Fields must be added inside a list. Only a field is accepted inside the list @@ -107,7 +104,7 @@ func from_many(collections_array : Array) -> FirestoreQuery: # @operator : from FirestoreQuery.OPERATOR # @value : can be any type - String, int, bool, float # @chain : from FirestoreQuery.OPERATOR.[OR/AND], use it only if you want to chain "AND" or "OR" logic with futher where() calls -# eg. .where("name", OPERATOR.EQUAL, "Matt", OPERATOR.AND).where("age", OPERATOR.LESS_THAN, 20) +# eg. super.where("name", OPERATOR.EQUAL, "Matt", OPERATOR.AND).where("age", OPERATOR.LESS_THAN, 20) func where(field : String, operator : int, value = null, chain : int = -1): if operator in [OPERATOR.IS_NAN, OPERATOR.IS_NULL, OPERATOR.IS_NOT_NAN, OPERATOR.IS_NOT_NULL]: if (chain in [OPERATOR.AND, OPERATOR.OR]) or (query.has("where") and query.where.has("compositeFilter")): diff --git a/addons/godot-firebase/firestore/firestore_task.gd b/addons/godot-firebase/firestore/firestore_task.gd index ad65f03..3fe53a4 100644 --- a/addons/godot-firebase/firestore/firestore_task.gd +++ b/addons/godot-firebase/firestore/firestore_task.gd @@ -4,42 +4,42 @@ ## A [code]FirestoreTask[/code] is an independent node inheriting [code]HTTPRequest[/code] that processes a [code]Firestore[/code] request. ## Once the Task is completed (both if successfully or not) it will emit the relative signal (or a general purpose signal [code]task_finished()[/code]) and will destroy automatically. ## -## Being a [code]Node[/code] it can be stored in a variable to yield on it, and receive its result as a callback. +## Being a [code]Node[/code] it can be stored in a variable to yield checked it, and receive its result as a callback. ## All signals emitted by a [code]FirestoreTask[/code] represent a direct level of signal communication, which can be high ([code]get_document(document), result_query(result)[/code]) or low ([code]task_finished(result)[/code]). ## An indirect level of communication with Tasks is also provided, redirecting signals to the [class FirebaseFirestore] module. ## ## ex. ## [code]var task : FirestoreTask = Firebase.Firestore.query(query)[/code] -## [code]var result : Array = yield(task, "task_finished")[/code] -## [code]var result : Array = yield(task, "result_query")[/code] -## [code]var result : Array = yield(Firebase.Firestore, "task_finished")[/code] -## [code]var result : Array = yield(Firebase.Firestore, "result_query")[/code] +## [code]var result : Array = await task.task_finished[/code] +## [code]var result : Array = await task.result_query[/code] +## [code]var result : Array = await Firebase.Firestore.task_finished[/code] +## [code]var result : Array = await Firebase.Firestore.result_query[/code] ## ## @tutorial https://github.com/GodotNuts/GodotFirebase/wiki/Firestore#FirestoreTask -tool +@tool class_name FirestoreTask -extends Reference +extends RefCounted ## Emitted when a request is completed. The request can be successful or not successful: if not, an [code]error[/code] Dictionary will be passed as a result. ## @arg-types Variant signal task_finished(task) -## Emitted when a [code]add(document)[/code] request on a [class FirebaseCollection] is successfully completed. [code]error()[/code] signal will be emitted otherwise. +## Emitted when a [code]add(document)[/code] request checked a [class FirebaseCollection] is successfully completed. [code]error()[/code] signal will be emitted otherwise. ## @arg-types FirestoreDocument signal add_document(doc) -## Emitted when a [code]get(document)[/code] request on a [class FirebaseCollection] is successfully completed. [code]error()[/code] signal will be emitted otherwise. +## Emitted when a [code]get(document)[/code] request checked a [class FirebaseCollection] is successfully completed. [code]error()[/code] signal will be emitted otherwise. ## @arg-types FirestoreDocument signal get_document(doc) -## Emitted when a [code]update(document)[/code] request on a [class FirebaseCollection] is successfully completed. [code]error()[/code] signal will be emitted otherwise. +## Emitted when a [code]update(document)[/code] request checked a [class FirebaseCollection] is successfully completed. [code]error()[/code] signal will be emitted otherwise. ## @arg-types FirestoreDocument signal update_document(doc) -## Emitted when a [code]delete(document)[/code] request on a [class FirebaseCollection] is successfully completed. [code]error()[/code] signal will be emitted otherwise. +## Emitted when a [code]delete(document)[/code] request checked a [class FirebaseCollection] is successfully completed. [code]error()[/code] signal will be emitted otherwise. ## @arg-types FirestoreDocument signal delete_document() -## Emitted when a [code]list(collection_id)[/code] request on [class FirebaseFirestore] is successfully completed. [code]error()[/code] signal will be emitted otherwise. +## Emitted when a [code]list(collection_id)[/code] request checked [class FirebaseFirestore] is successfully completed. [code]error()[/code] signal will be emitted otherwise. ## @arg-types Array signal listed_documents(documents) -## Emitted when a [code]query(collection_id)[/code] request on [class FirebaseFirestore] is successfully completed. [code]error()[/code] signal will be emitted otherwise. +## Emitted when a [code]query(collection_id)[/code] request checked [class FirebaseFirestore] is successfully completed. [code]error()[/code] signal will be emitted otherwise. ## @arg-types Array signal result_query(result) ## Emitted when a request is [b]not[/b] successfully completed. @@ -58,7 +58,7 @@ enum Task { ## The code indicating the request Firestore is processing. ## See @[enum FirebaseFirestore.Requests] to get a full list of codes identifiers. ## @setter set_action -var action : int = -1 setget set_action +var action : int = -1 : set = set_action ## A variable, temporary holding the result of the request. var data @@ -67,81 +67,44 @@ var document : FirestoreDocument ## Whether the data came from cache. var from_cache : bool = false -var _response_headers : PoolStringArray = PoolStringArray() +var _response_headers : PackedStringArray = PackedStringArray() var _response_code : int = 0 var _method : int = -1 var _url : String = "" var _fields : String = "" -var _headers : PoolStringArray = [] +var _headers : PackedStringArray = [] -#func _ready() -> void: -# connect("request_completed", self, "_on_request_completed") - - -#func _push_request(url := "", headers := "", fields := "") -> void: -# _url = url -# _fields = fields -# var temp_header : Array = [] -# temp_header.append(headers) -# _headers = PoolStringArray(temp_header) -# -# if Firebase.Firestore._offline: -# call_deferred("_on_request_completed", -1, 404, PoolStringArray(), PoolByteArray()) -# else: -# request(_url, _headers, true, _method, _fields) - - -func _on_request_completed(result : int, response_code : int, headers : PoolStringArray, body : PoolByteArray) -> void: - var bod - if validate_json(body.get_string_from_utf8()).empty(): - bod = JSON.parse(body.get_string_from_utf8()).result +func _on_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: + var bod = body.get_string_from_utf8() + if bod != "": + bod = Utilities.get_json_data(bod) var offline: bool = typeof(bod) == TYPE_NIL var failed: bool = bod is Dictionary and bod.has("error") and response_code != HTTPClient.RESPONSE_OK from_cache = offline - Firebase.Firestore._set_offline(offline) - - var cache_path : String = Firebase._config["cacheLocation"] - if not cache_path.empty() and not failed and Firebase.Firestore.persistence_enabled: - var encrypt_key: String = Firebase.Firestore._encrypt_key - var full_path : String - var url_segment : String - match action: - Task.TASK_LIST: - url_segment = data[0] - full_path = cache_path - Task.TASK_QUERY: - url_segment = JSON.print(data.query) - full_path = cache_path - _: - url_segment = to_json(data) - full_path = _get_doc_file(cache_path, url_segment, encrypt_key) - bod = _handle_cache(offline, data, encrypt_key, full_path, bod) - if not bod.empty() and offline: - response_code = HTTPClient.RESPONSE_OK - + # Probably going to regret this... if response_code == HTTPClient.RESPONSE_OK: data = bod match action: Task.TASK_POST: document = FirestoreDocument.new(bod) - emit_signal("add_document", document) + add_document.emit(document) Task.TASK_GET: document = FirestoreDocument.new(bod) - emit_signal("get_document", document) + get_document.emit(document) Task.TASK_PATCH: document = FirestoreDocument.new(bod) - emit_signal("update_document", document) + update_document.emit(document) Task.TASK_DELETE: - emit_signal("delete_document") + delete_document.emit() Task.TASK_QUERY: data = [] for doc in bod: if doc.has('document'): data.append(FirestoreDocument.new(doc.document)) - emit_signal("result_query", data) + result_query.emit(data) Task.TASK_LIST: data = [] if bod.has('documents'): @@ -149,25 +112,25 @@ func _on_request_completed(result : int, response_code : int, headers : PoolStri data.append(FirestoreDocument.new(doc)) if bod.has("nextPageToken"): data.append(bod.nextPageToken) - emit_signal("listed_documents", data) + listed_documents.emit(data) else: Firebase._printerr("Action in error was: " + str(action)) - emit_error("task_error", bod, action) + emit_error(task_error, bod, action) - emit_signal("task_finished", self) + task_finished.emit(self) -func emit_error(signal_name : String, bod, task) -> void: +func emit_error(_signal, bod, task) -> void: if bod: if bod is Array and bod.size() > 0 and bod[0].has("error"): error = bod[0].error elif bod is Dictionary and bod.keys().size() > 0 and bod.has("error"): error = bod.error - emit_signal(signal_name, error.code, error.status, error.message, task) + _signal.emit(error.code, error.status, error.message, task) return - emit_signal(signal_name, 1, 0, "Unknown error", task) + _signal.emit(1, 0, "Unknown error", task) func set_action(value : int) -> void: action = value @@ -183,137 +146,7 @@ func set_action(value : int) -> void: func _handle_cache(offline : bool, data, encrypt_key : String, cache_path : String, body) -> Dictionary: - var body_return := {} - - var dir := Directory.new() - dir.make_dir_recursive(cache_path) - var file := File.new() - match action: - Task.TASK_POST: - if offline: - var save: Dictionary - if offline: - save = { - "name": "projects/%s/databases/(default)/documents/%s" % [Firebase._config["storageBucket"], data], - "fields": JSON.parse(_fields).result["fields"], - "createTime": "from_cache_file", - "updateTime": "from_cache_file" - } - else: - save = body.duplicate() - - if file.open_encrypted_with_pass(cache_path, File.READ, encrypt_key) == OK: - file.store_line(data) - file.store_line(JSON.print(save)) - body_return = save - else: - Firebase._printerr("Error saving cache file! Error code: %d" % file.get_error()) - file.close() - - Task.TASK_PATCH: - if offline: - var save := { - "fields": {} - } - if offline: - var mod: Dictionary - mod = { - "name": "projects/%s/databases/(default)/documents/%s" % [Firebase._config["storageBucket"], data], - "fields": JSON.parse(_fields).result["fields"], - "createTime": "from_cache_file", - "updateTime": "from_cache_file" - } - - if file.file_exists(cache_path): - if file.open_encrypted_with_pass(cache_path, File.READ, encrypt_key) == OK: - if file.get_len(): - assert(data == file.get_line()) - var content := file.get_line() - if content != "--deleted--": - save = JSON.parse(content).result - else: - Firebase._printerr("Error updating cache file! Error code: %d" % file.get_error()) - file.close() - - save.fields = FirestoreDocument.dict2fields(_merge_dict( - FirestoreDocument.fields2dict({"fields": save.fields}), - FirestoreDocument.fields2dict({"fields": mod.fields}), - not offline - )).fields - save.name = mod.name - save.createTime = mod.createTime - save.updateTime = mod.updateTime - else: - save = body.duplicate() - - - if file.open_encrypted_with_pass(cache_path, File.WRITE, encrypt_key) == OK: - file.store_line(data) - file.store_line(JSON.print(save)) - body_return = save - else: - Firebase._printerr("Error updating cache file! Error code: %d" % file.get_error()) - file.close() - - Task.TASK_GET: - if offline and file.file_exists(cache_path): - if file.open_encrypted_with_pass(cache_path, File.READ, encrypt_key) == OK: - assert(data == file.get_line()) - var content := file.get_line() - if content != "--deleted--": - body_return = JSON.parse(content).result - else: - Firebase._printerr("Error reading cache file! Error code: %d" % file.get_error()) - file.close() - - Task.TASK_DELETE: - if offline: - if file.open_encrypted_with_pass(cache_path, File.WRITE, encrypt_key) == OK: - file.store_line(data) - file.store_line("--deleted--") - body_return = {"deleted": true} - else: - Firebase._printerr("Error \"deleting\" cache file! Error code: %d" % file.get_error()) - file.close() - else: - dir.remove(cache_path) - - Task.TASK_LIST: - if offline: - var cache_dir := Directory.new() - var cache_files := [] - if cache_dir.open(cache_path) == OK: - cache_dir.list_dir_begin(true) - var file_name = cache_dir.get_next() - while file_name != "": - if not cache_dir.current_is_dir() and file_name.ends_with(Firebase.Firestore._CACHE_EXTENSION): - cache_files.append(cache_path.plus_file(file_name)) - file_name = cache_dir.get_next() - cache_dir.list_dir_end() - cache_files.erase(cache_path.plus_file(Firebase.Firestore._CACHE_RECORD_FILE)) - cache_dir.remove(cache_path.plus_file(Firebase.Firestore._CACHE_RECORD_FILE)) - print(cache_files) - - body_return.documents = [] - for cache in cache_files: - if file.open_encrypted_with_pass(cache, File.READ, encrypt_key) == OK: - if file.get_line().begins_with(data[0]): - body_return.documents.append(JSON.parse(file.get_line()).result) - else: - Firebase._printerr("Error opening cache file for listing! Error code: %d" % file.get_error()) - file.close() - body_return.documents.resize(min(data[1], body_return.documents.size())) - body_return.nextPageToken = "" - - Task.TASK_QUERY: - if offline: - Firebase._printerr("Offline queries are currently unsupported!") - - if not offline: - return body - else: - return body_return - + return body # Removing caching for now, hopefully this works without killing everyone and everything func _merge_dict(dic_a : Dictionary, dic_b : Dictionary, nullify := false) -> Dictionary: var ret := dic_a.duplicate(true) @@ -340,7 +173,7 @@ func _merge_array(arr_a : Array, arr_b : Array, nullify := false) -> Array: var index : int = i - deletions var val = arr_b[index] if val == null and nullify: - ret.remove(index) + ret.remove_at(index) deletions += i elif val is Array: ret[index] = _merge_array(ret[index] if ret[index] else [], val) @@ -349,24 +182,3 @@ func _merge_array(arr_a : Array, arr_b : Array, nullify := false) -> Array: else: ret[index] = val return ret - - -static func _get_doc_file(cache_path : String, document_id : String, encrypt_key : String) -> String: - var file := File.new() - var path := "" - var i = 0 - while i < 256: - path = cache_path.plus_file("%s-%d.fscache" % [str(document_id.hash()).pad_zeros(10), i]) - if file.file_exists(path): - var is_file := false - if file.open_encrypted_with_pass(path, File.READ, encrypt_key) == OK: - is_file = file.get_line() == document_id - file.close() - - if is_file: - return path - else: - i += 1 - else: - return path - return path diff --git a/addons/godot-firebase/functions/function_task.gd b/addons/godot-firebase/functions/function_task.gd index 461e005..ba35b09 100644 --- a/addons/godot-firebase/functions/function_task.gd +++ b/addons/godot-firebase/functions/function_task.gd @@ -3,16 +3,16 @@ ## ## ex. ## [code]var task : FirestoreTask = Firebase.Firestore.query(query)[/code] -## [code]var result : Array = yield(task, "task_finished")[/code] -## [code]var result : Array = yield(task, "result_query")[/code] -## [code]var result : Array = yield(Firebase.Firestore, "task_finished")[/code] -## [code]var result : Array = yield(Firebase.Firestore, "result_query")[/code] +## [code]var result : Array = await task.task_finished[/code] +## [code]var result : Array = await task.result_query[/code] +## [code]var result : Array = await Firebase.Firestore.task_finished[/code] +## [code]var result : Array = await Firebase.Firestore.result_query[/code] ## ## @tutorial https://github.com/GodotNuts/GodotFirebase/wiki/Firestore#FirestoreTask -tool -class_name FunctionTask -extends Reference +@tool +class_name FunctionTask +extends RefCounted ## Emitted when a request is completed. The request can be successful or not successful: if not, an [code]error[/code] Dictionary will be passed as a result. ## @arg-types Variant @@ -31,42 +31,29 @@ var data: Dictionary var error: Dictionary ## Whether the data came from cache. -var from_cache: bool = false - -var _response_headers: PoolStringArray = PoolStringArray() -var _response_code: int = 0 +var from_cache : bool = false -var _method: int = -1 -var _url: String = "" -var _fields: String = "" -var _headers: PoolStringArray = [] +var _response_headers : PackedStringArray = PackedStringArray() +var _response_code : int = 0 +var _method : int = -1 +var _url : String = "" +var _fields : String = "" +var _headers : PackedStringArray = [] -func _on_request_completed(result: int, response_code: int, headers: PoolStringArray, body: PoolByteArray) -> void: - var bod - if validate_json(body.get_string_from_utf8()).empty(): - bod = JSON.parse(body.get_string_from_utf8()).result - else: - bod = {content = body.get_string_from_utf8()} +func _on_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: + var bod = Utilities.get_json_data(body) + if bod == null: + bod = {content = body.get_string_from_utf8()} # I don't understand what this line does at all. What the hell?! var offline: bool = typeof(bod) == TYPE_NIL from_cache = offline data = bod - if response_code == HTTPClient.RESPONSE_OK and data != null: - emit_signal("function_executed", result, data) + if response_code == HTTPClient.RESPONSE_OK and data!=null: + function_executed.emit(result, data) else: - error = {result = result, response_code = response_code, data = data} - emit_signal("task_error", result, response_code, str(data)) - - emit_signal("task_finished", data) - + error = {result=result, response_code=response_code, data=data} + task_error.emit(result, response_code, str(data)) -#func _handle_cache(offline : bool, data, encrypt_key : String, cache_path : String, body) -> Dictionary: -# if offline: -# Firebase._printerr("Offline queries are currently unsupported!") -# -# if not offline: -# return body -# else: -# return body_return + task_finished.emit(data) diff --git a/addons/godot-firebase/functions/functions.gd b/addons/godot-firebase/functions/functions.gd index 0bb600e..e7f2b5d 100644 --- a/addons/godot-firebase/functions/functions.gd +++ b/addons/godot-firebase/functions/functions.gd @@ -4,7 +4,7 @@ ## (source: [url=https://firebase.google.com/docs/functions]Functions[/url]) ## ## @tutorial https://github.com/GodotNuts/GodotFirebase/wiki/Functions -tool +@tool class_name FirebaseFunctions extends Node @@ -34,7 +34,7 @@ var persistence_enabled : bool = true ## Whether an internet connection can be used. ## @default true -var networking: bool = true setget set_networking +var networking: bool = true : set = set_networking ## A Dictionary containing all authentication fields for the current logged user. ## @type Dictionary @@ -48,7 +48,7 @@ var _base_url : String = "" var _http_request_pool : Array = [] -var _offline: bool = false setget _set_offline +var _offline: bool = false : set = _set_offline func _ready() -> void: pass @@ -60,7 +60,8 @@ func _process(delta : float) -> void: var lifetime: float = request.get_meta("lifetime") + delta if lifetime > _MAX_POOLED_REQUEST_AGE: request.queue_free() - _http_request_pool.remove(i) + _http_request_pool.remove_at(i) + return # Prevent setting a value on request after it's already been queue_freed request.set_meta("lifetime", lifetime) @@ -68,24 +69,23 @@ func _process(delta : float) -> void: ## @return FunctionTask func execute(function: String, method: int, params: Dictionary = {}, body: Dictionary = {}) -> FunctionTask: var function_task : FunctionTask = FunctionTask.new() - function_task.connect("task_error", self, "_on_task_error") - function_task.connect("task_finished", self, "_on_task_finished") - function_task.connect("function_executed", self, "_on_function_executed") + function_task.task_error.connect(_on_task_error) + function_task.task_finished.connect(_on_task_finished) + function_task.function_executed.connect(_on_function_executed) function_task._method = method var url : String = _base_url + ("/" if not _base_url.ends_with("/") else "") + function + function_task._url = url - if not params.empty(): + if not params.is_empty(): url += "?" for key in params.keys(): url += key + "=" + params[key] + "&" - - function_task._url = url - if not body.empty(): - function_task._headers = PoolStringArray(["Content-Type: application/json"]) - function_task._fields = to_json(body) + if not body.is_empty(): + function_task._headers = PackedStringArray(["Content-Type: application/json"]) + function_task._fields = JSON.stringify(body) _pooled_request(function_task) return function_task @@ -146,13 +146,13 @@ func _check_emulating() -> void : func _pooled_request(task : FunctionTask) -> void: if _offline: - task._on_request_completed(HTTPRequest.RESULT_CANT_CONNECT, 404, PoolStringArray(), PoolByteArray()) + task._on_request_completed(HTTPRequest.RESULT_CANT_CONNECT, 404, PackedStringArray(), PackedByteArray()) return - if not auth: + if auth == null or auth.is_empty(): Firebase._print("Unauthenticated request issued...") Firebase.Auth.login_anonymous() - var result : Array = yield(Firebase.Auth, "auth_request") + var result : Array = await Firebase.Auth.auth_request if result[0] != 1: _check_auth_error(result[0], result[1]) Firebase._print("Client connected as Anonymous") @@ -170,12 +170,12 @@ func _pooled_request(task : FunctionTask) -> void: http_request = HTTPRequest.new() _http_request_pool.append(http_request) add_child(http_request) - http_request.connect("request_completed", self, "_on_pooled_request_completed", [http_request]) + http_request.request_completed.connect(_on_pooled_request_completed.bind(http_request)) http_request.set_meta("requesting", true) http_request.set_meta("lifetime", 0.0) http_request.set_meta("task", task) - http_request.request(task._url, task._headers, true, task._method, task._fields) + http_request.request(task._url, task._headers, task._method, task._fields) # ------------- @@ -187,7 +187,7 @@ func _on_function_executed(result : int, data : Dictionary) : pass func _on_task_error(code : int, status : int, message : String): - emit_signal("task_error", code, status, message) + task_error.emit(code, status, message) Firebase._printerr(message) func _on_FirebaseAuth_login_succeeded(auth_result : Dictionary) -> void: @@ -198,14 +198,13 @@ func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void: auth = auth_result -func _on_pooled_request_completed(result : int, response_code : int, headers : PoolStringArray, body : PoolByteArray, request : HTTPRequest) -> void: +func _on_pooled_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray, request : HTTPRequest) -> void: request.get_meta("task")._on_request_completed(result, response_code, headers, body) request.set_meta("requesting", false) func _on_connect_check_request_completed(result : int, _response_code, _headers, _body) -> void: _set_offline(result != HTTPRequest.RESULT_SUCCESS) - #_connect_check_node.request(_base_url) func _on_FirebaseAuth_logout() -> void: diff --git a/addons/godot-firebase/icon.svg.import b/addons/godot-firebase/icon.svg.import index 19a1c3b..943b056 100644 --- a/addons/godot-firebase/icon.svg.import +++ b/addons/godot-firebase/icon.svg.import @@ -1,8 +1,9 @@ [remap] importer="texture" -type="StreamTexture" -path="res://.import/icon.svg-5c4f39d37c9275a3768de73a392fd315.stex" +type="CompressedTexture2D" +uid="uid://2selq12fp4q0" +path="res://.godot/imported/icon.svg-5c4f39d37c9275a3768de73a392fd315.ctex" metadata={ "vram_texture": false } @@ -10,26 +11,27 @@ metadata={ [deps] source_file="res://addons/godot-firebase/icon.svg" -dest_files=[ "res://.import/icon.svg-5c4f39d37c9275a3768de73a392fd315.stex" ] +dest_files=["res://.godot/imported/icon.svg-5c4f39d37c9275a3768de73a392fd315.ctex"] [params] compress/mode=0 +compress/high_quality=false compress/lossy_quality=0.7 -compress/hdr_mode=0 -compress/bptc_ldr=0 +compress/hdr_compression=1 compress/normal_map=0 -flags/repeat=0 -flags/filter=true -flags/mipmaps=false -flags/anisotropic=false -flags/srgb=2 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" process/fix_alpha_border=true process/premult_alpha=false -process/HDR_as_SRGB=false -process/invert_color=false process/normal_map_invert_y=false -stream=false -size_limit=0 -detect_3d=true +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/godot-firebase/plugin.cfg b/addons/godot-firebase/plugin.cfg index 9249db8..5636e73 100644 --- a/addons/godot-firebase/plugin.cfg +++ b/addons/godot-firebase/plugin.cfg @@ -1,7 +1,7 @@ [plugin] name="GodotFirebase" -description="Google Firebase SDK written in GDScript for use in Godot Engine projects." -author="Kyle Szklenski" -version="4.8" +description="Google Firebase SDK written in GDScript for use in Godot Engine 4.0 projects." +author="GodotNutsOrg" +version="1.0" script="plugin.gd" diff --git a/addons/godot-firebase/plugin.gd b/addons/godot-firebase/plugin.gd index e0f7678..f68d5ae 100644 --- a/addons/godot-firebase/plugin.gd +++ b/addons/godot-firebase/plugin.gd @@ -1,10 +1,8 @@ -tool +@tool extends EditorPlugin - func _enter_tree() -> void: add_autoload_singleton("Firebase", "res://addons/godot-firebase/firebase/firebase.tscn") - func _exit_tree() -> void: remove_autoload_singleton("Firebase") diff --git a/addons/godot-firebase/storage/storage.gd b/addons/godot-firebase/storage/storage.gd index af7c22a..3fcf62b 100644 --- a/addons/godot-firebase/storage/storage.gd +++ b/addons/godot-firebase/storage/storage.gd @@ -4,56 +4,53 @@ ## This object handles all firebase storage tasks, variables and references. To use this API, you must first create a [StorageReference] with [method ref]. With the reference, you can then query and manipulate the file or folder in the cloud storage. ## ## [i]Note: In HTML builds, you must configure [url=https://firebase.google.com/docs/storage/web/download-files#cors_configuration]CORS[/url] in your storage bucket.[i] -tool +@tool class_name FirebaseStorage extends Node +const _API_VERSION : String = "v0" -const _API_VERSION: String = "v0" - -## @arg-types int, int, PoolStringArray +## @arg-types int, int, PackedStringArray ## @arg-enums HTTPRequest.Result, HTTPClient.ResponseCode ## Emitted when a [StorageTask] has finished successful. signal task_successful(result, response_code, data) -## @arg-types int, int, PoolStringArray +## @arg-types int, int, PackedStringArray ## @arg-enums HTTPRequest.Result, HTTPClient.ResponseCode ## Emitted when a [StorageTask] has finished with an error. signal task_failed(result, response_code, data) ## The current storage bucket the Storage API is referencing. -var bucket: String +var bucket : String ## @default false ## Whether a task is currently being processed. -var requesting: bool = false - -var _auth: Dictionary -var _config: Dictionary +var requesting : bool = false -var _references: Dictionary = {} +var _auth : Dictionary +var _config : Dictionary -var _base_url: String = "" -var _extended_url: String = "/[API_VERSION]/b/[APP_ID]/o/[FILE_PATH]" -var _root_ref: StorageReference +var _references : Dictionary = {} -var _http_client: HTTPClient = HTTPClient.new() -var _pending_tasks: Array = [] +var _base_url : String = "" +var _extended_url : String = "/[API_VERSION]/b/[APP_ID]/o/[FILE_PATH]" +var _root_ref : StorageReference -var _current_task: StorageTask -var _response_code: int -var _response_headers: PoolStringArray -var _response_data: PoolByteArray -var _content_length: int -var _reading_body: bool +var _http_client : HTTPClient = HTTPClient.new() +var _pending_tasks : Array = [] +var _current_task : StorageTask +var _response_code : int +var _response_headers : PackedStringArray +var _response_data : PackedByteArray +var _content_length : int +var _reading_body : bool -func _notification(what: int) -> void: +func _notification(what : int) -> void: if what == NOTIFICATION_INTERNAL_PROCESS: _internal_process(get_process_delta_time()) - -func _internal_process(_delta: float) -> void: +func _internal_process(_delta : float) -> void: if not requesting: set_process_internal(false) return @@ -62,7 +59,7 @@ func _internal_process(_delta: float) -> void: match _http_client.get_status(): HTTPClient.STATUS_DISCONNECTED: - _http_client.connect_to_host(_base_url, 443, true) + _http_client.connect_to_host(_base_url, 443, TLSOptions.client()) # Uhh, check if this is going to work. I assume not. HTTPClient.STATUS_RESOLVING, \ HTTPClient.STATUS_REQUESTING, \ @@ -79,7 +76,7 @@ func _internal_process(_delta: float) -> void: _reading_body = true # If there is a response... - if _response_headers.empty(): + if _response_headers.is_empty(): _response_headers = _http_client.get_response_headers() # Get response headers. _response_code = _http_client.get_response_code() @@ -110,16 +107,15 @@ func _internal_process(_delta: float) -> void: _finish_request(HTTPRequest.RESULT_CANT_RESOLVE) HTTPClient.STATUS_CONNECTION_ERROR: _finish_request(HTTPRequest.RESULT_CONNECTION_ERROR) - HTTPClient.STATUS_SSL_HANDSHAKE_ERROR: - _finish_request(HTTPRequest.RESULT_SSL_HANDSHAKE_ERROR) - + HTTPClient.STATUS_TLS_HANDSHAKE_ERROR: + _finish_request(HTTPRequest.RESULT_TLS_HANDSHAKE_ERROR) ## @args path ## @arg-defaults "" ## @return StorageReference -## Returns a reference to a file or folder in the storage bucket. It's this reference that should be used to control the file/folder on the server end. +## Returns a reference to a file or folder in the storage bucket. It's this reference that should be used to control the file/folder checked the server end. func ref(path := "") -> StorageReference: - if not _config: + if _config == null or _config.is_empty(): return null # Create a root storage reference if there's none @@ -135,15 +131,14 @@ func ref(path := "") -> StorageReference: ref.bucket = bucket ref.full_path = path ref.name = path.get_file() - ref.parent = ref(path.plus_file("..")) + ref.parent = ref(path.path_join("..")) ref.root = _root_ref ref.storage = self return ref else: return _references[path] - -func _set_config(config_json: Dictionary) -> void: +func _set_config(config_json : Dictionary) -> void: _config = config_json if bucket != _config.storageBucket: bucket = _config.storageBucket @@ -151,20 +146,20 @@ func _set_config(config_json: Dictionary) -> void: _check_emulating() -func _check_emulating() -> void: +func _check_emulating() -> void : ## Check emulating if not Firebase.emulating: _base_url = "https://firebasestorage.googleapis.com" else: - var port: String = _config.emulators.ports.storage + var port : String = _config.emulators.ports.storage if port == "": Firebase._printerr("You are in 'emulated' mode, but the port for Storage has not been configured.") else: - _base_url = "http://localhost:{port}/{version}/".format({version = _API_VERSION, port = port}) + _base_url = "http://localhost:{port}/{version}/".format({ version = _API_VERSION, port = port }) -func _upload(data: PoolByteArray, headers: PoolStringArray, ref: StorageReference, meta_only: bool) -> StorageTask: - if not (_config and _auth): +func _upload(data : PackedByteArray, headers : PackedStringArray, ref : StorageReference, meta_only : bool) -> StorageTask: + if _is_invalid_authentication(): return null var task := StorageTask.new() @@ -176,9 +171,8 @@ func _upload(data: PoolByteArray, headers: PoolStringArray, ref: StorageReferenc _process_request(task) return task - -func _download(ref: StorageReference, meta_only: bool, url_only: bool) -> StorageTask: - if not (_config and _auth): +func _download(ref : StorageReference, meta_only : bool, url_only : bool) -> StorageTask: + if _is_invalid_authentication(): return null var info_task := StorageTask.new() @@ -196,24 +190,23 @@ func _download(ref: StorageReference, meta_only: bool, url_only: bool) -> Storag task.action = StorageTask.Task.TASK_DOWNLOAD _pending_tasks.append(task) - yield(info_task, "task_finished") - if info_task.data and not info_task.data.has("error"): - task._url += info_task.data.downloadTokens + var data = await info_task.task_finished + if info_task.result == OK: + task._url += data.downloadTokens # I don't see how this will ever work, but who knows; pretty sure it doesn't, which means in theory it should be running the else every single time; data is a PackedByteArray, not sure what the original code was smoking else: task.data = info_task.data task.response_headers = info_task.response_headers task.response_code = info_task.response_code task.result = info_task.result task.finished = true - task.emit_signal("task_finished") - emit_signal("task_failed", task.result, task.response_code, task.data) + task.task_finished.emit() + task_failed.emit(task.result, task.response_code, task.data) _pending_tasks.erase(task) return task - -func _list(ref: StorageReference, list_all: bool) -> StorageTask: - if not (_config and _auth): +func _list(ref : StorageReference, list_all : bool) -> StorageTask: + if _is_invalid_authentication(): return null var task := StorageTask.new() @@ -223,9 +216,8 @@ func _list(ref: StorageReference, list_all: bool) -> StorageTask: _process_request(task) return task - -func _delete(ref: StorageReference) -> StorageTask: - if not (_config and _auth): +func _delete(ref : StorageReference) -> StorageTask: + if _is_invalid_authentication(): return null var task := StorageTask.new() @@ -235,8 +227,7 @@ func _delete(ref: StorageReference) -> StorageTask: _process_request(task) return task - -func _process_request(task: StorageTask) -> void: +func _process_request(task : StorageTask) -> void: if requesting: _pending_tasks.append(task) return @@ -244,12 +235,12 @@ func _process_request(task: StorageTask) -> void: var headers = Array(task._headers) headers.append("Authorization: Bearer " + _auth.idtoken) - task._headers = PoolStringArray(headers) + task._headers = PackedStringArray(headers) _current_task = task _response_code = 0 - _response_headers = PoolStringArray() - _response_data = PoolByteArray() + _response_headers = PackedStringArray() + _response_data = PackedByteArray() _content_length = 0 _reading_body = false @@ -257,8 +248,7 @@ func _process_request(task: StorageTask) -> void: _http_client.close() set_process_internal(true) - -func _finish_request(result: int) -> void: +func _finish_request(result : int) -> void: var task := _current_task requesting = false @@ -273,29 +263,29 @@ func _finish_request(result: int) -> void: StorageTask.Task.TASK_DELETE: _references.erase(task.ref.full_path) task.ref.valid = false - if typeof(task.data) == TYPE_RAW_ARRAY: + if typeof(task.data) == TYPE_PACKED_BYTE_ARRAY: task.data = null StorageTask.Task.TASK_DOWNLOAD_URL: - var json: Dictionary = JSON.parse(_response_data.get_string_from_utf8()).result - if json and json.has("downloadTokens"): + var json = Utilities.get_json_data(_response_data) + if json != null and json.has("downloadTokens"): task.data = _base_url + _get_file_url(task.ref) + "?alt=media&token=" + json.downloadTokens else: task.data = "" StorageTask.Task.TASK_LIST, StorageTask.Task.TASK_LIST_ALL: - var json: Dictionary = JSON.parse(_response_data.get_string_from_utf8()).result + var json = Utilities.get_json_data(_response_data) var items := [] - if json and json.has("items"): + if json != null and json.has("items"): for item in json.items: - var item_name: String = item.name + var item_name : String = item.name if item.bucket != bucket: continue if not item_name.begins_with(task.ref.full_path): continue if task.action == StorageTask.Task.TASK_LIST: - var dir_path: Array = item_name.split("/") - var slash_count: int = task.ref.full_path.count("/") + var dir_path : Array = item_name.split("/") + var slash_count : int = task.ref.full_path.count("/") item_name = "" for i in slash_count + 1: item_name += dir_path[i] @@ -308,37 +298,37 @@ func _finish_request(result: int) -> void: task.data = items _: - task.data = JSON.parse(_response_data.get_string_from_utf8()).result + var json = Utilities.get_json_data(_response_data) + task.data = json - var next_task: StorageTask - if not _pending_tasks.empty(): + var next_task : StorageTask + if not _pending_tasks.is_empty(): next_task = _pending_tasks.pop_front() task.finished = true - task.emit_signal("task_finished") + task.task_finished.emit() if typeof(task.data) == TYPE_DICTIONARY and task.data.has("error"): - emit_signal("task_failed", task.result, task.response_code, task.data) + task_failed.emit(task.result, task.response_code, task.data) else: - emit_signal("task_successful", task.result, task.response_code, task.data) + task_successful.emit(task.result, task.response_code, task.data) while true: if next_task and not next_task.finished: _process_request(next_task) break - elif not _pending_tasks.empty(): + elif not _pending_tasks.is_empty(): next_task = _pending_tasks.pop_front() else: break -func _get_file_url(ref: StorageReference) -> String: +func _get_file_url(ref : StorageReference) -> String: var url := _extended_url.replace("[APP_ID]", ref.bucket) url = url.replace("[API_VERSION]", _API_VERSION) return url.replace("[FILE_PATH]", ref.full_path.replace("/", "%2F")) - # Removes any "../" or "./" in the file path. -func _simplify_path(path: String) -> String: +func _simplify_path(path : String) -> String: var dirs := path.split("/") var new_dirs := [] for dir in dirs: @@ -349,19 +339,19 @@ func _simplify_path(path: String) -> String: else: new_dirs.push_back(dir) - var new_path := PoolStringArray(new_dirs).join("/") + var new_path := "/".join(PackedStringArray(new_dirs)) new_path = new_path.replace("//", "/") new_path = new_path.replace("\\", "/") return new_path - -func _on_FirebaseAuth_login_succeeded(auth_token: Dictionary) -> void: +func _on_FirebaseAuth_login_succeeded(auth_token : Dictionary) -> void: _auth = auth_token - -func _on_FirebaseAuth_token_refresh_succeeded(auth_result: Dictionary) -> void: +func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void: _auth = auth_result - func _on_FirebaseAuth_logout() -> void: _auth = {} + +func _is_invalid_authentication() -> bool: + return (_config == null or _config.is_empty()) or (_auth == null or _auth.is_empty()) diff --git a/addons/godot-firebase/storage/storage_reference.gd b/addons/godot-firebase/storage/storage_reference.gd index f47b786..08b1a49 100644 --- a/addons/godot-firebase/storage/storage_reference.gd +++ b/addons/godot-firebase/storage/storage_reference.gd @@ -2,16 +2,15 @@ ## @meta-version 2.2 ## A reference to a file or folder in the Firebase cloud storage. ## This object is used to interact with the cloud storage. You may get data from the server, as well as upload your own back to it. -tool +@tool class_name StorageReference -extends Reference - +extends RefCounted ## The default MIME type to use when uploading a file. -## Data sent with this type are interpreted as plain binary data. Note that firebase will generate an MIME type based on the file extenstion if none is provided. +## Data sent with this type are interpreted as plain binary data. Note that firebase will generate an MIME type based checked the file extenstion if none is provided. const DEFAULT_MIME_TYPE = "application/octet-stream" -## A dictionary of common MIME types based on a file extension. +## A dictionary of common MIME types based checked a file extension. ## Example: [code]MIME_TYPES.png[/code] will return [code]image/png[/code]. const MIME_TYPES = { "bmp": "image/bmp", @@ -43,23 +42,23 @@ const MIME_TYPES = { ## @default "" ## The stroage bucket this referenced file/folder is located in. -var bucket: String = "" +var bucket : String = "" ## @default "" ## The path to the file/folder relative to [member bucket]. -var full_path: String = "" +var full_path : String = "" ## @default "" ## The name of the file/folder, including any file extension. ## Example: If the [member full_path] is [code]images/user/image.png[/code], then the [member name] would be [code]image.png[/code]. -var name: String = "" +var name : String = "" ## The parent [StorageReference] one level up the file hierarchy. ## If the current [StorageReference] is the root (i.e. the [member full_path] is [code]""[/code]) then the [member parent] will be [code]null[/code]. -var parent: StorageReference +var parent : StorageReference ## The root [StorageReference]. -var root: StorageReference +var root : StorageReference ## @type FirebaseStorage ## The Storage API that created this [StorageReference] to begin with. @@ -68,22 +67,20 @@ var storage # FirebaseStorage (Can't static type due to cyclic reference) ## @default false ## Whether this [StorageReference] is valid. None of the functions will work when in an invalid state. ## It is set to false when [method delete] is called. -var valid: bool = false - +var valid : bool = false ## @args path ## @return StorageReference ## Returns a reference to another [StorageReference] relative to this one. -func child(path: String) -> StorageReference: +func child(path : String) -> StorageReference: if not valid: return null - return storage.ref(full_path.plus_file(path)) - + return storage.ref(full_path.path_join(path)) ## @args data, metadata ## @return StorageTask -## Makes an attempt to upload data to the referenced file location. Status on this task is found in the returned [StorageTask]. -func put_data(data: PoolByteArray, metadata := {}) -> StorageTask: +## Makes an attempt to upload data to the referenced file location. Status checked this task is found in the returned [StorageTask]. +func put_data(data : PackedByteArray, metadata := {}) -> StorageTask: if not valid: return null if not "Content-Length" in metadata and OS.get_name() != "HTML5": @@ -95,21 +92,18 @@ func put_data(data: PoolByteArray, metadata := {}) -> StorageTask: return storage._upload(data, headers, self, false) - ## @args data, metadata ## @return StorageTask ## Like [method put_data], but [code]data[/code] is a [String]. -func put_string(data: String, metadata := {}) -> StorageTask: - return put_data(data.to_utf8(), metadata) - +func put_string(data : String, metadata := {}) -> StorageTask: + return put_data(data.to_utf8_buffer(), metadata) ## @args file_path, metadata ## @return StorageTask ## Like [method put_data], but the data comes from a file at [code]file_path[/code]. -func put_file(file_path: String, metadata := {}) -> StorageTask: - var file := File.new() - file.open(file_path, File.READ) - var data := file.get_buffer(file.get_len()) +func put_file(file_path : String, metadata := {}) -> StorageTask: + var file := FileAccess.open(file_path, FileAccess.READ) + var data := file.get_buffer(file.get_length()) file.close() if "Content-Type" in metadata: @@ -117,84 +111,74 @@ func put_file(file_path: String, metadata := {}) -> StorageTask: return put_data(data, metadata) - ## @return StorageTask -## Makes an attempt to download the files from the referenced file location. Status on this task is found in the returned [StorageTask]. +## Makes an attempt to download the files from the referenced file location. Status checked this task is found in the returned [StorageTask]. func get_data() -> StorageTask: if not valid: return null storage._download(self, false, false) return storage._pending_tasks[-1] - ## @return StorageTask ## Like [method get_data], but the data in the returned [StorageTask] comes in the form of a [String]. func get_string() -> StorageTask: var task := get_data() - task.connect("task_finished", self, "_on_task_finished", [task, "stringify"]) + task.task_finished.connect(_on_task_finished.bind(task, "stringify")) return task - ## @return StorageTask -## Attempts to get the download url that points to the referenced file's data. Using the url directly may require an authentication header. Status on this task is found in the returned [StorageTask]. +## Attempts to get the download url that points to the referenced file's data. Using the url directly may require an authentication header. Status checked this task is found in the returned [StorageTask]. func get_download_url() -> StorageTask: if not valid: return null return storage._download(self, false, true) - ## @return StorageTask -## Attempts to get the metadata of the referenced file. Status on this task is found in the returned [StorageTask]. +## Attempts to get the metadata of the referenced file. Status checked this task is found in the returned [StorageTask]. func get_metadata() -> StorageTask: if not valid: return null return storage._download(self, true, false) - ## @args metadata ## @return StorageTask -## Attempts to update the metadata of the referenced file. Any field with a value of [code]null[/code] will be deleted on the server end. Status on this task is found in the returned [StorageTask]. -func update_metadata(metadata: Dictionary) -> StorageTask: +## Attempts to update the metadata of the referenced file. Any field with a value of [code]null[/code] will be deleted checked the server end. Status checked this task is found in the returned [StorageTask]. +func update_metadata(metadata : Dictionary) -> StorageTask: if not valid: return null - var data := JSON.print(metadata).to_utf8() - var headers := PoolStringArray(["Accept: application/json"]) + var data := JSON.stringify(metadata).to_utf8_buffer() + var headers := PackedStringArray(["Accept: application/json"]) return storage._upload(data, headers, self, true) - ## @return StorageTask -## Attempts to get the list of files and/or folders under the referenced folder This function is not nested unlike [method list_all]. Status on this task is found in the returned [StorageTask]. +## Attempts to get the list of files and/or folders under the referenced folder This function is not nested unlike [method list_all]. Status checked this task is found in the returned [StorageTask]. func list() -> StorageTask: if not valid: return null return storage._list(self, false) - ## @return StorageTask -## Attempts to get the list of files and/or folders under the referenced folder This function is nested unlike [method list]. Status on this task is found in the returned [StorageTask]. +## Attempts to get the list of files and/or folders under the referenced folder This function is nested unlike [method list]. Status checked this task is found in the returned [StorageTask]. func list_all() -> StorageTask: if not valid: return null return storage._list(self, true) - ## @return StorageTask -## Attempts to delete the referenced file/folder. If successful, the reference will become invalid And can no longer be used. If you need to reference this location again, make a new reference with [method StorageTask.ref]. Status on this task is found in the returned [StorageTask]. +## Attempts to delete the referenced file/folder. If successful, the reference will become invalid And can no longer be used. If you need to reference this location again, make a new reference with [method StorageTask.ref]. Status checked this task is found in the returned [StorageTask]. func delete() -> StorageTask: if not valid: return null return storage._delete(self) - func _to_string() -> String: var string := "gs://%s/%s" % [bucket, full_path] if not valid: - string += " [Invalid Reference]" + string += " [Invalid RefCounted]" return string - -func _on_task_finished(task: StorageTask, action: String) -> void: +func _on_task_finished(task : StorageTask, action : String) -> void: match action: "stringify": - if typeof(task.data) == TYPE_RAW_ARRAY: + if typeof(task.data) == TYPE_PACKED_BYTE_ARRAY: task.data = task.data.get_string_from_utf8() diff --git a/addons/godot-firebase/storage/storage_task.gd b/addons/godot-firebase/storage/storage_task.gd index 3efc353..ad810c5 100644 --- a/addons/godot-firebase/storage/storage_task.gd +++ b/addons/godot-firebase/storage/storage_task.gd @@ -1,10 +1,9 @@ ## @meta-authors SIsilicon ## @meta-version 2.2 ## An object that keeps track of an operation performed by [StorageReference]. -tool +@tool class_name StorageTask -extends Reference - +extends RefCounted enum Task { TASK_UPLOAD, @@ -18,51 +17,50 @@ enum Task { TASK_MAX ## The number of [enum Task] constants. } -## Emitted when the task is finished. Returns data depending on the success and action of the task. +## Emitted when the task is finished. Returns data depending checked the success and action of the task. signal task_finished(data) ## @type StorageReference ## The [StorageReference] that created this [StorageTask]. -var ref # Storage Reference (Can't static type due to cyclic reference) +var ref # Storage RefCounted (Can't static type due to cyclic reference) ## @enum Task ## @default -1 ## @setter set_action ## The kind of operation this [StorageTask] is keeping track of. -var action: int = -1 setget set_action +var action : int = -1 : set = set_action -## @default PoolByteArray() +## @default PackedByteArray() ## Data that the tracked task will/has returned. -var data = PoolByteArray() # data can be of any type. +var data = PackedByteArray() # data can be of any type. ## @default 0.0 ## The percentage of data that has been received. -var progress: float = 0.0 +var progress : float = 0.0 ## @default -1 ## @enum HTTPRequest.Result ## The resulting status of the task. Anyting other than [constant HTTPRequest.RESULT_SUCCESS] means an error has occured. -var result: int = -1 +var result : int = -1 ## @default false ## Whether the task is finished processing. -var finished: bool = false +var finished : bool = false -## @default PoolStringArray() +## @default PackedStringArray() ## The returned HTTP response headers. -var response_headers := PoolStringArray() +var response_headers := PackedStringArray() ## @default 0 ## @enum HTTPClient.ResponseCode ## The returned HTTP response code. -var response_code: int = 0 - -var _method: int = -1 -var _url: String = "" -var _headers: PoolStringArray = PoolStringArray() +var response_code : int = 0 +var _method : int = -1 +var _url : String = "" +var _headers : PackedStringArray = PackedStringArray() -func set_action(value: int) -> void: +func set_action(value : int) -> void: action = value match action: Task.TASK_UPLOAD: diff --git a/addons/http-sse-client/HTTPSSEClient.gd b/addons/http-sse-client/HTTPSSEClient.gd index ea77a34..aac13bd 100644 --- a/addons/http-sse-client/HTTPSSEClient.gd +++ b/addons/http-sse-client/HTTPSSEClient.gd @@ -1,7 +1,6 @@ -tool +@tool extends Node - signal new_sse_event(headers, event, data) signal connected signal connection_error(error) @@ -16,31 +15,29 @@ var is_connected = false var domain var url_after_domain var port -var use_ssl -var verify_host +var trusted_chain +var common_name_override var told_to_connect = false var connection_in_progress = false var is_requested = false -var response_body = PoolByteArray() - +var response_body = PackedByteArray() -func connect_to_host(domain: String, url_after_domain: String, port: int = -1, use_ssl: bool = false, verify_host: bool = true): +func connect_to_host(domain : String, url_after_domain : String, port : int = -1, trusted_chain : X509Certificate = null, common_name_override : String = ""): self.domain = domain self.url_after_domain = url_after_domain self.port = port - self.use_ssl = use_ssl - self.verify_host = verify_host + self.trusted_chain = trusted_chain + self.common_name_override = common_name_override told_to_connect = true - func attempt_to_connect(): - var err = httpclient.connect_to_host(domain, port, use_ssl, verify_host) + var tls_options = TLSOptions.client(trusted_chain, common_name_override) + var err = httpclient.connect_to_host(domain, port, tls_options) if err == OK: - emit_signal("connected") + connected.emit() is_connected = true else: - emit_signal("connection_error", str(err)) - + connection_error.emit(str(err)) func attempt_to_request(httpclient_status): if httpclient_status == HTTPClient.STATUS_CONNECTING or httpclient_status == HTTPClient.STATUS_RESOLVING: @@ -51,33 +48,34 @@ func attempt_to_request(httpclient_status): if err == OK: is_requested = true - func _parse_response_body(headers): var body = response_body.get_string_from_utf8() if body: var event_data = get_event_data(body) if event_data.event != "keep-alive" and event_data.event != continue_internal: - var result = JSON.parse(event_data.data).result - if response_body.size() > 0 and result: # stop here if the value doesn't parse + var result = Utilities.get_json_data(event_data.data) + if result != null: + var parsed_text = result + if response_body.size() > 0: # stop here if the value doesn't parse + response_body.resize(0) + new_sse_event.emit(headers, event_data.event, result) + else: + if event_data.event != continue_internal: response_body.resize(0) - emit_signal("new_sse_event", headers, event_data.event, result) - elif event_data.event != continue_internal: - response_body.resize(0) - func _process(delta): - if not told_to_connect: + if !told_to_connect: return - if not is_connected: - if not connection_in_progress: + if !is_connected: + if !connection_in_progress: attempt_to_connect() connection_in_progress = true return httpclient.poll() var httpclient_status = httpclient.get_status() - if not is_requested: + if !is_requested: attempt_to_request(httpclient_status) return @@ -87,7 +85,7 @@ func _process(delta): if httpclient_status == HTTPClient.STATUS_BODY: httpclient.poll() var chunk = httpclient.read_response_body_chunk() - if chunk.size() == 0: + if(chunk.size() == 0): return else: response_body = response_body + chunk @@ -105,8 +103,7 @@ func _process(delta): if response_body.size() > 0: _parse_response_body(headers) - -func get_event_data(body: String) -> Dictionary: +func get_event_data(body : String) -> Dictionary: var result = {} var event_idx = body.find(event_tag) if event_idx == -1: @@ -126,7 +123,6 @@ func get_event_data(body: String) -> Dictionary: result["data"] = data return result - func _exit_tree(): if httpclient: httpclient.close() diff --git a/addons/http-sse-client/httpsseclient_plugin.gd b/addons/http-sse-client/httpsseclient_plugin.gd index 26dda6c..87303c8 100644 --- a/addons/http-sse-client/httpsseclient_plugin.gd +++ b/addons/http-sse-client/httpsseclient_plugin.gd @@ -1,10 +1,8 @@ -tool +@tool extends EditorPlugin - func _enter_tree(): add_custom_type("HTTPSSEClient", "Node", preload("HTTPSSEClient.gd"), preload("icon.png")) - func _exit_tree(): remove_custom_type("HTTPSSEClient") From 1c52793a038c083f550b1988ab3663c62f8af041 Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Mon, 6 Feb 2023 09:12:32 -0500 Subject: [PATCH 04/49] Fix remaining issues --- addons/godot-firebase/Utilies.gd | 2 ++ addons/godot-firebase/auth/auth.gd | 3 +- addons/godot-firebase/database/database.gd | 1 + addons/godot-firebase/firebase/firebase.tscn | 33 ++++++++++--------- addons/godot-firebase/storage/storage.gd | 4 +-- .../storage/storage_reference.gd | 1 - addons/http-sse-client/HTTPSSEClient.gd | 13 ++++---- 7 files changed, 30 insertions(+), 27 deletions(-) diff --git a/addons/godot-firebase/Utilies.gd b/addons/godot-firebase/Utilies.gd index 1e66a0c..6e821bc 100644 --- a/addons/godot-firebase/Utilies.gd +++ b/addons/godot-firebase/Utilies.gd @@ -2,6 +2,8 @@ extends Node class_name Utilities static func get_json_data(value): + if value is PackedByteArray: + value = value.get_string_from_utf8() var json = JSON.new() var json_parse_result = json.parse(value) if json_parse_result == OK: diff --git a/addons/godot-firebase/auth/auth.gd b/addons/godot-firebase/auth/auth.gd index 55dd786..11694f8 100644 --- a/addons/godot-firebase/auth/auth.gd +++ b/addons/godot-firebase/auth/auth.gd @@ -403,7 +403,7 @@ func manual_token_refresh(auth_data): # This function is called whenever there is an authentication request to Firebase # On an error, this function with emit the signal 'login_failed' and print the error to the console func _on_FirebaseAuth_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: - var json = Utilities.get_json_data(body) + var json = Utilities.get_json_data(body.get_string_from_utf8()) is_busy = false var res if response_code == 0: @@ -467,7 +467,6 @@ func save_auth(auth : Dictionary) -> void: Firebase._printerr("Error Opening File. Error Code: " + str(FileAccess.get_open_error())) else: encrypted_file.store_line(JSON.stringify(auth)) - encrypted_file.close() # Function used to load the auth data file that has been stored locally diff --git a/addons/godot-firebase/database/database.gd b/addons/godot-firebase/database/database.gd index 430b33a..4d928cb 100644 --- a/addons/godot-firebase/database/database.gd +++ b/addons/godot-firebase/database/database.gd @@ -42,6 +42,7 @@ func _on_FirebaseAuth_logout() -> void: func get_database_reference(path : String, filter : Dictionary = {}) -> FirebaseDatabaseReference: var firebase_reference : FirebaseDatabaseReference = FirebaseDatabaseReference.new() var pusher : HTTPRequest = HTTPRequest.new() + pusher.use_threads = true var listener : Node = Node.new() listener.set_script(load("res://addons/http-sse-client/HTTPSSEClient.gd")) var store : FirebaseDatabaseStore = FirebaseDatabaseStore.new() diff --git a/addons/godot-firebase/firebase/firebase.tscn b/addons/godot-firebase/firebase/firebase.tscn index 3bc100d..d5b34d7 100644 --- a/addons/godot-firebase/firebase/firebase.tscn +++ b/addons/godot-firebase/firebase/firebase.tscn @@ -1,31 +1,32 @@ -[gd_scene load_steps=8 format=2] +[gd_scene load_steps=8 format=3 uid="uid://cvb26atjckwlq"] -[ext_resource path="res://addons/godot-firebase/database/database.gd" type="Script" id=1] -[ext_resource path="res://addons/godot-firebase/firestore/firestore.gd" type="Script" id=2] -[ext_resource path="res://addons/godot-firebase/firebase/firebase.gd" type="Script" id=3] -[ext_resource path="res://addons/godot-firebase/auth/auth.gd" type="Script" id=4] -[ext_resource path="res://addons/godot-firebase/storage/storage.gd" type="Script" id=5] -[ext_resource path="res://addons/godot-firebase/dynamiclinks/dynamiclinks.gd" type="Script" id=6] -[ext_resource path="res://addons/godot-firebase/functions/functions.gd" type="Script" id=7] +[ext_resource type="Script" path="res://addons/godot-firebase/database/database.gd" id="1"] +[ext_resource type="Script" path="res://addons/godot-firebase/firestore/firestore.gd" id="2"] +[ext_resource type="Script" path="res://addons/godot-firebase/firebase/firebase.gd" id="3"] +[ext_resource type="Script" path="res://addons/godot-firebase/auth/auth.gd" id="4"] +[ext_resource type="Script" path="res://addons/godot-firebase/storage/storage.gd" id="5"] +[ext_resource type="Script" path="res://addons/godot-firebase/dynamiclinks/dynamiclinks.gd" id="6"] +[ext_resource type="Script" path="res://addons/godot-firebase/functions/functions.gd" id="7"] [node name="Firebase" type="Node"] -process_mode = 2 -script = ExtResource( 3 ) +script = ExtResource("3") [node name="Auth" type="HTTPRequest" parent="."] -script = ExtResource( 4 ) +max_redirects = 12 +timeout = 10.0 +script = ExtResource("4") [node name="Firestore" type="Node" parent="."] -script = ExtResource( 2 ) +script = ExtResource("2") [node name="Database" type="Node" parent="."] -script = ExtResource( 1 ) +script = ExtResource("1") [node name="Storage" type="Node" parent="."] -script = ExtResource( 5 ) +script = ExtResource("5") [node name="DynamicLinks" type="Node" parent="."] -script = ExtResource( 6 ) +script = ExtResource("6") [node name="Functions" type="Node" parent="."] -script = ExtResource( 7 ) +script = ExtResource("7") diff --git a/addons/godot-firebase/storage/storage.gd b/addons/godot-firebase/storage/storage.gd index 3fcf62b..1b4b93e 100644 --- a/addons/godot-firebase/storage/storage.gd +++ b/addons/godot-firebase/storage/storage.gd @@ -192,7 +192,7 @@ func _download(ref : StorageReference, meta_only : bool, url_only : bool) -> Sto var data = await info_task.task_finished if info_task.result == OK: - task._url += data.downloadTokens # I don't see how this will ever work, but who knows; pretty sure it doesn't, which means in theory it should be running the else every single time; data is a PackedByteArray, not sure what the original code was smoking + task._url += info_task.data.downloadTokens else: task.data = info_task.data task.response_headers = info_task.response_headers @@ -306,7 +306,7 @@ func _finish_request(result : int) -> void: next_task = _pending_tasks.pop_front() task.finished = true - task.task_finished.emit() + task.task_finished.emit(task.data) # I believe this parameter has been missing all along, but not sure. Caused weird results at times with a yield/await returning null, but the task containing data. if typeof(task.data) == TYPE_DICTIONARY and task.data.has("error"): task_failed.emit(task.result, task.response_code, task.data) else: diff --git a/addons/godot-firebase/storage/storage_reference.gd b/addons/godot-firebase/storage/storage_reference.gd index 08b1a49..3cad552 100644 --- a/addons/godot-firebase/storage/storage_reference.gd +++ b/addons/godot-firebase/storage/storage_reference.gd @@ -104,7 +104,6 @@ func put_string(data : String, metadata := {}) -> StorageTask: func put_file(file_path : String, metadata := {}) -> StorageTask: var file := FileAccess.open(file_path, FileAccess.READ) var data := file.get_buffer(file.get_length()) - file.close() if "Content-Type" in metadata: metadata["Content-Type"] = MIME_TYPES.get(file_path.get_extension(), DEFAULT_MIME_TYPE) diff --git a/addons/http-sse-client/HTTPSSEClient.gd b/addons/http-sse-client/HTTPSSEClient.gd index aac13bd..2a01706 100644 --- a/addons/http-sse-client/HTTPSSEClient.gd +++ b/addons/http-sse-client/HTTPSSEClient.gd @@ -23,6 +23,7 @@ var is_requested = false var response_body = PackedByteArray() func connect_to_host(domain : String, url_after_domain : String, port : int = -1, trusted_chain : X509Certificate = null, common_name_override : String = ""): + process_mode = Node.PROCESS_MODE_INHERIT self.domain = domain self.url_after_domain = url_after_domain self.port = port @@ -103,7 +104,7 @@ func _process(delta): if response_body.size() > 0: _parse_response_body(headers) -func get_event_data(body : String) -> Dictionary: +func get_event_data(body : String): var result = {} var event_idx = body.find(event_tag) if event_idx == -1: @@ -113,11 +114,11 @@ func get_event_data(body : String) -> Dictionary: var data_idx = body.find(data_tag, event_idx + event_tag.length()) assert(data_idx != -1) var event = body.substr(event_idx, data_idx) - event = event.replace(event_tag, "").strip_edges() - assert(event) - assert(event.length() > 0) - result["event"] = event - var data = body.right(data_idx + data_tag.length()).strip_edges() + var event_value = event.replace(event_tag, "").strip_edges() + assert(event_value) + assert(event_value.length() > 0) + result["event"] = event_value + var data = body.right(body.length() - (data_idx + data_tag.length())).strip_edges() assert(data) assert(data.length() > 0) result["data"] = data From 5f3e8d15dd875b3151ab1cde645faa8428c86c9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Santilio?= Date: Wed, 17 May 2023 10:55:16 +0000 Subject: [PATCH 05/49] fixes --- addons/godot-firebase/auth/auth.gd | 12 +++++++----- addons/godot-firebase/firebase/firebase.gd | 16 ++++++++-------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/addons/godot-firebase/auth/auth.gd b/addons/godot-firebase/auth/auth.gd index 11694f8..44c62b3 100644 --- a/addons/godot-firebase/auth/auth.gd +++ b/addons/godot-firebase/auth/auth.gd @@ -460,18 +460,19 @@ func _on_FirebaseAuth_request_completed(result : int, response_code : int, heade # Function used to save the auth data provided by Firebase into an encrypted file # Note this does not work in HTML5 or UWP -func save_auth(auth : Dictionary) -> void: +func save_auth(auth : Dictionary) -> bool: var encrypted_file = FileAccess.open_encrypted_with_pass("user://user.auth", FileAccess.WRITE, _config.apiKey) var err = encrypted_file == null if err: Firebase._printerr("Error Opening File. Error Code: " + str(FileAccess.get_open_error())) else: encrypted_file.store_line(JSON.stringify(auth)) + return not err # Function used to load the auth data file that has been stored locally # Note this does not work in HTML5 or UWP -func load_auth() -> void: +func load_auth() -> bool: var encrypted_file = FileAccess.open_encrypted_with_pass("user://user.auth", FileAccess.WRITE, _config.apiKey) var err = encrypted_file == null if err: @@ -483,7 +484,7 @@ func load_auth() -> void: if json_parse_result == OK: var encrypted_file_data = json.data manual_token_refresh(encrypted_file_data) - + return not err # Function used to remove_at the local encrypted auth file func remove_auth() -> void: @@ -495,13 +496,14 @@ func remove_auth() -> void: # Function to check if there is an encrypted auth data file # If there is, the game will load it and refresh the token -func check_auth_file() -> void: +func check_auth_file() -> bool: if (FileAccess.file_exists("user://user.auth")): # Will ensure "auth_request" emitted - load_auth() + return load_auth() else: Firebase._printerr("Encrypted Firebase Auth file does not exist") auth_request.emit(ERR_DOES_NOT_EXIST, "Encrypted Firebase Auth file does not exist") + return false # Function used to change the email account for the currently logged in user diff --git a/addons/godot-firebase/firebase/firebase.gd b/addons/godot-firebase/firebase/firebase.gd index 8c106b3..0e63cae 100644 --- a/addons/godot-firebase/firebase/firebase.gd +++ b/addons/godot-firebase/firebase/firebase.gd @@ -17,27 +17,27 @@ const _AUTH_PROVIDERS : String = "firebase/auth_providers" ## @type FirebaseAuth ## The Firebase Authentication API. -@onready var Auth := $Auth +@onready var Auth : FirebaseAuth = $Auth ## @type FirebaseFirestore ## The Firebase Firestore API. -@onready var Firestore := $Firestore +@onready var Firestore : FirebaseFirestore = $Firestore ## @type FirebaseDatabase ## The Firebase Realtime Database API. -@onready var Database := $Database +@onready var Database : FirebaseDatabase = $Database ## @type FirebaseStorage ## The Firebase Storage API. -@onready var Storage := $Storage +@onready var Storage : FirebaseStorage = $Storage ## @type FirebaseDynamicLinks ## The Firebase Dynamic Links API. -@onready var DynamicLinks := $DynamicLinks +@onready var DynamicLinks : FirebaseDynamicLinks = $DynamicLinks ## @type FirebaseFunctions ## The Firebase Cloud Functions API -@onready var Functions := $Functions +@onready var Functions : FirebaseFunctions = $Functions @export var emulating : bool = false @@ -106,9 +106,9 @@ func _load_config() -> void: if key == "emulators" and config_value.has("ports"): for port in config_value["ports"].keys(): config_value["ports"][port] = env.get_value(_EMULATORS_PORTS, port, "") - if key == "auth_providers" and config_value.has("auth_providers"): + if key == "auth_providers": for provider in config_value.keys(): - config_value[provider] = env.get_value(_AUTH_PROVIDERS, provider) + config_value[provider] = env.get_value(_AUTH_PROVIDERS, provider, "") else: var value : String = env.get_value(_ENVIRONMENT_VARIABLES, key, "") if value == "": From 79bb6dd6c2fdb2849ca93b08578264a9f5aa4afa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Santilio?= Date: Wed, 17 May 2023 10:59:12 +0000 Subject: [PATCH 06/49] fixes --- addons/godot-firebase/auth/auth.gd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/godot-firebase/auth/auth.gd b/addons/godot-firebase/auth/auth.gd index 44c62b3..35e1b3e 100644 --- a/addons/godot-firebase/auth/auth.gd +++ b/addons/godot-firebase/auth/auth.gd @@ -473,7 +473,7 @@ func save_auth(auth : Dictionary) -> bool: # Function used to load the auth data file that has been stored locally # Note this does not work in HTML5 or UWP func load_auth() -> bool: - var encrypted_file = FileAccess.open_encrypted_with_pass("user://user.auth", FileAccess.WRITE, _config.apiKey) + var encrypted_file = FileAccess.open_encrypted_with_pass("user://user.auth", FileAccess.READ, _config.apiKey) var err = encrypted_file == null if err: Firebase._printerr("Error Opening Firebase Auth File. Error Code: " + str(FileAccess.get_open_error())) From dfadfcdcf4e6074e8a61f70375bbcc72ad725e0d Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Thu, 8 Jun 2023 07:50:08 -0400 Subject: [PATCH 07/49] Update firestore.gd --- addons/godot-firebase/firestore/firestore.gd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/godot-firebase/firestore/firestore.gd b/addons/godot-firebase/firestore/firestore.gd index 4fe44d6..07bb2a7 100644 --- a/addons/godot-firebase/firestore/firestore.gd +++ b/addons/godot-firebase/firestore/firestore.gd @@ -52,7 +52,7 @@ var request : int = -1 ## Whether cache files can be used and generated. ## @default true -var persistence_enabled : bool = true +var persistence_enabled : bool = false ## Whether an internet connection can be used. ## @default true From b8bd1fc4ad9e42799dc256c4b2fe1114a396725f Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Thu, 8 Jun 2023 07:51:39 -0400 Subject: [PATCH 08/49] Update README.md --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 14d0278..e33b49d 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,11 @@ godotnuts@gmail.com The following individuals and many more have contributed significantly to this project. If you would like to support this project's further development, consider supporting them. -- [Kyle Szklenski](https://github.com/WolfgangSenff) (creator) ([buy me a coffee](https://ko-fi.com/kyleszklenski)) -- [Nicolò Santilio](https://github.com/fenix-hub) (creator) -- [Chuck Lindblom](https://github.com/BearDooks) (creator) -- [SIsilicon](https://github.com/SISilicon) (creator) -- [Luke Hollenback](https://github.com/lukehollenback) ([buy me a coffee](https://ko-fi.com/lukehollenback)) +- [Kyle Szklenski](https://github.com/WolfgangSenff) (creator - original, Database, Functions, several features) ([buy me a coffee](https://ko-fi.com/kyleszklenski)) +- [Nicolò Santilio](https://github.com/fenix-hub) (creator - Firestore, several features) +- [Chuck Lindblom](https://github.com/BearDooks) (creator - several features across the board) +- [SIsilicon](https://github.com/SISilicon) (creator - Firestore, a few other features) +- [Luke Hollenback](https://github.com/lukehollenback) (unit testing) ([buy me a coffee](https://ko-fi.com/lukehollenback)) ## :arrow_down: Cloning SSH: From 67923d8c2ee7c032c3ffe9a002d6233a7a3f6802 Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Thu, 8 Jun 2023 08:42:21 -0400 Subject: [PATCH 09/49] Remove persistence --- addons/godot-firebase/functions/functions.gd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/godot-firebase/functions/functions.gd b/addons/godot-firebase/functions/functions.gd index e7f2b5d..76855a6 100644 --- a/addons/godot-firebase/functions/functions.gd +++ b/addons/godot-firebase/functions/functions.gd @@ -30,7 +30,7 @@ var request : int = -1 ## Whether cache files can be used and generated. ## @default true -var persistence_enabled : bool = true +var persistence_enabled : bool = false ## Whether an internet connection can be used. ## @default true From e9075deafafa4d896942f05a88c24dda71af6278 Mon Sep 17 00:00:00 2001 From: Ben Sarsgard Date: Tue, 4 Jul 2023 10:57:42 -0400 Subject: [PATCH 10/49] Fixes an issue with HTTPRequest on web exports * disable gzip compression on web export, as this results in HTTPRequest returning an empty body due to a failure in decompression * also check for "Web" in OS.get_name() ("HTML5" appears to be an old value) --- addons/godot-firebase/auth/auth.gd | 3 ++- addons/godot-firebase/database/database.gd | 2 ++ addons/godot-firebase/dynamiclinks/dynamiclinks.gd | 2 ++ addons/godot-firebase/firestore/firestore.gd | 2 ++ addons/godot-firebase/functions/functions.gd | 2 ++ 5 files changed, 10 insertions(+), 1 deletion(-) diff --git a/addons/godot-firebase/auth/auth.gd b/addons/godot-firebase/auth/auth.gd index 35e1b3e..3850c15 100644 --- a/addons/godot-firebase/auth/auth.gd +++ b/addons/godot-firebase/auth/auth.gd @@ -147,7 +147,8 @@ func _ready() -> void: tcp_timer.wait_time = tcp_timeout tcp_timer.timeout.connect(_tcp_stream_timer) - if OS.get_name() == "HTML5": + if OS.get_name() == "HTML5" or OS.get_name() == "Web": + accept_gzip = false # Fixes broken gzip compression in web exports _local_uri += "tmp_js_export.html" diff --git a/addons/godot-firebase/database/database.gd b/addons/godot-firebase/database/database.gd index 4d928cb..ab95761 100644 --- a/addons/godot-firebase/database/database.gd +++ b/addons/godot-firebase/database/database.gd @@ -42,6 +42,8 @@ func _on_FirebaseAuth_logout() -> void: func get_database_reference(path : String, filter : Dictionary = {}) -> FirebaseDatabaseReference: var firebase_reference : FirebaseDatabaseReference = FirebaseDatabaseReference.new() var pusher : HTTPRequest = HTTPRequest.new() + if OS.get_name() == "HTML5" or OS.get_name() == "Web": + pusher.accept_gzip = false # Fixes broken gzip compression in web exports pusher.use_threads = true var listener : Node = Node.new() listener.set_script(load("res://addons/http-sse-client/HTTPSSEClient.gd")) diff --git a/addons/godot-firebase/dynamiclinks/dynamiclinks.gd b/addons/godot-firebase/dynamiclinks/dynamiclinks.gd index c23e86c..62cd518 100644 --- a/addons/godot-firebase/dynamiclinks/dynamiclinks.gd +++ b/addons/godot-firebase/dynamiclinks/dynamiclinks.gd @@ -32,6 +32,8 @@ enum Requests { func _set_config(config_json : Dictionary) -> void: _config = config_json _request_list_node = HTTPRequest.new() + if OS.get_name() == "HTML5" or OS.get_name() == "Web": + _request_list_node .accept_gzip = false # Fixes broken gzip compression in web exports _request_list_node.request_completed.connect(_on_request_completed) add_child(_request_list_node) _check_emulating() diff --git a/addons/godot-firebase/firestore/firestore.gd b/addons/godot-firebase/firestore/firestore.gd index 07bb2a7..8a853cd 100644 --- a/addons/godot-firebase/firestore/firestore.gd +++ b/addons/godot-firebase/firestore/firestore.gd @@ -264,6 +264,8 @@ func _pooled_request(task : FirestoreTask) -> void: if not http_request: http_request = HTTPRequest.new() http_request.timeout = 5 + if OS.get_name() == "HTML5" or OS.get_name() == "Web": + http_request.accept_gzip = false # Fixes broken gzip compression in web exports _http_request_pool.append(http_request) add_child(http_request) http_request.request_completed.connect(_on_pooled_request_completed.bind(http_request)) diff --git a/addons/godot-firebase/functions/functions.gd b/addons/godot-firebase/functions/functions.gd index 76855a6..a73a4ba 100644 --- a/addons/godot-firebase/functions/functions.gd +++ b/addons/godot-firebase/functions/functions.gd @@ -168,6 +168,8 @@ func _pooled_request(task : FunctionTask) -> void: if not http_request: http_request = HTTPRequest.new() + if OS.get_name() == "HTML5" or OS.get_name() == "Web": + http_request.accept_gzip = false # Fixes broken gzip compression in web exports _http_request_pool.append(http_request) add_child(http_request) http_request.request_completed.connect(_on_pooled_request_completed.bind(http_request)) From 57508d4fc528ed1d53a9e5c57042dcdac9b88856 Mon Sep 17 00:00:00 2001 From: Ben Sarsgard Date: Tue, 4 Jul 2023 11:03:16 -0400 Subject: [PATCH 11/49] Fix whitespace --- addons/godot-firebase/dynamiclinks/dynamiclinks.gd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/godot-firebase/dynamiclinks/dynamiclinks.gd b/addons/godot-firebase/dynamiclinks/dynamiclinks.gd index 62cd518..8575519 100644 --- a/addons/godot-firebase/dynamiclinks/dynamiclinks.gd +++ b/addons/godot-firebase/dynamiclinks/dynamiclinks.gd @@ -33,7 +33,7 @@ func _set_config(config_json : Dictionary) -> void: _config = config_json _request_list_node = HTTPRequest.new() if OS.get_name() == "HTML5" or OS.get_name() == "Web": - _request_list_node .accept_gzip = false # Fixes broken gzip compression in web exports + _request_list_node.accept_gzip = false # Fixes broken gzip compression in web exports _request_list_node.request_completed.connect(_on_request_completed) add_child(_request_list_node) _check_emulating() From 37cd3f842d2fde9aa6066a52b6a3e22ac861c80a Mon Sep 17 00:00:00 2001 From: Ben Sarsgard Date: Tue, 4 Jul 2023 11:26:38 -0400 Subject: [PATCH 12/49] Move logic into static utility class --- addons/godot-firebase/Utilies.gd | 8 ++++++++ addons/godot-firebase/auth/auth.gd | 2 +- addons/godot-firebase/database/database.gd | 3 +-- addons/godot-firebase/dynamiclinks/dynamiclinks.gd | 3 +-- addons/godot-firebase/firestore/firestore.gd | 3 +-- addons/godot-firebase/functions/functions.gd | 3 +-- 6 files changed, 13 insertions(+), 9 deletions(-) diff --git a/addons/godot-firebase/Utilies.gd b/addons/godot-firebase/Utilies.gd index 6e821bc..3b7143b 100644 --- a/addons/godot-firebase/Utilies.gd +++ b/addons/godot-firebase/Utilies.gd @@ -10,3 +10,11 @@ static func get_json_data(value): return json.data return null + + +# HTTPRequeust seems to have an issue in Web exports where the body returns empty +# This appears to be caused by the gzip compression being unsupported, so we +# disable it when web export is detected. +static func fix_http_request(http_request): + if OS.get_name() == "HTML5" or OS.get_name() == "Web": + http_request.accept_gzip = false \ No newline at end of file diff --git a/addons/godot-firebase/auth/auth.gd b/addons/godot-firebase/auth/auth.gd index 3850c15..2c7081f 100644 --- a/addons/godot-firebase/auth/auth.gd +++ b/addons/godot-firebase/auth/auth.gd @@ -147,8 +147,8 @@ func _ready() -> void: tcp_timer.wait_time = tcp_timeout tcp_timer.timeout.connect(_tcp_stream_timer) + Utilities.fix_http_request(self) if OS.get_name() == "HTML5" or OS.get_name() == "Web": - accept_gzip = false # Fixes broken gzip compression in web exports _local_uri += "tmp_js_export.html" diff --git a/addons/godot-firebase/database/database.gd b/addons/godot-firebase/database/database.gd index ab95761..195b699 100644 --- a/addons/godot-firebase/database/database.gd +++ b/addons/godot-firebase/database/database.gd @@ -42,8 +42,7 @@ func _on_FirebaseAuth_logout() -> void: func get_database_reference(path : String, filter : Dictionary = {}) -> FirebaseDatabaseReference: var firebase_reference : FirebaseDatabaseReference = FirebaseDatabaseReference.new() var pusher : HTTPRequest = HTTPRequest.new() - if OS.get_name() == "HTML5" or OS.get_name() == "Web": - pusher.accept_gzip = false # Fixes broken gzip compression in web exports + Utilities.fix_http_request(pusher) pusher.use_threads = true var listener : Node = Node.new() listener.set_script(load("res://addons/http-sse-client/HTTPSSEClient.gd")) diff --git a/addons/godot-firebase/dynamiclinks/dynamiclinks.gd b/addons/godot-firebase/dynamiclinks/dynamiclinks.gd index 8575519..67a1374 100644 --- a/addons/godot-firebase/dynamiclinks/dynamiclinks.gd +++ b/addons/godot-firebase/dynamiclinks/dynamiclinks.gd @@ -32,8 +32,7 @@ enum Requests { func _set_config(config_json : Dictionary) -> void: _config = config_json _request_list_node = HTTPRequest.new() - if OS.get_name() == "HTML5" or OS.get_name() == "Web": - _request_list_node.accept_gzip = false # Fixes broken gzip compression in web exports + Utilities.fix_http_request(_request_list_node) _request_list_node.request_completed.connect(_on_request_completed) add_child(_request_list_node) _check_emulating() diff --git a/addons/godot-firebase/firestore/firestore.gd b/addons/godot-firebase/firestore/firestore.gd index 8a853cd..317c60e 100644 --- a/addons/godot-firebase/firestore/firestore.gd +++ b/addons/godot-firebase/firestore/firestore.gd @@ -264,8 +264,7 @@ func _pooled_request(task : FirestoreTask) -> void: if not http_request: http_request = HTTPRequest.new() http_request.timeout = 5 - if OS.get_name() == "HTML5" or OS.get_name() == "Web": - http_request.accept_gzip = false # Fixes broken gzip compression in web exports + Utilities.fix_http_request(http_request) _http_request_pool.append(http_request) add_child(http_request) http_request.request_completed.connect(_on_pooled_request_completed.bind(http_request)) diff --git a/addons/godot-firebase/functions/functions.gd b/addons/godot-firebase/functions/functions.gd index a73a4ba..78ccd0c 100644 --- a/addons/godot-firebase/functions/functions.gd +++ b/addons/godot-firebase/functions/functions.gd @@ -168,8 +168,7 @@ func _pooled_request(task : FunctionTask) -> void: if not http_request: http_request = HTTPRequest.new() - if OS.get_name() == "HTML5" or OS.get_name() == "Web": - http_request.accept_gzip = false # Fixes broken gzip compression in web exports + Utilities.fix_http_request(http_request) _http_request_pool.append(http_request) add_child(http_request) http_request.request_completed.connect(_on_pooled_request_completed.bind(http_request)) From b04b1317d1ba2a4c407994f9e5967939e2117a05 Mon Sep 17 00:00:00 2001 From: Nikhil verma <49404370+NIKHIL0VERMA@users.noreply.github.com> Date: Wed, 26 Jul 2023 15:44:18 +0530 Subject: [PATCH 13/49] Fixed error OAuth 2 fb login OAuth2 with Facebook give error auth.accestoken because of wrong respons reading. --- addons/godot-firebase/auth/auth.gd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/godot-firebase/auth/auth.gd b/addons/godot-firebase/auth/auth.gd index 2c7081f..77b30eb 100644 --- a/addons/godot-firebase/auth/auth.gd +++ b/addons/godot-firebase/auth/auth.gd @@ -346,7 +346,7 @@ func get_google_auth_manual(provider: AuthProvider = _local_provider) -> void: func _tcp_stream_timer() -> void: var peer : StreamPeer = tcp_server.take_connection() if peer != null: - var raw_result : String = peer.get_utf8_string(400) + var raw_result : String = peer.get_utf8_string(441) #https://discord.com/channels/794559524903714886/867410297333219338/1133130536198602752 if raw_result != "" and raw_result.begins_with("GET"): tcp_timer.stop() remove_child(tcp_timer) From 43ba2570c3110e4d07b7a890970741c1d563bf8d Mon Sep 17 00:00:00 2001 From: Nikhil verma <49404370+NIKHIL0VERMA@users.noreply.github.com> Date: Mon, 31 Jul 2023 21:42:37 +0530 Subject: [PATCH 14/49] Update auth.gd Removed discord link --- addons/godot-firebase/auth/auth.gd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/godot-firebase/auth/auth.gd b/addons/godot-firebase/auth/auth.gd index 77b30eb..c801b37 100644 --- a/addons/godot-firebase/auth/auth.gd +++ b/addons/godot-firebase/auth/auth.gd @@ -346,7 +346,7 @@ func get_google_auth_manual(provider: AuthProvider = _local_provider) -> void: func _tcp_stream_timer() -> void: var peer : StreamPeer = tcp_server.take_connection() if peer != null: - var raw_result : String = peer.get_utf8_string(441) #https://discord.com/channels/794559524903714886/867410297333219338/1133130536198602752 + var raw_result : String = peer.get_utf8_string(441) if raw_result != "" and raw_result.begins_with("GET"): tcp_timer.stop() remove_child(tcp_timer) From 2067fd6976397a22da21ad127098cd5940a7c840 Mon Sep 17 00:00:00 2001 From: Jason Zeederberg Date: Sun, 24 Sep 2023 18:13:57 +0200 Subject: [PATCH 15/49] prevent hanging await and return relative value --- .../firestore/firestore_task.gd | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/addons/godot-firebase/firestore/firestore_task.gd b/addons/godot-firebase/firestore/firestore_task.gd index 3fe53a4..def0736 100644 --- a/addons/godot-firebase/firestore/firestore_task.gd +++ b/addons/godot-firebase/firestore/firestore_task.gd @@ -24,22 +24,22 @@ extends RefCounted ## Emitted when a request is completed. The request can be successful or not successful: if not, an [code]error[/code] Dictionary will be passed as a result. ## @arg-types Variant signal task_finished(task) -## Emitted when a [code]add(document)[/code] request checked a [class FirebaseCollection] is successfully completed. [code]error()[/code] signal will be emitted otherwise. +## Emitted when a [code]add(document)[/code] request checked a [class FirebaseCollection] is successfully completed. [code]error()[/code] signal will be emitted otherwise and [code]null[/code] will be passed as a result.. ## @arg-types FirestoreDocument signal add_document(doc) -## Emitted when a [code]get(document)[/code] request checked a [class FirebaseCollection] is successfully completed. [code]error()[/code] signal will be emitted otherwise. +## Emitted when a [code]get(document)[/code] request checked a [class FirebaseCollection] is successfully completed. [code]error()[/code] signal will be emitted otherwise and [code]null[/code] will be passed as a result. ## @arg-types FirestoreDocument signal get_document(doc) -## Emitted when a [code]update(document)[/code] request checked a [class FirebaseCollection] is successfully completed. [code]error()[/code] signal will be emitted otherwise. +## Emitted when a [code]update(document)[/code] request checked a [class FirebaseCollection] is successfully completed. [code]error()[/code] signal will be emitted otherwise and [code]null[/code] will be passed as a result. ## @arg-types FirestoreDocument signal update_document(doc) -## Emitted when a [code]delete(document)[/code] request checked a [class FirebaseCollection] is successfully completed. [code]error()[/code] signal will be emitted otherwise. -## @arg-types FirestoreDocument -signal delete_document() -## Emitted when a [code]list(collection_id)[/code] request checked [class FirebaseFirestore] is successfully completed. [code]error()[/code] signal will be emitted otherwise. +## Emitted when a [code]delete(document)[/code] request checked a [class FirebaseCollection] is successfully completed and [code]true[/code] will be passed. [code]error()[/code] signal will be emitted otherwise and [code]false[/code] will be passed as a result. +## @arg-types bool +signal delete_document(success) +## Emitted when a [code]list(collection_id)[/code] request checked [class FirebaseFirestore] is successfully completed. [code]error()[/code] signal will be emitted otherwise and [code][][/code] will be passed as a result.. ## @arg-types Array signal listed_documents(documents) -## Emitted when a [code]query(collection_id)[/code] request checked [class FirebaseFirestore] is successfully completed. [code]error()[/code] signal will be emitted otherwise. +## Emitted when a [code]query(collection_id)[/code] request checked [class FirebaseFirestore] is successfully completed. [code]error()[/code] signal will be emitted otherwise and [code][][/code] will be passed as a result. ## @arg-types Array signal result_query(result) ## Emitted when a request is [b]not[/b] successfully completed. @@ -98,7 +98,7 @@ func _on_request_completed(result : int, response_code : int, headers : PackedSt document = FirestoreDocument.new(bod) update_document.emit(document) Task.TASK_DELETE: - delete_document.emit() + delete_document.emit(true) Task.TASK_QUERY: data = [] for doc in bod: @@ -116,6 +116,21 @@ func _on_request_completed(result : int, response_code : int, headers : PackedSt else: Firebase._printerr("Action in error was: " + str(action)) emit_error(task_error, bod, action) + match action: + Task.TASK_POST: + add_document.emit(null) + Task.TASK_GET: + get_document.emit(null) + Task.TASK_PATCH: + update_document.emit(null) + Task.TASK_DELETE: + delete_document.emit(false) + Task.TASK_QUERY: + data = [] + result_query.emit(data) + Task.TASK_LIST: + data = [] + listed_documents.emit(data) task_finished.emit(self) From b198bd84f5c2d15a933f43ebe058e19611f944ef Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Fri, 8 Dec 2023 11:39:32 -0500 Subject: [PATCH 16/49] Update for once functionality --- addons/godot-firebase/database/database.gd | 4 ++- addons/godot-firebase/database/reference.gd | 39 +++++++++++++++++++-- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/addons/godot-firebase/database/database.gd b/addons/godot-firebase/database/database.gd index 195b699..a2bf082 100644 --- a/addons/godot-firebase/database/database.gd +++ b/addons/godot-firebase/database/database.gd @@ -41,8 +41,9 @@ func _on_FirebaseAuth_logout() -> void: func get_database_reference(path : String, filter : Dictionary = {}) -> FirebaseDatabaseReference: var firebase_reference : FirebaseDatabaseReference = FirebaseDatabaseReference.new() + var getter := HTTPRequest.new() + getter.use_threads = true var pusher : HTTPRequest = HTTPRequest.new() - Utilities.fix_http_request(pusher) pusher.use_threads = true var listener : Node = Node.new() listener.set_script(load("res://addons/http-sse-client/HTTPSSEClient.gd")) @@ -50,6 +51,7 @@ func get_database_reference(path : String, filter : Dictionary = {}) -> Firebase firebase_reference.set_db_path(path, filter) firebase_reference.set_auth_and_config(_auth, _config) firebase_reference.set_pusher(pusher) + firebase_reference.set_getter(getter) firebase_reference.set_listener(listener) firebase_reference.set_store(store) add_child(firebase_reference) diff --git a/addons/godot-firebase/database/reference.gd b/addons/godot-firebase/database/reference.gd index 0c485cd..803da37 100644 --- a/addons/godot-firebase/database/reference.gd +++ b/addons/godot-firebase/database/reference.gd @@ -1,5 +1,5 @@ -## @meta-authors TODO -## @meta-version 2.3 +## @meta-authors BackAt50Ft +## @meta-version 2.4 ## A reference to a location in the Realtime Database. ## Documentation TODO. @tool @@ -10,9 +10,13 @@ signal new_data_update(data) signal patch_data_update(data) signal delete_data_update(data) +signal once_successful(dataSnapshot) +signal once_failed() + signal push_successful() signal push_failed() + const ORDER_BY : String = "orderBy" const LIMIT_TO_FIRST : String = "limitToFirst" const LIMIT_TO_LAST : String = "limitToLast" @@ -21,6 +25,7 @@ const END_AT : String = "endAt" const EQUAL_TO : String = "equalTo" var _pusher : HTTPRequest +var _getter : HTTPRequest var _listener : Node var _store : FirebaseDatabaseStore var _auth : Dictionary @@ -29,6 +34,7 @@ var _filter_query : Dictionary var _db_path : String var _cached_filter : String var _push_queue : Array = [] +var _get_queue : Array = [] var _update_queue : Array = [] var _delete_queue : Array = [] var _can_connect_to_host : bool = false @@ -64,6 +70,13 @@ func set_pusher(pusher_ref : HTTPRequest) -> void: add_child(_pusher) _pusher.request_completed.connect(on_push_request_complete) +func set_getter(getter_ref : HTTPRequest) -> void: + if !_getter: + _getter = getter_ref + add_child(_getter) + _getter.request_completed.connect(on_get_request_complete) + + func set_listener(listener_ref : Node) -> void: if !_listener: _listener = listener_ref @@ -126,6 +139,16 @@ func delete(reference : String) -> void: else: _delete_queue.append(reference) +# +# Gets a data snapshot once at the position passed in +# +func once(reference : String) -> void: + if _getter.get_http_client_status() == HTTPClient.STATUS_DISCONNECTED: + var ref_pos = _get_list_url() + _db_path + _separator + reference + _get_remaining_path() + _getter.request(ref_pos, _headers, HTTPClient.METHOD_GET, "") + else: + _get_queue.append(reference) + # # Returns a deep copy of the current local copy of the data stored at this reference in the Firebase # Realtime Database. @@ -204,3 +227,15 @@ func on_push_request_complete(result : int, response_code : int, headers : Packe if _delete_queue.size() > 0: delete(_delete_queue.pop_front()) + +func on_get_request_complete(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: + if response_code == HTTPClient.RESPONSE_OK: + var bod = Utilities.get_json_data(body) + once_successful.emit(bod) + else: + once_failed.emit() + + # handle queued operations + if _get_queue.size() > 0: + once(_get_queue.pop_front()) + From c943727d7ca3e4599a9bee94654bd7531141f662 Mon Sep 17 00:00:00 2001 From: Chuck Date: Wed, 17 Jan 2024 19:18:25 -0500 Subject: [PATCH 17/49] Add 'Web' to OS.get_name() for HTML and UWP exports --- addons/godot-firebase/firestore/firestore.gd | 2 +- addons/godot-firebase/functions/functions.gd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/godot-firebase/firestore/firestore.gd b/addons/godot-firebase/firestore/firestore.gd index 317c60e..2428ed8 100644 --- a/addons/godot-firebase/firestore/firestore.gd +++ b/addons/godot-firebase/firestore/firestore.gd @@ -68,7 +68,7 @@ var auth : Dictionary var _config : Dictionary = {} var _cache_loc: String -var _encrypt_key := "5vg76n90345f7w390346" if OS.get_name() in ["HTML5", "UWP"] else OS.get_unique_id() +var _encrypt_key := "5vg76n90345f7w390346" if OS.get_name() in ["HTML5", "UWP", "Web"] else OS.get_unique_id() var _base_url : String = "" diff --git a/addons/godot-firebase/functions/functions.gd b/addons/godot-firebase/functions/functions.gd index 78ccd0c..d76f3a2 100644 --- a/addons/godot-firebase/functions/functions.gd +++ b/addons/godot-firebase/functions/functions.gd @@ -42,7 +42,7 @@ var auth : Dictionary var _config : Dictionary = {} var _cache_loc: String -var _encrypt_key: String = "" if OS.get_name() in ["HTML5", "UWP"] else OS.get_unique_id() +var _encrypt_key: String = "" if OS.get_name() in ["HTML5", "UWP", "Web"] else OS.get_unique_id() var _base_url : String = "" From a9445fb853fa2a3de87952e7797fcbbebc5277d1 Mon Sep 17 00:00:00 2001 From: Boon Date: Sat, 27 Jan 2024 12:50:30 +0000 Subject: [PATCH 18/49] Replaced OS.has_feature('JavaScript') with OS.has_feature('web'), as the JavaScript feature tag is removed for Godot 4. --- addons/godot-firebase/auth/auth.gd | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/addons/godot-firebase/auth/auth.gd b/addons/godot-firebase/auth/auth.gd index c801b37..95442e3 100644 --- a/addons/godot-firebase/auth/auth.gd +++ b/addons/godot-firebase/auth/auth.gd @@ -146,7 +146,7 @@ var _local_provider : AuthProvider = AuthProvider.new() func _ready() -> void: tcp_timer.wait_time = tcp_timeout tcp_timer.timeout.connect(_tcp_stream_timer) - + Utilities.fix_http_request(self) if OS.get_name() == "HTML5" or OS.get_name() == "Web": _local_uri += "tmp_js_export.html" @@ -281,7 +281,7 @@ func get_auth_with_redirect(provider: AuthProvider) -> void: url_endpoint+=key+"="+provider.params[key]+"&" url_endpoint += provider.params.redirect_type+"="+_local_uri url_endpoint = _clean_url(url_endpoint) - if OS.get_name() == "HTML5" and OS.has_feature("JavaScript"): + if OS.get_name() == "HTML5" and OS.has_feature("web"): JavaScriptBridge.eval('window.location.replace("' + url_endpoint + '")') elif Engine.has_singleton(_INAPP_PLUGIN) and OS.get_name() == "iOS": #in app for ios if the iOS plugin exists @@ -334,7 +334,7 @@ func exchange_token(code : String, redirect_uri : String, request_url: String, _ if err != OK: is_busy = false Firebase._printerr("Error exchanging tokens: %s" % err) - + # Open a web page in browser redirecting to Google oAuth2 page for the current project # Once given user's authorization, a token will be generated. # NOTE** with this method, the authorization process will be copy-pasted @@ -357,13 +357,13 @@ func _tcp_stream_timer() -> void: if _local_provider.params.response_type in splitted[0]: token = splitted[1] break - + if token == "": login_failed.emit() peer.disconnect_from_host() tcp_server.stop() return - + var data : PackedByteArray = '

🔥 You can close this window now. 🔥

'.to_ascii_buffer() peer.put_data(("HTTP/1.1 200 OK\n").to_ascii_buffer()) peer.put_data(("Server: Godot Firebase SDK\n").to_ascii_buffer()) @@ -375,7 +375,7 @@ func _tcp_stream_timer() -> void: await self.login_succeeded peer.disconnect_from_host() tcp_server.stop() - + # Function used to logout of the system, this will also remove_at the local encrypted auth file if there is one func logout() -> void: @@ -419,7 +419,7 @@ func _on_FirebaseAuth_request_completed(result : int, response_code : int, heade Firebase._printerr("Error while parsing auth body json") auth_request.emit(ERR_PARSE_ERROR, "Error while parsing auth body json") return - + res = json if response_code == HTTPClient.RESPONSE_OK: @@ -619,8 +619,8 @@ func begin_refresh_countdown() -> void: func get_token_from_url(provider: AuthProvider): var token_type: String = provider.params.response_type if provider.params.response_type == "code" else "access_token" - if OS.has_feature('JavaScript'): - var token = JavaScriptBridge.eval(""" + if OS.has_feature('web'): + var token = JavaScriptBridge.eval(""" var url_string = window.location.href.replaceAll('?#', '?'); var url = new URL(url_string); url.searchParams.get('"""+token_type+"""'); From c2b0c73bd89cb497fbf175ff0f5bf885a48bfda2 Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Mon, 18 Mar 2024 07:19:18 -0400 Subject: [PATCH 19/49] Fix not login_succeeded if manual refresh --- addons/godot-firebase/auth/auth.gd | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/addons/godot-firebase/auth/auth.gd b/addons/godot-firebase/auth/auth.gd index 95442e3..1b824c1 100644 --- a/addons/godot-firebase/auth/auth.gd +++ b/addons/godot-firebase/auth/auth.gd @@ -431,6 +431,10 @@ func _on_FirebaseAuth_request_completed(result : int, response_code : int, heade begin_refresh_countdown() # Refresh token countdown auth_request.emit(1, auth) + + if _needs_refresh: + _needs_refresh = false + login_succeeded.emit(auth) else: match res.kind: RESPONSE_SIGNUP: From 2e2c302811e1980dec011d77ceef8d861436523d Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Tue, 19 Mar 2024 08:16:12 -0400 Subject: [PATCH 20/49] Add needs login method --- addons/godot-firebase/auth/auth.gd | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/addons/godot-firebase/auth/auth.gd b/addons/godot-firebase/auth/auth.gd index 1b824c1..ef35776 100644 --- a/addons/godot-firebase/auth/auth.gd +++ b/addons/godot-firebase/auth/auth.gd @@ -383,6 +383,11 @@ func logout() -> void: remove_auth() logged_out.emit() +# Checks to see if we need a hard login +func needs_login() -> bool: + var encrypted_file = FileAccess.open_encrypted_with_pass("user://user.auth", FileAccess.READ, _config.apiKey) + var err = encrypted_file == null + return err # Function is called when requesting a manual token refresh func manual_token_refresh(auth_data): From 226bdd546521a884902358c4f33e4121c1a408e9 Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Tue, 19 Mar 2024 09:31:07 -0400 Subject: [PATCH 21/49] Allow exported env file --- addons/godot-firebase/firebase/firebase.gd | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/addons/godot-firebase/firebase/firebase.gd b/addons/godot-firebase/firebase/firebase.gd index 0e63cae..f83eec9 100644 --- a/addons/godot-firebase/firebase/firebase.gd +++ b/addons/godot-firebase/firebase/firebase.gd @@ -15,6 +15,8 @@ const _ENVIRONMENT_VARIABLES : String = "firebase/environment_variables" const _EMULATORS_PORTS : String = "firebase/emulators/ports" const _AUTH_PROVIDERS : String = "firebase/auth_providers" +@export_global_file("*.env") var EnvPath : String + ## @type FirebaseAuth ## The Firebase Authentication API. @onready var Auth : FirebaseAuth = $Auth @@ -99,7 +101,7 @@ func _check_emulating() -> void: func _load_config() -> void: if not (_config.apiKey != "" and _config.authDomain != ""): var env = ConfigFile.new() - var err = env.load("res://addons/godot-firebase/.env") + var err = env.load("res://addons/godot-firebase/.env" if EnvPath == null else EnvPath) if err == OK: for key in _config.keys(): var config_value = _config[key] From 9b43b87c77cea838135c5ac8d91193c18d3f5fbe Mon Sep 17 00:00:00 2001 From: Vitor Augusto Pinheiro <26413854+Vitorgus@users.noreply.github.com> Date: Fri, 22 Mar 2024 15:01:32 -0300 Subject: [PATCH 22/49] fix: storage path now accepts space characters --- addons/godot-firebase/storage/storage.gd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/godot-firebase/storage/storage.gd b/addons/godot-firebase/storage/storage.gd index 1b4b93e..a4307c0 100644 --- a/addons/godot-firebase/storage/storage.gd +++ b/addons/godot-firebase/storage/storage.gd @@ -325,7 +325,7 @@ func _finish_request(result : int) -> void: func _get_file_url(ref : StorageReference) -> String: var url := _extended_url.replace("[APP_ID]", ref.bucket) url = url.replace("[API_VERSION]", _API_VERSION) - return url.replace("[FILE_PATH]", ref.full_path.replace("/", "%2F")) + return url.replace("[FILE_PATH]", ref.full_path.uri_encode()) # Removes any "../" or "./" in the file path. func _simplify_path(path : String) -> String: From 440fae34ff1301066fc222fd4da976599d8533db Mon Sep 17 00:00:00 2001 From: j Date: Tue, 26 Mar 2024 09:09:31 +0000 Subject: [PATCH 23/49] Added user-friendly descriptions of action codes for errors in Firestore Tasks --- addons/godot-firebase/firestore/firestore_task.gd | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/addons/godot-firebase/firestore/firestore_task.gd b/addons/godot-firebase/firestore/firestore_task.gd index def0736..7cf7bfa 100644 --- a/addons/godot-firebase/firestore/firestore_task.gd +++ b/addons/godot-firebase/firestore/firestore_task.gd @@ -55,6 +55,16 @@ enum Task { TASK_LIST ## A POST Request Task, processing a list() request } +## Mapping of Task enum values to descriptions for use in printing user-friendly error codes. +const TASK_MAP { + Task.TASK_GET: "GET DOCUMENT", + Task.TASK_POST: "ADD DOCUMENT", + Task.TASK_PATCH: "UPDATE DOCUMENT", + Task.TASK_DELETE: "DELETE DOCUMENT", + Task.TASK_QUERY: "QUERY COLLECTION", + Task.TASK_LIST: "LIST DOCUMENTS" +} + ## The code indicating the request Firestore is processing. ## See @[enum FirebaseFirestore.Requests] to get a full list of codes identifiers. ## @setter set_action @@ -114,7 +124,7 @@ func _on_request_completed(result : int, response_code : int, headers : PackedSt data.append(bod.nextPageToken) listed_documents.emit(data) else: - Firebase._printerr("Action in error was: " + str(action)) + Firebase._printerr("Action in error was: " + str(action) + " (" + TASK_MAP[action] + ")") emit_error(task_error, bod, action) match action: Task.TASK_POST: From 4f3852f84aee9563dc2d879d56fa6fb0182ee028 Mon Sep 17 00:00:00 2001 From: j Date: Tue, 26 Mar 2024 10:23:07 +0000 Subject: [PATCH 24/49] Ensure task description exists in map before printing --- addons/godot-firebase/firestore/firestore_task.gd | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/addons/godot-firebase/firestore/firestore_task.gd b/addons/godot-firebase/firestore/firestore_task.gd index 7cf7bfa..32d7e6a 100644 --- a/addons/godot-firebase/firestore/firestore_task.gd +++ b/addons/godot-firebase/firestore/firestore_task.gd @@ -124,7 +124,11 @@ func _on_request_completed(result : int, response_code : int, headers : PackedSt data.append(bod.nextPageToken) listed_documents.emit(data) else: - Firebase._printerr("Action in error was: " + str(action) + " (" + TASK_MAP[action] + ")") + var description = "" + if TASK_MAP.has(action): + description = "(" + TASK_MAP[action] + ")" + + Firebase._printerr("Action in error was: " + str(action) + " " + description) emit_error(task_error, bod, action) match action: Task.TASK_POST: From 5317bb4b3bf29db6da4e38cf13e09bccba77c1d7 Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Tue, 26 Mar 2024 06:50:11 -0400 Subject: [PATCH 25/49] Update .gitattributes --- .gitattributes | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitattributes b/.gitattributes index 1f7887f..66e603c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -25,3 +25,5 @@ /LICENSE export-ignore /README.md export-ignore /CODE_OF_CONDUCT.md export-ignore + +* text=auto eol=lf From e23e28b0ce1c77d9dc4047138214ce3d539667f7 Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Tue, 26 Mar 2024 07:14:45 -0400 Subject: [PATCH 26/49] Updates from the test harness (#390) * Add is_web func * Switch to is_web() from utils * Switch to is_web() in utils * Update firebase.gd * Switch to is_web() in utils * Switch to is_web() in utils * Switch to is_web() in utils * Create example.env * Update version --- addons/godot-firebase/Utilies.gd | 7 ++++-- addons/godot-firebase/auth/auth.gd | 4 ++-- .../godot-firebase/auth/providers/facebook.gd | 2 +- addons/godot-firebase/example.env | 24 +++++++++++++++++++ addons/godot-firebase/firebase/firebase.gd | 6 ++--- addons/godot-firebase/firestore/firestore.gd | 2 +- addons/godot-firebase/functions/functions.gd | 2 +- addons/godot-firebase/plugin.cfg | 2 +- .../storage/storage_reference.gd | 2 +- 9 files changed, 39 insertions(+), 12 deletions(-) create mode 100644 addons/godot-firebase/example.env diff --git a/addons/godot-firebase/Utilies.gd b/addons/godot-firebase/Utilies.gd index 3b7143b..ed94fe4 100644 --- a/addons/godot-firebase/Utilies.gd +++ b/addons/godot-firebase/Utilies.gd @@ -16,5 +16,8 @@ static func get_json_data(value): # This appears to be caused by the gzip compression being unsupported, so we # disable it when web export is detected. static func fix_http_request(http_request): - if OS.get_name() == "HTML5" or OS.get_name() == "Web": - http_request.accept_gzip = false \ No newline at end of file + if is_web(): + http_request.accept_gzip = false + +static func is_web() -> bool: + return OS.get_name() in ["HTML5", "Web"] diff --git a/addons/godot-firebase/auth/auth.gd b/addons/godot-firebase/auth/auth.gd index ef35776..dc47f14 100644 --- a/addons/godot-firebase/auth/auth.gd +++ b/addons/godot-firebase/auth/auth.gd @@ -148,7 +148,7 @@ func _ready() -> void: tcp_timer.timeout.connect(_tcp_stream_timer) Utilities.fix_http_request(self) - if OS.get_name() == "HTML5" or OS.get_name() == "Web": + if Utilities.is_web(): _local_uri += "tmp_js_export.html" @@ -281,7 +281,7 @@ func get_auth_with_redirect(provider: AuthProvider) -> void: url_endpoint+=key+"="+provider.params[key]+"&" url_endpoint += provider.params.redirect_type+"="+_local_uri url_endpoint = _clean_url(url_endpoint) - if OS.get_name() == "HTML5" and OS.has_feature("web"): + if Utilities.is_web() and OS.has_feature("JavaScript"): JavaScriptBridge.eval('window.location.replace("' + url_endpoint + '")') elif Engine.has_singleton(_INAPP_PLUGIN) and OS.get_name() == "iOS": #in app for ios if the iOS plugin exists diff --git a/addons/godot-firebase/auth/providers/facebook.gd b/addons/godot-firebase/auth/providers/facebook.gd index 9dc4b1d..926c4a1 100644 --- a/addons/godot-firebase/auth/providers/facebook.gd +++ b/addons/godot-firebase/auth/providers/facebook.gd @@ -11,7 +11,7 @@ func _init(client_id: String,client_secret: String): self.provider_id = "facebook.com" self.params.scope = "public_profile" self.params.state = str(randf_range(0, 1)) - if OS.get_name() == "HTML5": + if Utilities.is_web(): self.should_exchange = false self.params.response_type = "token" else: diff --git a/addons/godot-firebase/example.env b/addons/godot-firebase/example.env new file mode 100644 index 0000000..e35f66a --- /dev/null +++ b/addons/godot-firebase/example.env @@ -0,0 +1,24 @@ +[firebase/environment_variables] + +"apiKey"="", +"authDomain"="", +"databaseURL"="", +"projectId"="", +"storageBucket"="", +"messagingSenderId"="", +"appId"="", +"measurementId"="" +"clientId"="" +"clientSecret"="" +"domainUriPrefix"="" +"functionsGeoZone"="" +"cacheLocation"="" + +[firebase/emulators/ports] + +authentication="" +firestore="" +realtimeDatabase="" +functions="" +storage="" +dynamicLinks="" diff --git a/addons/godot-firebase/firebase/firebase.gd b/addons/godot-firebase/firebase/firebase.gd index f83eec9..5e6d681 100644 --- a/addons/godot-firebase/firebase/firebase.gd +++ b/addons/godot-firebase/firebase/firebase.gd @@ -59,7 +59,7 @@ var _config : Dictionary = { "clientSecret" : "", "domainUriPrefix" : "", "functionsGeoZone" : "", - "cacheLocation":"user://.firebase_cache", + "cacheLocation":"", "emulators": { "ports" : { "authentication" : "", @@ -136,7 +136,7 @@ func _setup_modules() -> void: # ------------- func _printerr(error : String) -> void: - printerr("[Firebase Error] >> "+error) + printerr("[Firebase Error] >> " + error) func _print(msg : String) -> void: - print("[Firebase] >> "+msg) + print("[Firebase] >> " + str(msg)) diff --git a/addons/godot-firebase/firestore/firestore.gd b/addons/godot-firebase/firestore/firestore.gd index 2428ed8..97e37a0 100644 --- a/addons/godot-firebase/firestore/firestore.gd +++ b/addons/godot-firebase/firestore/firestore.gd @@ -68,7 +68,7 @@ var auth : Dictionary var _config : Dictionary = {} var _cache_loc: String -var _encrypt_key := "5vg76n90345f7w390346" if OS.get_name() in ["HTML5", "UWP", "Web"] else OS.get_unique_id() +var _encrypt_key := "5vg76n90345f7w390346" if Utilities.is_web() else OS.get_unique_id() var _base_url : String = "" diff --git a/addons/godot-firebase/functions/functions.gd b/addons/godot-firebase/functions/functions.gd index d76f3a2..b1567e6 100644 --- a/addons/godot-firebase/functions/functions.gd +++ b/addons/godot-firebase/functions/functions.gd @@ -42,7 +42,7 @@ var auth : Dictionary var _config : Dictionary = {} var _cache_loc: String -var _encrypt_key: String = "" if OS.get_name() in ["HTML5", "UWP", "Web"] else OS.get_unique_id() +var _encrypt_key: String = "" if Utilities.is_web() else OS.get_unique_id() var _base_url : String = "" diff --git a/addons/godot-firebase/plugin.cfg b/addons/godot-firebase/plugin.cfg index 5636e73..b8ffaac 100644 --- a/addons/godot-firebase/plugin.cfg +++ b/addons/godot-firebase/plugin.cfg @@ -3,5 +3,5 @@ name="GodotFirebase" description="Google Firebase SDK written in GDScript for use in Godot Engine 4.0 projects." author="GodotNutsOrg" -version="1.0" +version="1.1" script="plugin.gd" diff --git a/addons/godot-firebase/storage/storage_reference.gd b/addons/godot-firebase/storage/storage_reference.gd index 3cad552..7f13ae2 100644 --- a/addons/godot-firebase/storage/storage_reference.gd +++ b/addons/godot-firebase/storage/storage_reference.gd @@ -83,7 +83,7 @@ func child(path : String) -> StorageReference: func put_data(data : PackedByteArray, metadata := {}) -> StorageTask: if not valid: return null - if not "Content-Length" in metadata and OS.get_name() != "HTML5": + if not "Content-Length" in metadata and not Utilities.is_web(): metadata["Content-Length"] = data.size() var headers := [] From 1dcd9872cc21264b02c8c17e6136a6e7b86e6a07 Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Tue, 26 Mar 2024 11:17:52 -0400 Subject: [PATCH 27/49] Fix bug in task map def --- addons/godot-firebase/firestore/firestore_task.gd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/godot-firebase/firestore/firestore_task.gd b/addons/godot-firebase/firestore/firestore_task.gd index 32d7e6a..ec24974 100644 --- a/addons/godot-firebase/firestore/firestore_task.gd +++ b/addons/godot-firebase/firestore/firestore_task.gd @@ -56,7 +56,7 @@ enum Task { } ## Mapping of Task enum values to descriptions for use in printing user-friendly error codes. -const TASK_MAP { +const TASK_MAP = { Task.TASK_GET: "GET DOCUMENT", Task.TASK_POST: "ADD DOCUMENT", Task.TASK_PATCH: "UPDATE DOCUMENT", From 5ae050f238f97a2b5dc9a7087a336d7c0a0b2bca Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Tue, 26 Mar 2024 19:55:42 -0400 Subject: [PATCH 28/49] Fix slight bug in EnvPath --- addons/godot-firebase/firebase/firebase.gd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/godot-firebase/firebase/firebase.gd b/addons/godot-firebase/firebase/firebase.gd index 5e6d681..2fbf47e 100644 --- a/addons/godot-firebase/firebase/firebase.gd +++ b/addons/godot-firebase/firebase/firebase.gd @@ -101,7 +101,7 @@ func _check_emulating() -> void: func _load_config() -> void: if not (_config.apiKey != "" and _config.authDomain != ""): var env = ConfigFile.new() - var err = env.load("res://addons/godot-firebase/.env" if EnvPath == null else EnvPath) + var err = env.load("res://addons/godot-firebase/.env" if EnvPath == "" else EnvPath) if err == OK: for key in _config.keys(): var config_value = _config[key] From 30c30294e54f42c7a98044cc280d6896ba199860 Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Tue, 26 Mar 2024 22:59:53 -0400 Subject: [PATCH 29/49] Fix attempt to export env file --- addons/godot-firebase/firebase/firebase.gd | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/addons/godot-firebase/firebase/firebase.gd b/addons/godot-firebase/firebase/firebase.gd index 2fbf47e..a3c787c 100644 --- a/addons/godot-firebase/firebase/firebase.gd +++ b/addons/godot-firebase/firebase/firebase.gd @@ -15,8 +15,6 @@ const _ENVIRONMENT_VARIABLES : String = "firebase/environment_variables" const _EMULATORS_PORTS : String = "firebase/emulators/ports" const _AUTH_PROVIDERS : String = "firebase/auth_providers" -@export_global_file("*.env") var EnvPath : String - ## @type FirebaseAuth ## The Firebase Authentication API. @onready var Auth : FirebaseAuth = $Auth @@ -101,7 +99,7 @@ func _check_emulating() -> void: func _load_config() -> void: if not (_config.apiKey != "" and _config.authDomain != ""): var env = ConfigFile.new() - var err = env.load("res://addons/godot-firebase/.env" if EnvPath == "" else EnvPath) + var err = env.load("res://addons/godot-firebase/.env") if err == OK: for key in _config.keys(): var config_value = _config[key] From 9a2d59691617fc0fb2b29c12a45043a9988a2a45 Mon Sep 17 00:00:00 2001 From: Matthew D'Souza <64600706+matth3wdsouza@users.noreply.github.com> Date: Wed, 1 May 2024 17:59:53 +0530 Subject: [PATCH 30/49] Remove auth file after successful account deletion (#397) * fix: remove auth file on successful account deletion * fix: use spaces for indentation instead of tabs --- addons/godot-firebase/auth/auth.gd | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/addons/godot-firebase/auth/auth.gd b/addons/godot-firebase/auth/auth.gd index dc47f14..915433a 100644 --- a/addons/godot-firebase/auth/auth.gd +++ b/addons/godot-firebase/auth/auth.gd @@ -437,9 +437,9 @@ func _on_FirebaseAuth_request_completed(result : int, response_code : int, heade # Refresh token countdown auth_request.emit(1, auth) - if _needs_refresh: - _needs_refresh = false - login_succeeded.emit(auth) + if _needs_refresh: + _needs_refresh = false + login_succeeded.emit(auth) else: match res.kind: RESPONSE_SIGNUP: @@ -601,6 +601,8 @@ func delete_user_account() -> void: if err != OK: is_busy = false Firebase._printerr("Error deleting user: %s" % err) + else: + remove_auth() # Function is called when a new token is issued to a user. The function will yield until the token has expired, and then request a new one. From 569ad208ea9797483cb55db801fc1e6970656d6c Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Sat, 25 May 2024 13:11:33 -0400 Subject: [PATCH 31/49] Refactor for realtime database, remote config, Firestore fixes --- addons/godot-firebase/Utilies.gd | 23 - addons/godot-firebase/Utilities.gd | 135 ++++ addons/godot-firebase/auth/auth.gd | 732 +++++++++--------- .../godot-firebase/auth/providers/facebook.gd | 34 +- .../godot-firebase/auth/providers/google.gd | 18 +- addons/godot-firebase/database/database.gd | 59 +- .../godot-firebase/database/database_store.gd | 102 +-- .../database/firebase_database_reference.tscn | 17 + .../firebase_once_database_reference.tscn | 16 + .../godot-firebase/database/once_reference.gd | 124 +++ addons/godot-firebase/database/reference.gd | 239 +++--- addons/godot-firebase/database/resource.gd | 6 +- .../dynamiclinks/dynamiclinks.gd | 118 +-- addons/godot-firebase/firebase/firebase.gd | 168 ++-- addons/godot-firebase/firebase/firebase.tscn | 6 +- .../firestore/field_transform.gd | 22 + .../firestore/field_transform_array.gd | 35 + .../field_transforms/decrement_transform.gd | 19 + .../field_transforms/increment_transform.gd | 19 + .../field_transforms/max_transform.gd | 19 + .../field_transforms/min_transform.gd | 19 + .../server_timestamp_transform.gd | 10 + addons/godot-firebase/firestore/firestore.gd | 273 ++++--- .../firestore/firestore_collection.gd | 166 ++-- .../firestore/firestore_document.gd | 253 +++--- .../firestore/firestore_query.gd | 282 +++---- .../firestore/firestore_task.gd | 253 +++--- .../firestore/firestore_transform.gd | 3 + .../godot-firebase/functions/function_task.gd | 24 +- addons/godot-firebase/functions/functions.gd | 196 ++--- .../queues/queueable_http_request.gd | 30 + .../queues/queueable_http_request.tscn | 6 + .../remote_config/firebase_remote_config.gd | 36 + .../remote_config/firebase_remote_config.tscn | 7 + .../remote_config/remote_config.gd | 14 + addons/godot-firebase/storage/storage.gd | 534 ++++++------- .../storage/storage_reference.gd | 154 ++-- 37 files changed, 2336 insertions(+), 1835 deletions(-) delete mode 100644 addons/godot-firebase/Utilies.gd create mode 100644 addons/godot-firebase/Utilities.gd create mode 100644 addons/godot-firebase/database/firebase_database_reference.tscn create mode 100644 addons/godot-firebase/database/firebase_once_database_reference.tscn create mode 100644 addons/godot-firebase/database/once_reference.gd create mode 100644 addons/godot-firebase/firestore/field_transform.gd create mode 100644 addons/godot-firebase/firestore/field_transform_array.gd create mode 100644 addons/godot-firebase/firestore/field_transforms/decrement_transform.gd create mode 100644 addons/godot-firebase/firestore/field_transforms/increment_transform.gd create mode 100644 addons/godot-firebase/firestore/field_transforms/max_transform.gd create mode 100644 addons/godot-firebase/firestore/field_transforms/min_transform.gd create mode 100644 addons/godot-firebase/firestore/field_transforms/server_timestamp_transform.gd create mode 100644 addons/godot-firebase/firestore/firestore_transform.gd create mode 100644 addons/godot-firebase/queues/queueable_http_request.gd create mode 100644 addons/godot-firebase/queues/queueable_http_request.tscn create mode 100644 addons/godot-firebase/remote_config/firebase_remote_config.gd create mode 100644 addons/godot-firebase/remote_config/firebase_remote_config.tscn create mode 100644 addons/godot-firebase/remote_config/remote_config.gd diff --git a/addons/godot-firebase/Utilies.gd b/addons/godot-firebase/Utilies.gd deleted file mode 100644 index ed94fe4..0000000 --- a/addons/godot-firebase/Utilies.gd +++ /dev/null @@ -1,23 +0,0 @@ -extends Node -class_name Utilities - -static func get_json_data(value): - if value is PackedByteArray: - value = value.get_string_from_utf8() - var json = JSON.new() - var json_parse_result = json.parse(value) - if json_parse_result == OK: - return json.data - - return null - - -# HTTPRequeust seems to have an issue in Web exports where the body returns empty -# This appears to be caused by the gzip compression being unsupported, so we -# disable it when web export is detected. -static func fix_http_request(http_request): - if is_web(): - http_request.accept_gzip = false - -static func is_web() -> bool: - return OS.get_name() in ["HTML5", "Web"] diff --git a/addons/godot-firebase/Utilities.gd b/addons/godot-firebase/Utilities.gd new file mode 100644 index 0000000..bd6c26f --- /dev/null +++ b/addons/godot-firebase/Utilities.gd @@ -0,0 +1,135 @@ +extends Node +class_name Utilities + +static func get_json_data(value): + if value is PackedByteArray: + value = value.get_string_from_utf8() + var json = JSON.new() + var json_parse_result = json.parse(value) + if json_parse_result == OK: + return json.data + + return null + + +# HTTPRequeust seems to have an issue in Web exports where the body returns empty +# This appears to be caused by the gzip compression being unsupported, so we +# disable it when web export is detected. +static func fix_http_request(http_request): + if is_web(): + http_request.accept_gzip = false + +static func is_web() -> bool: + return OS.get_name() in ["HTML5", "Web"] + + +class MultiSignal extends RefCounted: + signal completed(with_signal) + signal all_completed() + + var _has_signaled := false + var _early_exit := false + + var signal_count := 0 + + func _init(sigs : Array[Signal], early_exit := true, should_oneshot := true) -> void: + _early_exit = early_exit + for sig in sigs: + add_signal(sig, should_oneshot) + + func add_signal(sig : Signal, should_oneshot) -> void: + signal_count += 1 + sig.connect( + func(): + if not _has_signaled and _early_exit: + completed.emit(sig) + _has_signaled = true + elif not _early_exit: + completed.emit(sig) + signal_count -= 1 + if signal_count <= 0: # Not sure how it could be less than + all_completed.emit() + , CONNECT_ONE_SHOT if should_oneshot else CONNECT_REFERENCE_COUNTED + ) + +class SignalReducer extends RefCounted: # No need for a node, as this deals strictly with signals, which can be on any object. + signal completed + + var awaiters : Array[Signal] = [] + + var reducers = { + 0 : completed.emit, + 1 : func(p): completed.emit(), + 2 : func(p1, p2): completed.emit(), + 3 : func(p1, p2, p3): completed.emit(), + 4 : func(p1, p2, p3, p4): completed.emit() + } + + func add_signal(sig : Signal, param_count : int = 0) -> void: + assert(param_count < 5, "Too many parameters to reduce, just add more!") + sig.connect(reducers[param_count], CONNECT_ONE_SHOT) # May wish to not just one-shot, but instead track all of them firing + +class SignalReducerWithResult extends RefCounted: # No need for a node, as this deals strictly with signals, which can be on any object. + signal completed(result) + + var awaiters : Array[Signal] = [] + + var reducers = { + 0 : completed.emit, + 1 : func(p): completed.emit({1 : p}), + 2 : func(p1, p2): completed.emit({ 1 : p1, 2 : p2 }), + 3 : func(p1, p2, p3): completed.emit({ 1 : p1, 2 : p2, 3 : p3 }), + 4 : func(p1, p2, p3, p4): completed.emit({ 1 : p1, 2 : p2, 3 : p3, 4 : p4 }) + } + + func add_signal(sig : Signal, param_count : int = 0) -> void: + assert(param_count < 5, "Too many parameters to reduce, just add more!") + sig.connect(reducers[param_count], CONNECT_ONE_SHOT) # May wish to not just one-shot, but instead track all of them firing + +class ObservableDictionary extends RefCounted: + signal keys_changed() + + var _internal : Dictionary + var is_notifying := true + + func _init(copy : Dictionary = {}) -> void: + _internal = copy + + func add(key : Variant, value : Variant) -> void: + _internal[key] = value + if is_notifying: + keys_changed.emit() + + func update(key : Variant, value : Variant) -> void: + _internal[key] = value + if is_notifying: + keys_changed.emit() + + func has(key : Variant) -> bool: + return _internal.has(key) + + func keys(): + return _internal.keys() + + func values(): + return _internal.values() + + func erase(key : Variant) -> bool: + var result = _internal.erase(key) + if is_notifying: + keys_changed.emit() + + return result + + func get_value(key : Variant) -> Variant: + return _internal[key] + + func _get(property: StringName) -> Variant: + if _internal.has(property): + return _internal[property] + + return false + + func _set(property: StringName, value: Variant) -> bool: + update(property, value) + return true diff --git a/addons/godot-firebase/auth/auth.gd b/addons/godot-firebase/auth/auth.gd index 915433a..1cd4923 100644 --- a/addons/godot-firebase/auth/auth.gd +++ b/addons/godot-firebase/auth/auth.gd @@ -55,88 +55,88 @@ var tcp_timer : Timer = Timer.new() var tcp_timeout : float = 0.5 var _headers : PackedStringArray = [ - "Content-Type: application/json", - "Accept: application/json", + "Content-Type: application/json", + "Accept: application/json", ] var requesting : int = -1 enum Requests { - NONE = -1, - EXCHANGE_TOKEN, - LOGIN_WITH_OAUTH + NONE = -1, + EXCHANGE_TOKEN, + LOGIN_WITH_OAUTH } var auth_request_type : int = -1 enum Auth_Type { - NONE = -1, - LOGIN_EP, - LOGIN_ANON, - LOGIN_CT, - LOGIN_OAUTH, - SIGNUP_EP + NONE = -1, + LOGIN_EP, + LOGIN_ANON, + LOGIN_CT, + LOGIN_OAUTH, + SIGNUP_EP } var _login_request_body : Dictionary = { - "email":"", - "password":"", - "returnSecureToken": true, + "email":"", + "password":"", + "returnSecureToken": true, } var _oauth_login_request_body : Dictionary = { - "postBody":"", - "requestUri":"", - "returnIdpCredential":false, - "returnSecureToken":true + "postBody":"", + "requestUri":"", + "returnIdpCredential":false, + "returnSecureToken":true } var _anonymous_login_request_body : Dictionary = { - "returnSecureToken":true + "returnSecureToken":true } var _refresh_request_body : Dictionary = { - "grant_type":"refresh_token", - "refresh_token":"", + "grant_type":"refresh_token", + "refresh_token":"", } var _custom_token_body : Dictionary = { - "token":"", - "returnSecureToken":true + "token":"", + "returnSecureToken":true } var _password_reset_body : Dictionary = { - "requestType":"password_reset", - "email":"", + "requestType":"password_reset", + "email":"", } var _change_email_body : Dictionary = { - "idToken":"", - "email":"", - "returnSecureToken": true, + "idToken":"", + "email":"", + "returnSecureToken": true, } var _change_password_body : Dictionary = { - "idToken":"", - "password":"", - "returnSecureToken": true, + "idToken":"", + "password":"", + "returnSecureToken": true, } var _account_verification_body : Dictionary = { - "requestType":"verify_email", - "idToken":"", + "requestType":"verify_email", + "idToken":"", } var _update_profile_body : Dictionary = { - "idToken":"", - "displayName":"", - "photoUrl":"", - "deleteAttribute":"", - "returnSecureToken":true + "idToken":"", + "displayName":"", + "photoUrl":"", + "deleteAttribute":"", + "returnSecureToken":true } var _local_port : int = 8060 @@ -144,81 +144,81 @@ var _local_uri : String = "http://localhost:%s/"%_local_port var _local_provider : AuthProvider = AuthProvider.new() func _ready() -> void: - tcp_timer.wait_time = tcp_timeout - tcp_timer.timeout.connect(_tcp_stream_timer) + tcp_timer.wait_time = tcp_timeout + tcp_timer.timeout.connect(_tcp_stream_timer) - Utilities.fix_http_request(self) - if Utilities.is_web(): - _local_uri += "tmp_js_export.html" + Utilities.fix_http_request(self) + if Utilities.is_web(): + _local_uri += "tmp_js_export.html" # Sets the configuration needed for the plugin to talk to Firebase # These settings come from the Firebase.gd script automatically func _set_config(config_json : Dictionary) -> void: - _config = config_json - _signup_request_url %= _config.apiKey - _signin_request_url %= _config.apiKey - _signin_custom_token_url %= _config.apiKey - _signin_with_oauth_request_url %= _config.apiKey - _userdata_request_url %= _config.apiKey - _refresh_request_url %= _config.apiKey - _oobcode_request_url %= _config.apiKey - _delete_account_request_url %= _config.apiKey - _update_account_request_url %= _config.apiKey + _config = config_json + _signup_request_url %= _config.apiKey + _signin_request_url %= _config.apiKey + _signin_custom_token_url %= _config.apiKey + _signin_with_oauth_request_url %= _config.apiKey + _userdata_request_url %= _config.apiKey + _refresh_request_url %= _config.apiKey + _oobcode_request_url %= _config.apiKey + _delete_account_request_url %= _config.apiKey + _update_account_request_url %= _config.apiKey - request_completed.connect(_on_FirebaseAuth_request_completed) - _check_emulating() + request_completed.connect(_on_FirebaseAuth_request_completed) + _check_emulating() func _check_emulating() -> void : - ## Check emulating - if not Firebase.emulating: - _base_url = "https://identitytoolkit.googleapis.com/{version}/".format({ version = _API_VERSION }) - _refresh_request_base_url = "https://securetoken.googleapis.com" - else: - var port : String = _config.emulators.ports.authentication - if port == "": - Firebase._printerr("You are in 'emulated' mode, but the port for Authentication has not been configured.") - else: - _base_url = "http://localhost:{port}/identitytoolkit.googleapis.com/{version}/".format({ version = _API_VERSION ,port = port }) - _refresh_request_base_url = "http://localhost:{port}/securetoken.googleapis.com".format({port = port}) + ## Check emulating + if not Firebase.emulating: + _base_url = "https://identitytoolkit.googleapis.com/{version}/".format({ version = _API_VERSION }) + _refresh_request_base_url = "https://securetoken.googleapis.com" + else: + var port : String = _config.emulators.ports.authentication + if port == "": + Firebase._printerr("You are in 'emulated' mode, but the port for Authentication has not been configured.") + else: + _base_url = "http://localhost:{port}/identitytoolkit.googleapis.com/{version}/".format({ version = _API_VERSION ,port = port }) + _refresh_request_base_url = "http://localhost:{port}/securetoken.googleapis.com".format({port = port}) # Function is used to check if the auth script is ready to process a request. Returns true if it is not currently processing # If false it will print an error func _is_ready() -> bool: - if is_busy: - Firebase._printerr("Firebase Auth is currently busy and cannot process this request") - return false - else: - return true + if is_busy: + Firebase._printerr("Firebase Auth is currently busy and cannot process this request") + return false + else: + return true # Function cleans the URI and replaces spaces with %20 # As of right now we only replace spaces # We may need to decide to use the uri_encode() String function func _clean_url(_url): - _url = _url.replace(' ','%20') - return _url + _url = _url.replace(' ','%20') + return _url # Synchronous call to check if any user is already logged in. func is_logged_in() -> bool: - return auth != null and auth.has("idtoken") + return auth != null and auth.has("idtoken") # Called with Firebase.Auth.signup_with_email_and_password(email, password) # You must pass in the email and password to this function for it to work correctly func signup_with_email_and_password(email : String, password : String) -> void: - if _is_ready(): - is_busy = true - _login_request_body.email = email - _login_request_body.password = password - auth_request_type = Auth_Type.SIGNUP_EP - var err = request(_base_url + _signup_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_login_request_body)) - _login_request_body.email = "" - _login_request_body.password = "" - if err != OK: - is_busy = false - Firebase._printerr("Error signing up with password and email: %s" % err) + if _is_ready(): + is_busy = true + _login_request_body.email = email + _login_request_body.password = password + auth_request_type = Auth_Type.SIGNUP_EP + var err = request(_base_url + _signup_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_login_request_body)) + _login_request_body.email = "" + _login_request_body.password = "" + if err != OK: + is_busy = false + Firebase._printerr("Error signing up with password and email: %s" % err) # Called with Firebase.Auth.anonymous_login() @@ -226,216 +226,216 @@ func signup_with_email_and_password(email : String, password : String) -> void: # The response contains the Firebase ID token and refresh token associated with the anonymous user. # The 'mail' field will be empty since no email is linked to an anonymous user func login_anonymous() -> void: - if _is_ready(): - is_busy = true - auth_request_type = Auth_Type.LOGIN_ANON - var err = request(_base_url + _signup_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_anonymous_login_request_body)) - if err != OK: - is_busy = false - Firebase._printerr("Error logging in as anonymous: %s" % err) + if _is_ready(): + is_busy = true + auth_request_type = Auth_Type.LOGIN_ANON + var err = request(_base_url + _signup_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_anonymous_login_request_body)) + if err != OK: + is_busy = false + Firebase._printerr("Error logging in as anonymous: %s" % err) # Called with Firebase.Auth.login_with_email_and_password(email, password) # You must pass in the email and password to this function for it to work correctly # If the login fails it will return an error code through the function _on_FirebaseAuth_request_completed func login_with_email_and_password(email : String, password : String) -> void: - if _is_ready(): - is_busy = true - _login_request_body.email = email - _login_request_body.password = password - auth_request_type = Auth_Type.LOGIN_EP - var err = request(_base_url + _signin_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_login_request_body)) - _login_request_body.email = "" - _login_request_body.password = "" - if err != OK: - is_busy = false - Firebase._printerr("Error logging in with password and email: %s" % err) + if _is_ready(): + is_busy = true + _login_request_body.email = email + _login_request_body.password = password + auth_request_type = Auth_Type.LOGIN_EP + var err = request(_base_url + _signin_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_login_request_body)) + _login_request_body.email = "" + _login_request_body.password = "" + if err != OK: + is_busy = false + Firebase._printerr("Error logging in with password and email: %s" % err) # Login with a custom valid token # The token needs to be generated using an external service/function func login_with_custom_token(token : String) -> void: - if _is_ready(): - is_busy = true - _custom_token_body.token = token - auth_request_type = Auth_Type.LOGIN_CT - var err = request(_base_url + _signin_custom_token_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_custom_token_body)) - if err != OK: - is_busy = false - Firebase._printerr("Error logging in with custom token: %s" % err) + if _is_ready(): + is_busy = true + _custom_token_body.token = token + auth_request_type = Auth_Type.LOGIN_CT + var err = request(_base_url + _signin_custom_token_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_custom_token_body)) + if err != OK: + is_busy = false + Firebase._printerr("Error logging in with custom token: %s" % err) # Open a web page in browser redirecting to Google oAuth2 page for the current project # Once given user's authorization, a token will be generated. # NOTE** the generated token will be automatically captured and a login request will be made if the token is correct func get_auth_localhost(provider: AuthProvider = get_GoogleProvider(), port : int = _local_port): - get_auth_with_redirect(provider) - await get_tree().create_timer(0.5).timeout - if has_child == false: - add_child(tcp_timer) - has_child = true - tcp_timer.start() - tcp_server.listen(port, "*") + get_auth_with_redirect(provider) + await get_tree().create_timer(0.5).timeout + if has_child == false: + add_child(tcp_timer) + has_child = true + tcp_timer.start() + tcp_server.listen(port, "*") func get_auth_with_redirect(provider: AuthProvider) -> void: - var url_endpoint: String = provider.redirect_uri - for key in provider.params.keys(): - url_endpoint+=key+"="+provider.params[key]+"&" - url_endpoint += provider.params.redirect_type+"="+_local_uri - url_endpoint = _clean_url(url_endpoint) - if Utilities.is_web() and OS.has_feature("JavaScript"): - JavaScriptBridge.eval('window.location.replace("' + url_endpoint + '")') - elif Engine.has_singleton(_INAPP_PLUGIN) and OS.get_name() == "iOS": - #in app for ios if the iOS plugin exists - set_local_provider(provider) - Engine.get_singleton(_INAPP_PLUGIN).popup(url_endpoint) - else: - set_local_provider(provider) - OS.shell_open(url_endpoint) - print(url_endpoint) + var url_endpoint: String = provider.redirect_uri + for key in provider.params.keys(): + url_endpoint+=key+"="+provider.params[key]+"&" + url_endpoint += provider.params.redirect_type+"="+_local_uri + url_endpoint = _clean_url(url_endpoint) + if Utilities.is_web() and OS.has_feature("JavaScript"): + JavaScriptBridge.eval('window.location.replace("' + url_endpoint + '")') + elif Engine.has_singleton(_INAPP_PLUGIN) and OS.get_name() == "iOS": + #in app for ios if the iOS plugin exists + set_local_provider(provider) + Engine.get_singleton(_INAPP_PLUGIN).popup(url_endpoint) + else: + set_local_provider(provider) + OS.shell_open(url_endpoint) + print(url_endpoint) # Login with Google oAuth2. # A token is automatically obtained using an authorization code using @get_google_auth() # @provider_id and @request_uri can be changed func login_with_oauth(_token: String, provider: AuthProvider) -> void: - if _token: - var token : String = _token.uri_decode() - print(token) - var is_successful: bool = true - if provider.should_exchange: - exchange_token(token, _local_uri, provider.access_token_uri, provider.get_client_id(), provider.get_client_secret()) - is_successful = await self.token_exchanged - token = auth.accesstoken - if is_successful and _is_ready(): - is_busy = true - _oauth_login_request_body.postBody = "access_token="+token+"&providerId="+provider.provider_id - _oauth_login_request_body.requestUri = _local_uri - requesting = Requests.LOGIN_WITH_OAUTH - auth_request_type = Auth_Type.LOGIN_OAUTH - var err = request(_base_url + _signin_with_oauth_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_oauth_login_request_body)) - _oauth_login_request_body.postBody = "" - _oauth_login_request_body.requestUri = "" - if err != OK: - is_busy = false - Firebase._printerr("Error logging in with oauth: %s" % err) + if _token: + var token : String = _token.uri_decode() + print(token) + var is_successful: bool = true + if provider.should_exchange: + exchange_token(token, _local_uri, provider.access_token_uri, provider.get_client_id(), provider.get_client_secret()) + is_successful = await self.token_exchanged + token = auth.accesstoken + if is_successful and _is_ready(): + is_busy = true + _oauth_login_request_body.postBody = "access_token="+token+"&providerId="+provider.provider_id + _oauth_login_request_body.requestUri = _local_uri + requesting = Requests.LOGIN_WITH_OAUTH + auth_request_type = Auth_Type.LOGIN_OAUTH + var err = request(_base_url + _signin_with_oauth_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_oauth_login_request_body)) + _oauth_login_request_body.postBody = "" + _oauth_login_request_body.requestUri = "" + if err != OK: + is_busy = false + Firebase._printerr("Error logging in with oauth: %s" % err) # Exchange the authorization oAuth2 code obtained from browser with a proper access id_token func exchange_token(code : String, redirect_uri : String, request_url: String, _client_id: String, _client_secret: String) -> void: - if _is_ready(): - is_busy = true - var exchange_token_body : Dictionary = { - code = code, - redirect_uri = redirect_uri, - client_id = _client_id, - client_secret = _client_secret, - grant_type = "authorization_code", - } - requesting = Requests.EXCHANGE_TOKEN - var err = request(request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(exchange_token_body)) - if err != OK: - is_busy = false - Firebase._printerr("Error exchanging tokens: %s" % err) + if _is_ready(): + is_busy = true + var exchange_token_body : Dictionary = { + code = code, + redirect_uri = redirect_uri, + client_id = _client_id, + client_secret = _client_secret, + grant_type = "authorization_code", + } + requesting = Requests.EXCHANGE_TOKEN + var err = request(request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(exchange_token_body)) + if err != OK: + is_busy = false + Firebase._printerr("Error exchanging tokens: %s" % err) # Open a web page in browser redirecting to Google oAuth2 page for the current project # Once given user's authorization, a token will be generated. # NOTE** with this method, the authorization process will be copy-pasted func get_google_auth_manual(provider: AuthProvider = _local_provider) -> void: - provider.params.redirect_uri = "urn:ietf:wg:oauth:2.0:oob" - get_auth_with_redirect(provider) + provider.params.redirect_uri = "urn:ietf:wg:oauth:2.0:oob" + get_auth_with_redirect(provider) # A timer used to listen through TCP checked the redirect uri of the request func _tcp_stream_timer() -> void: - var peer : StreamPeer = tcp_server.take_connection() - if peer != null: - var raw_result : String = peer.get_utf8_string(441) - if raw_result != "" and raw_result.begins_with("GET"): - tcp_timer.stop() - remove_child(tcp_timer) - has_child = false - var token : String = "" - for value in raw_result.split(" ")[1].lstrip("/?").split("&"): - var splitted: PackedStringArray = value.split("=") - if _local_provider.params.response_type in splitted[0]: - token = splitted[1] - break - - if token == "": - login_failed.emit() - peer.disconnect_from_host() - tcp_server.stop() - return - - var data : PackedByteArray = '

🔥 You can close this window now. 🔥

'.to_ascii_buffer() - peer.put_data(("HTTP/1.1 200 OK\n").to_ascii_buffer()) - peer.put_data(("Server: Godot Firebase SDK\n").to_ascii_buffer()) - peer.put_data(("Content-Length: %d\n" % data.size()).to_ascii_buffer()) - peer.put_data("Connection: close\n".to_ascii_buffer()) - peer.put_data(("Content-Type: text/html; charset=UTF-8\n\n").to_ascii_buffer()) - peer.put_data(data) - login_with_oauth(token, _local_provider) - await self.login_succeeded - peer.disconnect_from_host() - tcp_server.stop() + var peer : StreamPeer = tcp_server.take_connection() + if peer != null: + var raw_result : String = peer.get_utf8_string(441) + if raw_result != "" and raw_result.begins_with("GET"): + tcp_timer.stop() + remove_child(tcp_timer) + has_child = false + var token : String = "" + for value in raw_result.split(" ")[1].lstrip("/?").split("&"): + var splitted: PackedStringArray = value.split("=") + if _local_provider.params.response_type in splitted[0]: + token = splitted[1] + break + + if token == "": + login_failed.emit() + peer.disconnect_from_host() + tcp_server.stop() + return + + var data : PackedByteArray = '

🔥 You can close this window now. 🔥

'.to_ascii_buffer() + peer.put_data(("HTTP/1.1 200 OK\n").to_ascii_buffer()) + peer.put_data(("Server: Godot Firebase SDK\n").to_ascii_buffer()) + peer.put_data(("Content-Length: %d\n" % data.size()).to_ascii_buffer()) + peer.put_data("Connection: close\n".to_ascii_buffer()) + peer.put_data(("Content-Type: text/html; charset=UTF-8\n\n").to_ascii_buffer()) + peer.put_data(data) + login_with_oauth(token, _local_provider) + await self.login_succeeded + peer.disconnect_from_host() + tcp_server.stop() # Function used to logout of the system, this will also remove_at the local encrypted auth file if there is one func logout() -> void: - auth = {} - remove_auth() - logged_out.emit() + auth = {} + remove_auth() + logged_out.emit() # Checks to see if we need a hard login func needs_login() -> bool: - var encrypted_file = FileAccess.open_encrypted_with_pass("user://user.auth", FileAccess.READ, _config.apiKey) - var err = encrypted_file == null - return err + var encrypted_file = FileAccess.open_encrypted_with_pass("user://user.auth", FileAccess.READ, _config.apiKey) + var err = encrypted_file == null + return err # Function is called when requesting a manual token refresh func manual_token_refresh(auth_data): - auth = auth_data - var refresh_token = null - auth = get_clean_keys(auth) - if auth.has("refreshtoken"): - refresh_token = auth.refreshtoken - elif auth.has("refresh_token"): - refresh_token = auth.refresh_token - _needs_refresh = true - _refresh_request_body.refresh_token = refresh_token - var err = request(_refresh_request_base_url + _refresh_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_refresh_request_body)) - if err != OK: - is_busy = false - Firebase._printerr("Error manually refreshing token: %s" % err) + auth = auth_data + var refresh_token = null + auth = get_clean_keys(auth) + if auth.has("refreshtoken"): + refresh_token = auth.refreshtoken + elif auth.has("refresh_token"): + refresh_token = auth.refresh_token + _needs_refresh = true + _refresh_request_body.refresh_token = refresh_token + var err = request(_refresh_request_base_url + _refresh_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_refresh_request_body)) + if err != OK: + is_busy = false + Firebase._printerr("Error manually refreshing token: %s" % err) # This function is called whenever there is an authentication request to Firebase # On an error, this function with emit the signal 'login_failed' and print the error to the console func _on_FirebaseAuth_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: - var json = Utilities.get_json_data(body.get_string_from_utf8()) - is_busy = false - var res - if response_code == 0: - # Mocked error results to trigger the correct signal. - # Can occur if there is no internet connection, or the service is down, - # in which case there is no json_body (and thus parsing would fail). - res = {"error": { - "code": "Connection error", - "message": "Error connecting to auth service"}} - else: - if json == null: - Firebase._printerr("Error while parsing auth body json") - auth_request.emit(ERR_PARSE_ERROR, "Error while parsing auth body json") - return - - res = json - - if response_code == HTTPClient.RESPONSE_OK: - if not res.has("kind"): - auth = get_clean_keys(res) - match requesting: - Requests.EXCHANGE_TOKEN: - token_exchanged.emit(true) - begin_refresh_countdown() - # Refresh token countdown - auth_request.emit(1, auth) + var json = Utilities.get_json_data(body.get_string_from_utf8()) + is_busy = false + var res + if response_code == 0: + # Mocked error results to trigger the correct signal. + # Can occur if there is no internet connection, or the service is down, + # in which case there is no json_body (and thus parsing would fail). + res = {"error": { + "code": "Connection error", + "message": "Error connecting to auth service"}} + else: + if json == null: + Firebase._printerr("Error while parsing auth body json") + auth_request.emit(ERR_PARSE_ERROR, "Error while parsing auth body json") + return + + res = json + + if response_code == HTTPClient.RESPONSE_OK: + if not res.has("kind"): + auth = get_clean_keys(res) + match requesting: + Requests.EXCHANGE_TOKEN: + token_exchanged.emit(true) + begin_refresh_countdown() + # Refresh token countdown + auth_request.emit(1, auth) if _needs_refresh: _needs_refresh = false @@ -471,126 +471,126 @@ func _on_FirebaseAuth_request_completed(result : int, response_code : int, heade # Function used to save the auth data provided by Firebase into an encrypted file # Note this does not work in HTML5 or UWP func save_auth(auth : Dictionary) -> bool: - var encrypted_file = FileAccess.open_encrypted_with_pass("user://user.auth", FileAccess.WRITE, _config.apiKey) - var err = encrypted_file == null - if err: - Firebase._printerr("Error Opening File. Error Code: " + str(FileAccess.get_open_error())) - else: - encrypted_file.store_line(JSON.stringify(auth)) - return not err + var encrypted_file = FileAccess.open_encrypted_with_pass("user://user.auth", FileAccess.WRITE, _config.apiKey) + var err = encrypted_file == null + if err: + Firebase._printerr("Error Opening File. Error Code: " + str(FileAccess.get_open_error())) + else: + encrypted_file.store_line(JSON.stringify(auth)) + return not err # Function used to load the auth data file that has been stored locally # Note this does not work in HTML5 or UWP func load_auth() -> bool: - var encrypted_file = FileAccess.open_encrypted_with_pass("user://user.auth", FileAccess.READ, _config.apiKey) - var err = encrypted_file == null - if err: - Firebase._printerr("Error Opening Firebase Auth File. Error Code: " + str(FileAccess.get_open_error())) - auth_request.emit(err, "Error Opening Firebase Auth File.") - else: - var json = JSON.new() - var json_parse_result = json.parse(encrypted_file.get_line()) - if json_parse_result == OK: - var encrypted_file_data = json.data - manual_token_refresh(encrypted_file_data) - return not err + var encrypted_file = FileAccess.open_encrypted_with_pass("user://user.auth", FileAccess.READ, _config.apiKey) + var err = encrypted_file == null + if err: + Firebase._printerr("Error Opening Firebase Auth File. Error Code: " + str(FileAccess.get_open_error())) + auth_request.emit(err, "Error Opening Firebase Auth File.") + else: + var json = JSON.new() + var json_parse_result = json.parse(encrypted_file.get_line()) + if json_parse_result == OK: + var encrypted_file_data = json.data + manual_token_refresh(encrypted_file_data) + return not err # Function used to remove_at the local encrypted auth file func remove_auth() -> void: - if (FileAccess.file_exists("user://user.auth")): - DirAccess.remove_absolute("user://user.auth") - else: - Firebase._printerr("No encrypted auth file exists") + if (FileAccess.file_exists("user://user.auth")): + DirAccess.remove_absolute("user://user.auth") + else: + Firebase._printerr("No encrypted auth file exists") # Function to check if there is an encrypted auth data file # If there is, the game will load it and refresh the token func check_auth_file() -> bool: - if (FileAccess.file_exists("user://user.auth")): - # Will ensure "auth_request" emitted - return load_auth() - else: - Firebase._printerr("Encrypted Firebase Auth file does not exist") - auth_request.emit(ERR_DOES_NOT_EXIST, "Encrypted Firebase Auth file does not exist") - return false + if (FileAccess.file_exists("user://user.auth")): + # Will ensure "auth_request" emitted + return load_auth() + else: + Firebase._printerr("Encrypted Firebase Auth file does not exist") + auth_request.emit(ERR_DOES_NOT_EXIST, "Encrypted Firebase Auth file does not exist") + return false # Function used to change the email account for the currently logged in user func change_user_email(email : String) -> void: - if _is_ready(): - is_busy = true - _change_email_body.email = email - _change_email_body.idToken = auth.idtoken - var err = request(_base_url + _update_account_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_change_email_body)) - if err != OK: - is_busy = false - Firebase._printerr("Error changing user email: %s" % err) + if _is_ready(): + is_busy = true + _change_email_body.email = email + _change_email_body.idToken = auth.idtoken + var err = request(_base_url + _update_account_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_change_email_body)) + if err != OK: + is_busy = false + Firebase._printerr("Error changing user email: %s" % err) # Function used to change the password for the currently logged in user func change_user_password(password : String) -> void: - if _is_ready(): - is_busy = true - _change_password_body.password = password - _change_password_body.idToken = auth.idtoken - var err = request(_base_url + _update_account_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_change_password_body)) - if err != OK: - is_busy = false - Firebase._printerr("Error changing user password: %s" % err) + if _is_ready(): + is_busy = true + _change_password_body.password = password + _change_password_body.idToken = auth.idtoken + var err = request(_base_url + _update_account_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_change_password_body)) + if err != OK: + is_busy = false + Firebase._printerr("Error changing user password: %s" % err) # User Profile handlers func update_account(idToken : String, displayName : String, photoUrl : String, deleteAttribute : PackedStringArray, returnSecureToken : bool) -> void: - if _is_ready(): - is_busy = true - _update_profile_body.idToken = idToken - _update_profile_body.displayName = displayName - _update_profile_body.photoUrl = photoUrl - _update_profile_body.deleteAttribute = deleteAttribute - _update_profile_body.returnSecureToken = returnSecureToken - var err = request(_base_url + _update_account_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_update_profile_body)) - if err != OK: - is_busy = false - Firebase._printerr("Error updating account: %s" % err) + if _is_ready(): + is_busy = true + _update_profile_body.idToken = idToken + _update_profile_body.displayName = displayName + _update_profile_body.photoUrl = photoUrl + _update_profile_body.deleteAttribute = deleteAttribute + _update_profile_body.returnSecureToken = returnSecureToken + var err = request(_base_url + _update_account_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_update_profile_body)) + if err != OK: + is_busy = false + Firebase._printerr("Error updating account: %s" % err) # Function to send a account verification email func send_account_verification_email() -> void: - if _is_ready(): - is_busy = true - _account_verification_body.idToken = auth.idtoken - var err = request(_base_url + _oobcode_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_account_verification_body)) - if err != OK: - is_busy = false - Firebase._printerr("Error sending account verification email: %s" % err) + if _is_ready(): + is_busy = true + _account_verification_body.idToken = auth.idtoken + var err = request(_base_url + _oobcode_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_account_verification_body)) + if err != OK: + is_busy = false + Firebase._printerr("Error sending account verification email: %s" % err) # Function used to reset the password for a user who has forgotten in. # This will send the users account an email with a password reset link func send_password_reset_email(email : String) -> void: - if _is_ready(): - is_busy = true - _password_reset_body.email = email - var err = request(_base_url + _oobcode_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_password_reset_body)) - if err != OK: - is_busy = false - Firebase._printerr("Error sending password reset email: %s" % err) + if _is_ready(): + is_busy = true + _password_reset_body.email = email + var err = request(_base_url + _oobcode_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_password_reset_body)) + if err != OK: + is_busy = false + Firebase._printerr("Error sending password reset email: %s" % err) # Function called to get all func get_user_data() -> void: - if _is_ready(): - is_busy = true - if not is_logged_in(): - print_debug("Not logged in") - is_busy = false - return + if _is_ready(): + is_busy = true + if not is_logged_in(): + print_debug("Not logged in") + is_busy = false + return - var err = request(_base_url + _userdata_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify({"idToken":auth.idtoken})) - if err != OK: - is_busy = false - Firebase._printerr("Error getting user data: %s" % err) + var err = request(_base_url + _userdata_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify({"idToken":auth.idtoken})) + if err != OK: + is_busy = false + Firebase._printerr("Error getting user data: %s" % err) # Function used to delete the account of the currently authenticated user @@ -607,67 +607,67 @@ func delete_user_account() -> void: # Function is called when a new token is issued to a user. The function will yield until the token has expired, and then request a new one. func begin_refresh_countdown() -> void: - var refresh_token = null - var expires_in = 1000 - auth = get_clean_keys(auth) - if auth.has("refreshtoken"): - refresh_token = auth.refreshtoken - expires_in = auth.expiresin - elif auth.has("refresh_token"): - refresh_token = auth.refresh_token - expires_in = auth.expires_in - if auth.has("userid"): - auth["localid"] = auth.userid - _needs_refresh = true - token_refresh_succeeded.emit(auth) - await get_tree().create_timer(float(expires_in)).timeout - _refresh_request_body.refresh_token = refresh_token - var err = request(_refresh_request_base_url + _refresh_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_refresh_request_body)) - if err != OK: - is_busy = false - Firebase._printerr("Error refreshing via countdown: %s" % err) + var refresh_token = null + var expires_in = 1000 + auth = get_clean_keys(auth) + if auth.has("refreshtoken"): + refresh_token = auth.refreshtoken + expires_in = auth.expiresin + elif auth.has("refresh_token"): + refresh_token = auth.refresh_token + expires_in = auth.expires_in + if auth.has("userid"): + auth["localid"] = auth.userid + _needs_refresh = true + token_refresh_succeeded.emit(auth) + await get_tree().create_timer(float(expires_in)).timeout + _refresh_request_body.refresh_token = refresh_token + var err = request(_refresh_request_base_url + _refresh_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_refresh_request_body)) + if err != OK: + is_busy = false + Firebase._printerr("Error refreshing via countdown: %s" % err) func get_token_from_url(provider: AuthProvider): - var token_type: String = provider.params.response_type if provider.params.response_type == "code" else "access_token" - if OS.has_feature('web'): - var token = JavaScriptBridge.eval(""" - var url_string = window.location.href.replaceAll('?#', '?'); - var url = new URL(url_string); - url.searchParams.get('"""+token_type+"""'); - """) - JavaScriptBridge.eval("""window.history.pushState({}, null, location.href.split('?')[0]);""") - return token - return null + var token_type: String = provider.params.response_type if provider.params.response_type == "code" else "access_token" + if OS.has_feature('web'): + var token = JavaScriptBridge.eval(""" + var url_string = window.location.href.replaceAll('?#', '?'); + var url = new URL(url_string); + url.searchParams.get('"""+token_type+"""'); + """) + JavaScriptBridge.eval("""window.history.pushState({}, null, location.href.split('?')[0]);""") + return token + return null func set_redirect_uri(redirect_uri : String) -> void: - self._local_uri = redirect_uri + self._local_uri = redirect_uri func set_local_provider(provider : AuthProvider) -> void: - self._local_provider = provider + self._local_provider = provider # This function is used to make all keys lowercase # This is only used to cut down checked processing errors from Firebase # This is due to Google have inconsistencies in the API that we are trying to fix func get_clean_keys(auth_result : Dictionary) -> Dictionary: - var cleaned = {} - for key in auth_result.keys(): - cleaned[key.replace("_", "").to_lower()] = auth_result[key] - return cleaned + var cleaned = {} + for key in auth_result.keys(): + cleaned[key.replace("_", "").to_lower()] = auth_result[key] + return cleaned # -------------------- # PROVIDERS # -------------------- func get_GoogleProvider() -> GoogleProvider: - return GoogleProvider.new(_config.clientId, _config.clientSecret) + return GoogleProvider.new(_config.clientId, _config.clientSecret) func get_FacebookProvider() -> FacebookProvider: - return FacebookProvider.new(_config.auth_providers.facebook_id, _config.auth_providers.facebook_secret) + return FacebookProvider.new(_config.auth_providers.facebook_id, _config.auth_providers.facebook_secret) func get_GitHubProvider() -> GitHubProvider: - return GitHubProvider.new(_config.auth_providers.github_id, _config.auth_providers.github_secret) + return GitHubProvider.new(_config.auth_providers.github_id, _config.auth_providers.github_secret) func get_TwitterProvider() -> TwitterProvider: - return TwitterProvider.new(_config.auth_providers.twitter_id, _config.auth_providers.twitter_secret) + return TwitterProvider.new(_config.auth_providers.twitter_id, _config.auth_providers.twitter_secret) diff --git a/addons/godot-firebase/auth/providers/facebook.gd b/addons/godot-firebase/auth/providers/facebook.gd index 926c4a1..35e8dd8 100644 --- a/addons/godot-firebase/auth/providers/facebook.gd +++ b/addons/godot-firebase/auth/providers/facebook.gd @@ -2,20 +2,20 @@ class_name FacebookProvider extends AuthProvider func _init(client_id: String,client_secret: String): - randomize() - set_client_id(client_id) - set_client_secret(client_secret) - - self.redirect_uri = "https://www.facebook.com/v13.0/dialog/oauth?" - self.access_token_uri = "https://graph.facebook.com/v13.0/oauth/access_token" - self.provider_id = "facebook.com" - self.params.scope = "public_profile" - self.params.state = str(randf_range(0, 1)) - if Utilities.is_web(): - self.should_exchange = false - self.params.response_type = "token" - else: - self.should_exchange = true - self.params.response_type = "code" - - + randomize() + set_client_id(client_id) + set_client_secret(client_secret) + + self.redirect_uri = "https://www.facebook.com/v13.0/dialog/oauth?" + self.access_token_uri = "https://graph.facebook.com/v13.0/oauth/access_token" + self.provider_id = "facebook.com" + self.params.scope = "public_profile" + self.params.state = str(randf_range(0, 1)) + if Utilities.is_web(): + self.should_exchange = false + self.params.response_type = "token" + else: + self.should_exchange = true + self.params.response_type = "code" + + diff --git a/addons/godot-firebase/auth/providers/google.gd b/addons/godot-firebase/auth/providers/google.gd index 2ea88cf..152a5cc 100644 --- a/addons/godot-firebase/auth/providers/google.gd +++ b/addons/godot-firebase/auth/providers/google.gd @@ -2,12 +2,12 @@ class_name GoogleProvider extends AuthProvider func _init(client_id: String,client_secret: String): - set_client_id(client_id) - set_client_secret(client_secret) - self.should_exchange = true - self.redirect_uri = "https://accounts.google.com/o/oauth2/v2/auth?" - self.access_token_uri = "https://oauth2.googleapis.com/token" - self.provider_id = "google.com" - self.params.response_type = "code" - self.params.scope = "email openid profile" - self.params.response_type = "code" + set_client_id(client_id) + set_client_secret(client_secret) + self.should_exchange = true + self.redirect_uri = "https://accounts.google.com/o/oauth2/v2/auth?" + self.access_token_uri = "https://oauth2.googleapis.com/token" + self.provider_id = "google.com" + self.params.response_type = "code" + self.params.scope = "email openid profile" + self.params.response_type = "code" diff --git a/addons/godot-firebase/database/database.gd b/addons/godot-firebase/database/database.gd index a2bf082..3391518 100644 --- a/addons/godot-firebase/database/database.gd +++ b/addons/godot-firebase/database/database.gd @@ -13,46 +13,39 @@ var _config : Dictionary = {} var _auth : Dictionary = {} func _set_config(config_json : Dictionary) -> void: - _config = config_json - _check_emulating() - + _config = config_json + _check_emulating() func _check_emulating() -> void : - ## Check emulating - if not Firebase.emulating: - _base_url = _config.databaseURL - else: - var port : String = _config.emulators.ports.realtimeDatabase - if port == "": - Firebase._printerr("You are in 'emulated' mode, but the port for Realtime Database has not been configured.") - else: - _base_url = "http://localhost" - - + ## Check emulating + if not Firebase.emulating: + _base_url = _config.databaseURL + else: + var port : String = _config.emulators.ports.realtimeDatabase + if port == "": + Firebase._printerr("You are in 'emulated' mode, but the port for Realtime Database has not been configured.") + else: + _base_url = "http://localhost" func _on_FirebaseAuth_login_succeeded(auth_result : Dictionary) -> void: - _auth = auth_result + _auth = auth_result func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void: - _auth = auth_result + _auth = auth_result func _on_FirebaseAuth_logout() -> void: - _auth = {} + _auth = {} func get_database_reference(path : String, filter : Dictionary = {}) -> FirebaseDatabaseReference: - var firebase_reference : FirebaseDatabaseReference = FirebaseDatabaseReference.new() - var getter := HTTPRequest.new() - getter.use_threads = true - var pusher : HTTPRequest = HTTPRequest.new() - pusher.use_threads = true - var listener : Node = Node.new() - listener.set_script(load("res://addons/http-sse-client/HTTPSSEClient.gd")) - var store : FirebaseDatabaseStore = FirebaseDatabaseStore.new() - firebase_reference.set_db_path(path, filter) - firebase_reference.set_auth_and_config(_auth, _config) - firebase_reference.set_pusher(pusher) - firebase_reference.set_getter(getter) - firebase_reference.set_listener(listener) - firebase_reference.set_store(store) - add_child(firebase_reference) - return firebase_reference + var firebase_reference = load("res://addons/godot-firebase/database/firebase_database_reference.tscn").instantiate() + firebase_reference.set_db_path(path, filter) + firebase_reference.set_auth_and_config(_auth, _config) + add_child(firebase_reference) + return firebase_reference + +func get_once_database_reference(path : String, filter : Dictionary = {}) -> FirebaseOnceDatabaseReference: + var firebase_reference = load("res://addons/godot-firebase/database/firebase_once_database_reference.tscn").instantiate() + firebase_reference.set_db_path(path, filter) + firebase_reference.set_auth_and_config(_auth, _config) + add_child(firebase_reference) + return firebase_reference diff --git a/addons/godot-firebase/database/database_store.gd b/addons/godot-firebase/database/database_store.gd index e6ea028..a407241 100644 --- a/addons/godot-firebase/database/database_store.gd +++ b/addons/godot-firebase/database/database_store.gd @@ -19,91 +19,91 @@ var _data : Dictionary = { } ## Puts a new payload into this data store at the given path. Any existing values in this data store ## at the specified path will be completely erased. func put(path : String, payload) -> void: - _update_data(path, payload, false) + _update_data(path, payload, false) ## @args path, payload ## Patches an update payload into this data store at the specified path. ## NOTE: When patching in updates to arrays, payload should contain the entire new array! Updating single elements/indexes of an array is not supported. Sometimes when manually mutating array data directly from the Firebase Realtime Database console, single-element patches will be sent out which can cause issues here. func patch(path : String, payload) -> void: - _update_data(path, payload, true) + _update_data(path, payload, true) ## @args path, payload ## Deletes data at the reference point provided ## NOTE: This will delete without warning, so make sure the reference is pointed to the level you want and not the root or you will lose everything func delete(path : String, payload) -> void: - _update_data(path, payload, true) + _update_data(path, payload, true) ## Returns a deep copy of this data store's payload. func get_data() -> Dictionary: - return _data[_ROOT].duplicate(true) + return _data[_ROOT].duplicate(true) # # Updates this data store by either putting or patching the provided payload into it at the given # path. The provided payload can technically be any value. # func _update_data(path: String, payload, patch: bool) -> void: - if debug: - print("Updating data store (patch = %s) (%s = %s)..." % [patch, path, payload]) + if debug: + print("Updating data store (patch = %s) (%s = %s)..." % [patch, path, payload]) - # - # Remove any leading separators. - # - path = path.lstrip(_DELIMITER) + # + # Remove any leading separators. + # + path = path.lstrip(_DELIMITER) - # - # Traverse the path. - # - var dict = _data - var keys = PackedStringArray([_ROOT]) + # + # Traverse the path. + # + var dict = _data + var keys = PackedStringArray([_ROOT]) - keys.append_array(path.split(_DELIMITER, false)) + keys.append_array(path.split(_DELIMITER, false)) - var final_key_idx = (keys.size() - 1) - var final_key = (keys[final_key_idx]) + var final_key_idx = (keys.size() - 1) + var final_key = (keys[final_key_idx]) - keys.remove_at(final_key_idx) + keys.remove_at(final_key_idx) - for key in keys: - if !dict.has(key): - dict[key] = { } + for key in keys: + if !dict.has(key): + dict[key] = { } - dict = dict[key] + dict = dict[key] - # - # Handle non-patch (a.k.a. put) mode and then update the destination value. - # - var new_type = typeof(payload) + # + # Handle non-patch (a.k.a. put) mode and then update the destination value. + # + var new_type = typeof(payload) - if !patch: - dict.erase(final_key) + if !patch: + dict.erase(final_key) - if new_type == TYPE_NIL: - dict.erase(final_key) - elif new_type == TYPE_DICTIONARY: - if !dict.has(final_key): - dict[final_key] = { } + if new_type == TYPE_NIL: + dict.erase(final_key) + elif new_type == TYPE_DICTIONARY: + if !dict.has(final_key): + dict[final_key] = { } - _update_dictionary(dict[final_key], payload) - else: - dict[final_key] = payload + _update_dictionary(dict[final_key], payload) + else: + dict[final_key] = payload - if debug: - print("...Data store updated (%s)." % _data) + if debug: + print("...Data store updated (%s)." % _data) # # Helper method to "blit" changes in an update dictionary payload onto an original dictionary. # Parameters are directly changed via reference. # func _update_dictionary(original_dict: Dictionary, update_payload: Dictionary) -> void: - for key in update_payload.keys(): - var val_type = typeof(update_payload[key]) - - if val_type == TYPE_NIL: - original_dict.erase(key) - elif val_type == TYPE_DICTIONARY: - if !original_dict.has(key): - original_dict[key] = { } - - _update_dictionary(original_dict[key], update_payload[key]) - else: - original_dict[key] = update_payload[key] + for key in update_payload.keys(): + var val_type = typeof(update_payload[key]) + + if val_type == TYPE_NIL: + original_dict.erase(key) + elif val_type == TYPE_DICTIONARY: + if !original_dict.has(key): + original_dict[key] = { } + + _update_dictionary(original_dict[key], update_payload[key]) + else: + original_dict[key] = update_payload[key] diff --git a/addons/godot-firebase/database/firebase_database_reference.tscn b/addons/godot-firebase/database/firebase_database_reference.tscn new file mode 100644 index 0000000..27abf89 --- /dev/null +++ b/addons/godot-firebase/database/firebase_database_reference.tscn @@ -0,0 +1,17 @@ +[gd_scene load_steps=5 format=3 uid="uid://btltp52tywbe4"] + +[ext_resource type="Script" path="res://addons/godot-firebase/database/reference.gd" id="1_l3oy5"] +[ext_resource type="PackedScene" uid="uid://ctb4l7plg8kqg" path="res://addons/godot-firebase/queues/queueable_http_request.tscn" id="2_0qpk7"] +[ext_resource type="Script" path="res://addons/http-sse-client/HTTPSSEClient.gd" id="2_4l0io"] +[ext_resource type="Script" path="res://addons/godot-firebase/database/database_store.gd" id="3_c3r2w"] + +[node name="FirebaseDatabaseReference" type="Node"] +script = ExtResource("1_l3oy5") + +[node name="Pusher" parent="." instance=ExtResource("2_0qpk7")] + +[node name="Listener" type="Node" parent="."] +script = ExtResource("2_4l0io") + +[node name="DataStore" type="Node" parent="."] +script = ExtResource("3_c3r2w") diff --git a/addons/godot-firebase/database/firebase_once_database_reference.tscn b/addons/godot-firebase/database/firebase_once_database_reference.tscn new file mode 100644 index 0000000..c1e2913 --- /dev/null +++ b/addons/godot-firebase/database/firebase_once_database_reference.tscn @@ -0,0 +1,16 @@ +[gd_scene load_steps=3 format=3 uid="uid://d1u1bxp2fd60e"] + +[ext_resource type="Script" path="res://addons/godot-firebase/database/once_reference.gd" id="1_hq5s2"] +[ext_resource type="PackedScene" uid="uid://ctb4l7plg8kqg" path="res://addons/godot-firebase/queues/queueable_http_request.tscn" id="2_t2f32"] + +[node name="FirebaseOnceDatabaseReference" type="Node"] +script = ExtResource("1_hq5s2") + +[node name="Pusher" parent="." instance=ExtResource("2_t2f32")] +accept_gzip = false + +[node name="Oncer" parent="." instance=ExtResource("2_t2f32")] +accept_gzip = false + +[connection signal="queue_request_completed" from="Pusher" to="." method="on_push_request_complete"] +[connection signal="queue_request_completed" from="Oncer" to="." method="on_get_request_complete"] diff --git a/addons/godot-firebase/database/once_reference.gd b/addons/godot-firebase/database/once_reference.gd new file mode 100644 index 0000000..ac816e4 --- /dev/null +++ b/addons/godot-firebase/database/once_reference.gd @@ -0,0 +1,124 @@ +class_name FirebaseOnceDatabaseReference +extends Node + + +## @meta-authors BackAt50Ft +## @meta-version 1.0 +## A once off reference to a location in the Realtime Database. +## Documentation TODO. + +signal once_successful(dataSnapshot) +signal once_failed() + +signal push_successful() +signal push_failed() + +const ORDER_BY : String = "orderBy" +const LIMIT_TO_FIRST : String = "limitToFirst" +const LIMIT_TO_LAST : String = "limitToLast" +const START_AT : String = "startAt" +const END_AT : String = "endAt" +const EQUAL_TO : String = "equalTo" + +@onready var _oncer = $Oncer +@onready var _pusher = $Pusher + +var _auth : Dictionary +var _config : Dictionary +var _filter_query : Dictionary +var _db_path : String + +const _separator : String = "/" +const _json_list_tag : String = ".json" +const _query_tag : String = "?" +const _auth_tag : String = "auth=" + +const _auth_variable_begin : String = "[" +const _auth_variable_end : String = "]" +const _filter_tag : String = "&" +const _escaped_quote : String = '"' +const _equal_tag : String = "=" +const _key_filter_tag : String = "$key" + +var _headers : PackedStringArray = [] + +func set_db_path(path : String, filter_query_dict : Dictionary) -> void: + _db_path = path + _filter_query = filter_query_dict + +func set_auth_and_config(auth_ref : Dictionary, config_ref : Dictionary) -> void: + _auth = auth_ref + _config = config_ref + +# +# Gets a data snapshot once at the position passed in +# +func once(reference : String) -> void: + var ref_pos = _get_list_url() + _db_path + _separator + reference + _get_remaining_path() + _oncer.request(ref_pos, _headers, HTTPClient.METHOD_GET, "") + +func _get_remaining_path(is_push : bool = true) -> String: + var remaining_path = "" + if _filter_query_empty() or is_push: + remaining_path = _json_list_tag + _query_tag + _auth_tag + Firebase.Auth.auth.idtoken + else: + remaining_path = _json_list_tag + _query_tag + _get_filter() + _filter_tag + _auth_tag + Firebase.Auth.auth.idtoken + + if Firebase.emulating: + remaining_path += "&ns="+_config.projectId+"-default-rtdb" + + return remaining_path + +func _get_list_url(with_port:bool = true) -> String: + var url = Firebase.Database._base_url.trim_suffix(_separator) + if with_port and Firebase.emulating: + url += ":" + _config.emulators.ports.realtimeDatabase + return url + _separator + + +func _get_filter(): + if _filter_query_empty(): + return "" + + var filter = "" + + if _filter_query.has(ORDER_BY): + filter += ORDER_BY + _equal_tag + _escaped_quote + _filter_query[ORDER_BY] + _escaped_quote + _filter_query.erase(ORDER_BY) + else: + filter += ORDER_BY + _equal_tag + _escaped_quote + _key_filter_tag + _escaped_quote # Presumptuous, but to get it to work at all... + + for key in _filter_query.keys(): + filter += _filter_tag + key + _equal_tag + _filter_query[key] + + return filter + +func _filter_query_empty() -> bool: + return _filter_query == null or _filter_query.is_empty() + +func on_get_request_complete(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: + if response_code == HTTPClient.RESPONSE_OK: + var bod = Utilities.get_json_data(body) + once_successful.emit(bod) + else: + once_failed.emit() + +func on_push_request_complete(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: + if response_code == HTTPClient.RESPONSE_OK: + push_successful.emit() + else: + push_failed.emit() + +func push(data : Dictionary) -> void: + var to_push = JSON.stringify(data) + _pusher.request(_get_list_url() + _db_path + _get_remaining_path(true), _headers, HTTPClient.METHOD_POST, to_push) + +func update(path : String, data : Dictionary) -> void: + path = path.strip_edges(true, true) + + if path == _separator: + path = "" + + var to_update = JSON.stringify(data) + var resolved_path = (_get_list_url() + _db_path + "/" + path + _get_remaining_path()) + _pusher.request(resolved_path, _headers, HTTPClient.METHOD_PATCH, to_update) diff --git a/addons/godot-firebase/database/reference.gd b/addons/godot-firebase/database/reference.gd index 803da37..0429b92 100644 --- a/addons/godot-firebase/database/reference.gd +++ b/addons/godot-firebase/database/reference.gd @@ -24,19 +24,15 @@ const START_AT : String = "startAt" const END_AT : String = "endAt" const EQUAL_TO : String = "equalTo" -var _pusher : HTTPRequest -var _getter : HTTPRequest -var _listener : Node -var _store : FirebaseDatabaseStore +@onready var _pusher := $Pusher +@onready var _listener := $Listener +@onready var _store := $DataStore + var _auth : Dictionary var _config : Dictionary var _filter_query : Dictionary var _db_path : String var _cached_filter : String -var _push_queue : Array = [] -var _get_queue : Array = [] -var _update_queue : Array = [] -var _delete_queue : Array = [] var _can_connect_to_host : bool = false const _put_tag : String = "put" @@ -56,186 +52,125 @@ const _key_filter_tag : String = "$key" var _headers : PackedStringArray = [] +func _ready() -> void: +#region Set Listener info + $Listener.new_sse_event.connect(on_new_sse_event) + var base_url = _get_list_url(false).trim_suffix(_separator) + var extended_url = _separator + _db_path + _get_remaining_path(false) + var port = -1 + if Firebase.emulating: + port = int(_config.emulators.ports.realtimeDatabase) + $Listener.connect_to_host(base_url, extended_url, port) +#endregion Set Listener info + +#region Set Pusher info + $Pusher.queue_request_completed.connect(on_push_request_complete) +#endregion Set Pusher info + func set_db_path(path : String, filter_query_dict : Dictionary) -> void: - _db_path = path - _filter_query = filter_query_dict + _db_path = path + _filter_query = filter_query_dict func set_auth_and_config(auth_ref : Dictionary, config_ref : Dictionary) -> void: - _auth = auth_ref - _config = config_ref - -func set_pusher(pusher_ref : HTTPRequest) -> void: - if !_pusher: - _pusher = pusher_ref - add_child(_pusher) - _pusher.request_completed.connect(on_push_request_complete) - -func set_getter(getter_ref : HTTPRequest) -> void: - if !_getter: - _getter = getter_ref - add_child(_getter) - _getter.request_completed.connect(on_get_request_complete) - - -func set_listener(listener_ref : Node) -> void: - if !_listener: - _listener = listener_ref - add_child(_listener) - _listener.new_sse_event.connect(on_new_sse_event) - var base_url = _get_list_url(false).trim_suffix(_separator) - var extended_url = _separator + _db_path + _get_remaining_path(false) - var port = -1 - if Firebase.emulating: - port = int(_config.emulators.ports.realtimeDatabase) - _listener.connect_to_host(base_url, extended_url, port) + _auth = auth_ref + _config = config_ref func on_new_sse_event(headers : Dictionary, event : String, data : Dictionary) -> void: - if data: - var command = event - if command and command != "keep-alive": - _route_data(command, data.path, data.data) - if command == _put_tag: - if data.path == _separator and data.data and data.data.keys().size() > 0: - for key in data.data.keys(): - new_data_update.emit(FirebaseResource.new(_separator + key, data.data[key])) - elif data.path != _separator: - new_data_update.emit(FirebaseResource.new(data.path, data.data)) - elif command == _patch_tag: - patch_data_update.emit(FirebaseResource.new(data.path, data.data)) - elif command == _delete_tag: - delete_data_update.emit(FirebaseResource.new(data.path, data.data)) - pass - -func set_store(store_ref : FirebaseDatabaseStore) -> void: - if !_store: - _store = store_ref - add_child(_store) + if data: + var command = event + if command and command != "keep-alive": + _route_data(command, data.path, data.data) + if command == _put_tag: + if data.path == _separator and data.data and data.data.keys().size() > 0: + for key in data.data.keys(): + new_data_update.emit(FirebaseResource.new(_separator + key, data.data[key])) + elif data.path != _separator: + new_data_update.emit(FirebaseResource.new(data.path, data.data)) + elif command == _patch_tag: + patch_data_update.emit(FirebaseResource.new(data.path, data.data)) + elif command == _delete_tag: + delete_data_update.emit(FirebaseResource.new(data.path, data.data)) func update(path : String, data : Dictionary) -> void: - path = path.strip_edges(true, true) - - if path == _separator: - path = "" + path = path.strip_edges(true, true) - var to_update = JSON.stringify(data) - var status = _pusher.get_http_client_status() - if status == HTTPClient.STATUS_DISCONNECTED || status != HTTPClient.STATUS_REQUESTING: - var resolved_path = (_get_list_url() + _db_path + "/" + path + _get_remaining_path()) + if path == _separator: + path = "" - _pusher.request(resolved_path, _headers, HTTPClient.METHOD_PATCH, to_update) - else: - _update_queue.append({"path": path, "data": data}) + var to_update = JSON.stringify(data) + + var resolved_path = (_get_list_url() + _db_path + "/" + path + _get_remaining_path()) + _pusher.request(resolved_path, _headers, HTTPClient.METHOD_PATCH, to_update) func push(data : Dictionary) -> void: - var to_push = JSON.stringify(data) - if _pusher.get_http_client_status() == HTTPClient.STATUS_DISCONNECTED: - _pusher.request(_get_list_url() + _db_path + _get_remaining_path(), _headers, HTTPClient.METHOD_POST, to_push) - else: - _push_queue.append(data) + var to_push = JSON.stringify(data) + _pusher.request(_get_list_url() + _db_path + _get_remaining_path(), _headers, HTTPClient.METHOD_POST, to_push) func delete(reference : String) -> void: - if _pusher.get_http_client_status() == HTTPClient.STATUS_DISCONNECTED: - _pusher.request(_get_list_url() + _db_path + _separator + reference + _get_remaining_path(), _headers, HTTPClient.METHOD_DELETE, "") - else: - _delete_queue.append(reference) - -# -# Gets a data snapshot once at the position passed in -# -func once(reference : String) -> void: - if _getter.get_http_client_status() == HTTPClient.STATUS_DISCONNECTED: - var ref_pos = _get_list_url() + _db_path + _separator + reference + _get_remaining_path() - _getter.request(ref_pos, _headers, HTTPClient.METHOD_GET, "") - else: - _get_queue.append(reference) + _pusher.request(_get_list_url() + _db_path + _separator + reference + _get_remaining_path(), _headers, HTTPClient.METHOD_DELETE, "") # # Returns a deep copy of the current local copy of the data stored at this reference in the Firebase # Realtime Database. # func get_data() -> Dictionary: - if _store == null: - return { } + if _store == null: + return { } - return _store.get_data() + return _store.get_data() func _get_remaining_path(is_push : bool = true) -> String: - var remaining_path = "" - if _filter_query_empty() or is_push: - remaining_path = _json_list_tag + _query_tag + _auth_tag + Firebase.Auth.auth.idtoken - else: - remaining_path = _json_list_tag + _query_tag + _get_filter() + _filter_tag + _auth_tag + Firebase.Auth.auth.idtoken + var remaining_path = "" + if _filter_query_empty() or is_push: + remaining_path = _json_list_tag + _query_tag + _auth_tag + Firebase.Auth.auth.idtoken + else: + remaining_path = _json_list_tag + _query_tag + _get_filter() + _filter_tag + _auth_tag + Firebase.Auth.auth.idtoken - if Firebase.emulating: - remaining_path += "&ns="+_config.projectId+"-default-rtdb" + if Firebase.emulating: + remaining_path += "&ns="+_config.projectId+"-default-rtdb" - return remaining_path + return remaining_path func _get_list_url(with_port:bool = true) -> String: - var url = Firebase.Database._base_url.trim_suffix(_separator) - if with_port and Firebase.emulating: - url += ":" + _config.emulators.ports.realtimeDatabase - return url + _separator + var url = Firebase.Database._base_url.trim_suffix(_separator) + if with_port and Firebase.emulating: + url += ":" + _config.emulators.ports.realtimeDatabase + return url + _separator func _get_filter(): - if _filter_query_empty(): - return "" - # At the moment, this means you can't dynamically change your filter; I think it's okay to specify that in the rules. - if _cached_filter != "": - _cached_filter = "" - if _filter_query.has(ORDER_BY): - _cached_filter += ORDER_BY + _equal_tag + _escaped_quote + _filter_query[ORDER_BY] + _escaped_quote - _filter_query.erase(ORDER_BY) - else: - _cached_filter += ORDER_BY + _equal_tag + _escaped_quote + _key_filter_tag + _escaped_quote # Presumptuous, but to get it to work at all... - for key in _filter_query.keys(): - _cached_filter += _filter_tag + key + _equal_tag + _filter_query[key] - - return _cached_filter + if _filter_query_empty(): + return "" + # At the moment, this means you can't dynamically change your filter; I think it's okay to specify that in the rules. + if _cached_filter != "": + _cached_filter = "" + if _filter_query.has(ORDER_BY): + _cached_filter += ORDER_BY + _equal_tag + _escaped_quote + _filter_query[ORDER_BY] + _escaped_quote + _filter_query.erase(ORDER_BY) + else: + _cached_filter += ORDER_BY + _equal_tag + _escaped_quote + _key_filter_tag + _escaped_quote # Presumptuous, but to get it to work at all... + for key in _filter_query.keys(): + _cached_filter += _filter_tag + key + _equal_tag + _filter_query[key] + + return _cached_filter func _filter_query_empty() -> bool: - return _filter_query == null or _filter_query.is_empty() + return _filter_query == null or _filter_query.is_empty() # # Appropriately updates the current local copy of the data stored at this reference in the Firebase # Realtime Database. # func _route_data(command : String, path : String, data) -> void: - if command == _put_tag: - _store.put(path, data) - elif command == _patch_tag: - _store.patch(path, data) - elif command == _delete_tag: - _store.delete(path, data) + if command == _put_tag: + _store.put(path, data) + elif command == _patch_tag: + _store.patch(path, data) + elif command == _delete_tag: + _store.delete(path, data) func on_push_request_complete(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: - if response_code == HTTPClient.RESPONSE_OK: - push_successful.emit() - else: - push_failed.emit() - - # handle queued operations - if _push_queue.size() > 0: - push(_push_queue.pop_front()) - return - - if _update_queue.size() > 0: - var e = _update_queue.pop_front() - update(e['path'], e['data']) - return - - if _delete_queue.size() > 0: - delete(_delete_queue.pop_front()) - -func on_get_request_complete(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: - if response_code == HTTPClient.RESPONSE_OK: - var bod = Utilities.get_json_data(body) - once_successful.emit(bod) - else: - once_failed.emit() - - # handle queued operations - if _get_queue.size() > 0: - once(_get_queue.pop_front()) - + if response_code == HTTPClient.RESPONSE_OK: + push_successful.emit() + else: + push_failed.emit() diff --git a/addons/godot-firebase/database/resource.gd b/addons/godot-firebase/database/resource.gd index ff5b985..ff8a620 100644 --- a/addons/godot-firebase/database/resource.gd +++ b/addons/godot-firebase/database/resource.gd @@ -9,8 +9,8 @@ var key : String var data func _init(key : String,data): - self.key = key.lstrip("/") - self.data = data + self.key = key.lstrip("/") + self.data = data func _to_string(): - return "{ key:{key}, data:{data} }".format({key = key, data = data}) + return "{ key:{key}, data:{data} }".format({key = key, data = data}) diff --git a/addons/godot-firebase/dynamiclinks/dynamiclinks.gd b/addons/godot-firebase/dynamiclinks/dynamiclinks.gd index 67a1374..8f7a6c7 100644 --- a/addons/godot-firebase/dynamiclinks/dynamiclinks.gd +++ b/addons/godot-firebase/dynamiclinks/dynamiclinks.gd @@ -25,85 +25,85 @@ var _request_list_node : HTTPRequest var _headers : PackedStringArray = [] enum Requests { - NONE = -1, - GENERATE + NONE = -1, + GENERATE } func _set_config(config_json : Dictionary) -> void: - _config = config_json - _request_list_node = HTTPRequest.new() - Utilities.fix_http_request(_request_list_node) - _request_list_node.request_completed.connect(_on_request_completed) - add_child(_request_list_node) - _check_emulating() + _config = config_json + _request_list_node = HTTPRequest.new() + Utilities.fix_http_request(_request_list_node) + _request_list_node.request_completed.connect(_on_request_completed) + add_child(_request_list_node) + _check_emulating() func _check_emulating() -> void : - ## Check emulating - if not Firebase.emulating: - _base_url = "https://firebasedynamiclinks.googleapis.com/v1/shortLinks?key=%s" - _base_url %= _config.apiKey - else: - var port : String = _config.emulators.ports.dynamicLinks - if port == "": - Firebase._printerr("You are in 'emulated' mode, but the port for Dynamic Links has not been configured.") - else: - _base_url = "http://localhost:{port}/{version}/".format({ version = _API_VERSION, port = port }) + ## Check emulating + if not Firebase.emulating: + _base_url = "https://firebasedynamiclinks.googleapis.com/v1/shortLinks?key=%s" + _base_url %= _config.apiKey + else: + var port : String = _config.emulators.ports.dynamicLinks + if port == "": + Firebase._printerr("You are in 'emulated' mode, but the port for Dynamic Links has not been configured.") + else: + _base_url = "http://localhost:{port}/{version}/".format({ version = _API_VERSION, port = port }) var _link_request_body : Dictionary = { - "dynamicLinkInfo": { - "domainUriPrefix": "", - "link": "", - "androidInfo": { - "androidPackageName": "" - }, - "iosInfo": { - "iosBundleId": "" - } - }, - "suffix": { - "option": "" - } + "dynamicLinkInfo": { + "domainUriPrefix": "", + "link": "", + "androidInfo": { + "androidPackageName": "" + }, + "iosInfo": { + "iosBundleId": "" + } + }, + "suffix": { + "option": "" + } } ## @args log_link, APN, IBI, is_unguessable ## This function is used to generate a dynamic link using the Firebase REST API ## It will return a JSON with the shortened link func generate_dynamic_link(long_link : String, APN : String, IBI : String, is_unguessable : bool) -> void: - if not _config.domainUriPrefix or _config.domainUriPrefix == "": - generate_dynamic_link_error.emit("Error: Missing domainUriPrefix in config file. Parameter is required.") - Firebase._printerr("Error: Missing domainUriPrefix in config file. Parameter is required.") - return - - request = Requests.GENERATE - _link_request_body.dynamicLinkInfo.domainUriPrefix = _config.domainUriPrefix - _link_request_body.dynamicLinkInfo.link = long_link - _link_request_body.dynamicLinkInfo.androidInfo.androidPackageName = APN - _link_request_body.dynamicLinkInfo.iosInfo.iosBundleId = IBI - if is_unguessable: - _link_request_body.suffix.option = "UNGUESSABLE" - else: - _link_request_body.suffix.option = "SHORT" - _request_list_node.request(_base_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_link_request_body)) + if not _config.domainUriPrefix or _config.domainUriPrefix == "": + generate_dynamic_link_error.emit("Error: Missing domainUriPrefix in config file. Parameter is required.") + Firebase._printerr("Error: Missing domainUriPrefix in config file. Parameter is required.") + return + + request = Requests.GENERATE + _link_request_body.dynamicLinkInfo.domainUriPrefix = _config.domainUriPrefix + _link_request_body.dynamicLinkInfo.link = long_link + _link_request_body.dynamicLinkInfo.androidInfo.androidPackageName = APN + _link_request_body.dynamicLinkInfo.iosInfo.iosBundleId = IBI + if is_unguessable: + _link_request_body.suffix.option = "UNGUESSABLE" + else: + _link_request_body.suffix.option = "SHORT" + _request_list_node.request(_base_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_link_request_body)) func _on_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: - var json = JSON.new() - var json_parse_result = json.parse(body.get_string_from_utf8()) - if json_parse_result == OK: - var result_body = json.data.result # Check this - dynamic_link_generated.emit(result_body.shortLink) - else: - generate_dynamic_link_error.emit(json.get_error_message()) - # This used to return immediately when above, but it should still clear the request, so removing it - - request = Requests.NONE + var json = JSON.new() + var json_parse_result = json.parse(body.get_string_from_utf8()) + if json_parse_result == OK: + var result_body = json.data.result # Check this + dynamic_link_generated.emit(result_body.shortLink) + else: + generate_dynamic_link_error.emit(json.get_error_message()) + # This used to return immediately when above, but it should still clear the request, so removing it + + request = Requests.NONE func _on_FirebaseAuth_login_succeeded(auth_result : Dictionary) -> void: - _auth = auth_result + _auth = auth_result func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void: - _auth = auth_result + _auth = auth_result func _on_FirebaseAuth_logout() -> void: - _auth = {} + _auth = {} diff --git a/addons/godot-firebase/firebase/firebase.gd b/addons/godot-firebase/firebase/firebase.gd index a3c787c..f2eed9f 100644 --- a/addons/godot-firebase/firebase/firebase.gd +++ b/addons/godot-firebase/firebase/firebase.gd @@ -1,11 +1,12 @@ ## @meta-authors Kyle Szklenski -## @meta-version 2.5 +## @meta-version 2.6 ## The Firebase Godot API. ## This singleton gives you access to your Firebase project and its capabilities. Using this requires you to fill out some Firebase configuration settings. It currently comes with four modules. ## - [code]Auth[/code]: Manages user authentication (logging and out, etc...) ## - [code]Database[/code]: A NonSQL realtime database for managing data in JSON structures. ## - [code]Firestore[/code]: Similar to Database, but stores data in collections and documents, among other things. ## - [code]Storage[/code]: Gives access to Cloud Storage; perfect for storing files like images and other assets. +## - [code]RemoteConfig[/code]: Gives access to Remote Config functionality; allows you to download your app's configuration from Firebase, do A/B testing, and more. ## ## @tutorial https://github.com/GodotNuts/GodotFirebase/wiki @tool @@ -17,27 +18,31 @@ const _AUTH_PROVIDERS : String = "firebase/auth_providers" ## @type FirebaseAuth ## The Firebase Authentication API. -@onready var Auth : FirebaseAuth = $Auth +@onready var Auth := $Auth ## @type FirebaseFirestore ## The Firebase Firestore API. -@onready var Firestore : FirebaseFirestore = $Firestore +@onready var Firestore := $Firestore ## @type FirebaseDatabase ## The Firebase Realtime Database API. -@onready var Database : FirebaseDatabase = $Database +@onready var Database := $Database ## @type FirebaseStorage ## The Firebase Storage API. -@onready var Storage : FirebaseStorage = $Storage +@onready var Storage := $Storage ## @type FirebaseDynamicLinks ## The Firebase Dynamic Links API. -@onready var DynamicLinks : FirebaseDynamicLinks = $DynamicLinks +@onready var DynamicLinks := $DynamicLinks ## @type FirebaseFunctions ## The Firebase Cloud Functions API -@onready var Functions : FirebaseFunctions = $Functions +@onready var Functions := $Functions + +## @type FirebaseRemoteConfig +## The Firebase Remote Config API +@onready var RemoteConfigAPI := $RemoteConfig @export var emulating : bool = false @@ -45,96 +50,95 @@ const _AUTH_PROVIDERS : String = "firebase/auth_providers" # These values can be found in your Firebase Project # See the README checked Github for how to access var _config : Dictionary = { - "apiKey": "", - "authDomain": "", - "databaseURL": "", - "projectId": "", - "storageBucket": "", - "messagingSenderId": "", - "appId": "", - "measurementId": "", - "clientId": "", - "clientSecret" : "", - "domainUriPrefix" : "", - "functionsGeoZone" : "", - "cacheLocation":"", - "emulators": { - "ports" : { - "authentication" : "", - "firestore" : "", - "realtimeDatabase" : "", - "functions" : "", - "storage" : "", - "dynamicLinks" : "" - } - }, - "workarounds":{ - "database_connection_closed_issue": false, # fixes https://github.com/firebase/firebase-tools/issues/3329 - }, - "auth_providers": { - "facebook_id":"", - "facebook_secret":"", - "github_id":"", - "github_secret":"", - "twitter_id":"", - "twitter_secret":"" - } + "apiKey": "", + "authDomain": "", + "databaseURL": "", + "projectId": "", + "storageBucket": "", + "messagingSenderId": "", + "appId": "", + "measurementId": "", + "clientId": "", + "clientSecret" : "", + "domainUriPrefix" : "", + "functionsGeoZone" : "", + "cacheLocation":"", + "emulators": { + "ports" : { + "authentication" : "", + "firestore" : "", + "realtimeDatabase" : "", + "functions" : "", + "storage" : "", + "dynamicLinks" : "" + } + }, + "workarounds":{ + "database_connection_closed_issue": false, # fixes https://github.com/firebase/firebase-tools/issues/3329 + }, + "auth_providers": { + "facebook_id":"", + "facebook_secret":"", + "github_id":"", + "github_secret":"", + "twitter_id":"", + "twitter_secret":"" + } } func _ready() -> void: - _load_config() + _load_config() func set_emulated(emulating : bool = true) -> void: - self.emulating = emulating - _check_emulating() + self.emulating = emulating + _check_emulating() func _check_emulating() -> void: - if emulating: - print("[Firebase] You are now in 'emulated' mode: the services you are using will try to connect to your local emulators, if available.") - for module in get_children(): - if module.has_method("_check_emulating"): - module._check_emulating() + if emulating: + print("[Firebase] You are now in 'emulated' mode: the services you are using will try to connect to your local emulators, if available.") + for module in get_children(): + if module.has_method("_check_emulating"): + module._check_emulating() func _load_config() -> void: - if not (_config.apiKey != "" and _config.authDomain != ""): - var env = ConfigFile.new() - var err = env.load("res://addons/godot-firebase/.env") - if err == OK: - for key in _config.keys(): - var config_value = _config[key] - if key == "emulators" and config_value.has("ports"): - for port in config_value["ports"].keys(): - config_value["ports"][port] = env.get_value(_EMULATORS_PORTS, port, "") - if key == "auth_providers": - for provider in config_value.keys(): - config_value[provider] = env.get_value(_AUTH_PROVIDERS, provider, "") - else: - var value : String = env.get_value(_ENVIRONMENT_VARIABLES, key, "") - if value == "": - _print("The value for `%s` is not configured. If you are not planning to use it, ignore this message." % key) - else: - _config[key] = value - else: - _printerr("Unable to read .env file at path 'res://addons/godot-firebase/.env'") - - _setup_modules() + if not (_config.apiKey != "" and _config.authDomain != ""): + var env = ConfigFile.new() + var err = env.load("res://addons/godot-firebase/.env") + if err == OK: + for key in _config.keys(): + var config_value = _config[key] + if key == "emulators" and config_value.has("ports"): + for port in config_value["ports"].keys(): + config_value["ports"][port] = env.get_value(_EMULATORS_PORTS, port, "") + if key == "auth_providers": + for provider in config_value.keys(): + config_value[provider] = env.get_value(_AUTH_PROVIDERS, provider, "") + else: + var value : String = env.get_value(_ENVIRONMENT_VARIABLES, key, "") + if value == "": + _print("The value for `%s` is not configured. If you are not planning to use it, ignore this message." % key) + else: + _config[key] = value + else: + _printerr("Unable to read .env file at path 'res://addons/godot-firebase/.env'") + + _setup_modules() func _setup_modules() -> void: - for module in get_children(): - module._set_config(_config) - if not module.has_method("_on_FirebaseAuth_login_succeeded"): - continue - Auth.login_succeeded.connect(module._on_FirebaseAuth_login_succeeded) - Auth.signup_succeeded.connect(module._on_FirebaseAuth_login_succeeded) - Auth.token_refresh_succeeded.connect(module._on_FirebaseAuth_token_refresh_succeeded) - Auth.logged_out.connect(module._on_FirebaseAuth_logout) - + for module in get_children(): + module._set_config(_config) + if not module.has_method("_on_FirebaseAuth_login_succeeded"): + continue + Auth.login_succeeded.connect(module._on_FirebaseAuth_login_succeeded) + Auth.signup_succeeded.connect(module._on_FirebaseAuth_login_succeeded) + Auth.token_refresh_succeeded.connect(module._on_FirebaseAuth_token_refresh_succeeded) + Auth.logged_out.connect(module._on_FirebaseAuth_logout) # ------------- func _printerr(error : String) -> void: - printerr("[Firebase Error] >> " + error) + printerr("[Firebase Error] >> " + error) func _print(msg : String) -> void: - print("[Firebase] >> " + str(msg)) + print("[Firebase] >> " + str(msg)) diff --git a/addons/godot-firebase/firebase/firebase.tscn b/addons/godot-firebase/firebase/firebase.tscn index d5b34d7..31f5b56 100644 --- a/addons/godot-firebase/firebase/firebase.tscn +++ b/addons/godot-firebase/firebase/firebase.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=8 format=3 uid="uid://cvb26atjckwlq"] +[gd_scene load_steps=9 format=3 uid="uid://cvb26atjckwlq"] [ext_resource type="Script" path="res://addons/godot-firebase/database/database.gd" id="1"] [ext_resource type="Script" path="res://addons/godot-firebase/firestore/firestore.gd" id="2"] @@ -7,6 +7,7 @@ [ext_resource type="Script" path="res://addons/godot-firebase/storage/storage.gd" id="5"] [ext_resource type="Script" path="res://addons/godot-firebase/dynamiclinks/dynamiclinks.gd" id="6"] [ext_resource type="Script" path="res://addons/godot-firebase/functions/functions.gd" id="7"] +[ext_resource type="PackedScene" uid="uid://5xa6ulbllkjk" path="res://addons/godot-firebase/remote_config/firebase_remote_config.tscn" id="8_mvdf4"] [node name="Firebase" type="Node"] script = ExtResource("3") @@ -30,3 +31,6 @@ script = ExtResource("6") [node name="Functions" type="Node" parent="."] script = ExtResource("7") + +[node name="RemoteConfig" parent="." instance=ExtResource("8_mvdf4")] +accept_gzip = false diff --git a/addons/godot-firebase/firestore/field_transform.gd b/addons/godot-firebase/firestore/field_transform.gd new file mode 100644 index 0000000..b69395b --- /dev/null +++ b/addons/godot-firebase/firestore/field_transform.gd @@ -0,0 +1,22 @@ +extends FirestoreTransform +class_name FieldTransform + +enum TransformType { SetToServerValue, Maximum, Minimum, Increment, AppendMissingElements, RemoveAllFromArray } + +const transtype_string_map = { + TransformType.SetToServerValue : "setToServerValue", + TransformType.Increment : "increment", + TransformType.Maximum : "maximum", + TransformType.Minimum : "minimum", + TransformType.AppendMissingElements : "appendMissingElements", + TransformType.RemoveAllFromArray : "removeAllFromArray" +} + +var document_exists : bool +var document_name : String +var field_path : String +var transform_type : TransformType +var value : Variant + +func get_transform_type() -> String: + return transtype_string_map[transform_type] diff --git a/addons/godot-firebase/firestore/field_transform_array.gd b/addons/godot-firebase/firestore/field_transform_array.gd new file mode 100644 index 0000000..72552e9 --- /dev/null +++ b/addons/godot-firebase/firestore/field_transform_array.gd @@ -0,0 +1,35 @@ +class_name FieldTransformArray +extends RefCounted + +var transforms = [] + +var _extended_url +var _collection_name +const _separator = "/" + +func set_config(config : Dictionary): + _extended_url = config.extended_url + _collection_name = config.collection_name + +func push_back(transform : FieldTransform) -> void: + transforms.push_back(transform) + +func serialize() -> Dictionary: + var body = {} + var writes_array = [] + for transform in transforms: + writes_array.push_back({ + "currentDocument": { "exists" : transform.document_exists }, + "transform" : { + "document": _extended_url + _collection_name + _separator + transform.document_name, + "fieldTransforms": [ + { + "fieldPath": transform.field_path, + transform.get_transform_type(): transform.value + }] + } + }) + + body = { "writes": writes_array } + + return body diff --git a/addons/godot-firebase/firestore/field_transforms/decrement_transform.gd b/addons/godot-firebase/firestore/field_transforms/decrement_transform.gd new file mode 100644 index 0000000..ed7f4b7 --- /dev/null +++ b/addons/godot-firebase/firestore/field_transforms/decrement_transform.gd @@ -0,0 +1,19 @@ +class_name DecrementTransform +extends FieldTransform + +func _init(doc_name : String, doc_must_exist : bool, path_to_field : String, by_this_much : Variant) -> void: + document_name = doc_name + document_exists = doc_must_exist + field_path = path_to_field + + transform_type = FieldTransform.TransformType.Increment + + var value_type = typeof(by_this_much) + if value_type == TYPE_INT: + self.value = { + "integerValue": -by_this_much + } + elif value_type == TYPE_FLOAT: + self.value = { + "doubleValue": -by_this_much + } diff --git a/addons/godot-firebase/firestore/field_transforms/increment_transform.gd b/addons/godot-firebase/firestore/field_transforms/increment_transform.gd new file mode 100644 index 0000000..5c7a38c --- /dev/null +++ b/addons/godot-firebase/firestore/field_transforms/increment_transform.gd @@ -0,0 +1,19 @@ +class_name IncrementTransform +extends FieldTransform + +func _init(doc_name : String, doc_must_exist : bool, path_to_field : String, by_this_much : Variant) -> void: + document_name = doc_name + document_exists = doc_must_exist + field_path = path_to_field + + transform_type = FieldTransform.TransformType.Increment + + var value_type = typeof(by_this_much) + if value_type == TYPE_INT: + self.value = { + "integerValue": by_this_much + } + elif value_type == TYPE_FLOAT: + self.value = { + "doubleValue": by_this_much + } diff --git a/addons/godot-firebase/firestore/field_transforms/max_transform.gd b/addons/godot-firebase/firestore/field_transforms/max_transform.gd new file mode 100644 index 0000000..a10c87e --- /dev/null +++ b/addons/godot-firebase/firestore/field_transforms/max_transform.gd @@ -0,0 +1,19 @@ +class_name MaxTransform +extends FieldTransform + +func _init(doc_name : String, doc_must_exist : bool, path_to_field : String, value : Variant) -> void: + document_name = doc_name + document_exists = doc_must_exist + field_path = path_to_field + + transform_type = FieldTransform.TransformType.Maximum + + var value_type = typeof(value) + if value_type == TYPE_INT: + self.value = { + "integerValue": value + } + elif value_type == TYPE_FLOAT: + self.value = { + "doubleValue": value + } diff --git a/addons/godot-firebase/firestore/field_transforms/min_transform.gd b/addons/godot-firebase/firestore/field_transforms/min_transform.gd new file mode 100644 index 0000000..82fd8e4 --- /dev/null +++ b/addons/godot-firebase/firestore/field_transforms/min_transform.gd @@ -0,0 +1,19 @@ +class_name MinTransform +extends FieldTransform + +func _init(doc_name : String, doc_must_exist : bool, path_to_field : String, value : Variant) -> void: + document_name = doc_name + document_exists = doc_must_exist + field_path = path_to_field + + transform_type = FieldTransform.TransformType.Minimum + + var value_type = typeof(value) + if value_type == TYPE_INT: + self.value = { + "integerValue": value + } + elif value_type == TYPE_FLOAT: + self.value = { + "doubleValue": value + } diff --git a/addons/godot-firebase/firestore/field_transforms/server_timestamp_transform.gd b/addons/godot-firebase/firestore/field_transforms/server_timestamp_transform.gd new file mode 100644 index 0000000..7c7c380 --- /dev/null +++ b/addons/godot-firebase/firestore/field_transforms/server_timestamp_transform.gd @@ -0,0 +1,10 @@ +class_name ServerTimestampTransform +extends FieldTransform + +func _init(doc_name : String, doc_must_exist : bool, path_to_field : String) -> void: + document_name = doc_name + document_exists = doc_must_exist + field_path = path_to_field + + transform_type = FieldTransform.TransformType.SetToServerValue + value = "REQUEST_TIME" diff --git a/addons/godot-firebase/firestore/firestore.gd b/addons/godot-firebase/firestore/firestore.gd index 97e37a0..979006d 100644 --- a/addons/godot-firebase/firestore/firestore.gd +++ b/addons/godot-firebase/firestore/firestore.gd @@ -30,9 +30,9 @@ signal result_query(result) signal task_error(code,status,message) enum Requests { - NONE = -1, ## Firestore is not processing any request. - LIST, ## Firestore is processing a [code]list()[/code] request checked a collection. - QUERY ## Firestore is processing a [code]query()[/code] request checked a collection. + NONE = -1, ## Firestore is not processing any request. + LIST, ## Firestore is processing a [code]list()[/code] request checked a collection. + QUERY ## Firestore is processing a [code]query()[/code] request checked a collection. } # TODO: Implement cache size limit @@ -86,18 +86,18 @@ var _http_request_pool := [] var _offline: bool = false : set = _set_offline func _ready() -> void: - pass + pass func _process(delta : float) -> void: - for i in range(_http_request_pool.size() - 1, -1, -1): - var request = _http_request_pool[i] - if not request.get_meta("requesting"): - var lifetime: float = request.get_meta("lifetime") + delta - if lifetime > _MAX_POOLED_REQUEST_AGE: - request.queue_free() - _http_request_pool.remove_at(i) - continue # Just to skip set_meta on a queue_freed request - request.set_meta("lifetime", lifetime) + for i in range(_http_request_pool.size() - 1, -1, -1): + var request = _http_request_pool[i] + if not request.get_meta("requesting"): + var lifetime: float = request.get_meta("lifetime") + delta + if lifetime > _MAX_POOLED_REQUEST_AGE: + request.queue_free() + _http_request_pool.remove_at(i) + continue # Just to skip set_meta on a queue_freed request + request.set_meta("lifetime", lifetime) ## Returns a reference collection by its [i]path[/i]. @@ -107,18 +107,17 @@ func _process(delta : float) -> void: ## @args path ## @return FirestoreCollection func collection(path : String) -> FirestoreCollection: - if not collections.has(path): - var coll : FirestoreCollection = FirestoreCollection.new() - coll._extended_url = _extended_url - coll._base_url = _base_url - coll._config = _config - coll.auth = auth - coll.collection_name = path - coll.firestore = self - collections[path] = coll - return coll - else: - return collections[path] + if not collections.has(path): + var coll : FirestoreCollection = FirestoreCollection.new() + coll._extended_url = _extended_url + coll._base_url = _base_url + coll._config = _config + coll.auth = auth + coll.collection_name = path + collections[path] = coll + return coll + else: + return collections[path] ## Issue a query checked your Firestore database. @@ -141,18 +140,18 @@ func collection(path : String) -> FirestoreCollection: ## @arg-types FirestoreQuery ## @return FirestoreTask func query(query : FirestoreQuery) -> FirestoreTask: - var firestore_task : FirestoreTask = FirestoreTask.new() - firestore_task.result_query.connect(_on_result_query) # In theory, this and the following could be a CONNECT_ONE_SHOT, but I'm iffy on whether or not that might break any client code, so leaving this for now. - firestore_task.task_error.connect(_on_task_error) - firestore_task.action = FirestoreTask.Task.TASK_QUERY - var body : Dictionary = { structuredQuery = query.query } - var url : String = _base_url + _extended_url + _query_suffix + var firestore_task : FirestoreTask = FirestoreTask.new() + firestore_task.result_query.connect(_on_result_query) # In theory, this and the following could be a CONNECT_ONE_SHOT, but I'm iffy on whether or not that might break any client code, so leaving this for now. + firestore_task.task_error.connect(_on_task_error) + firestore_task.action = FirestoreTask.Task.TASK_QUERY + var body : Dictionary = { structuredQuery = query.query } + var url : String = _base_url + _extended_url + _query_suffix - firestore_task.data = query - firestore_task._fields = JSON.stringify(body) - firestore_task._url = url - _pooled_request(firestore_task) - return firestore_task + firestore_task.data = query + firestore_task._fields = JSON.stringify(body) + firestore_task._url = url + _pooled_request(firestore_task) + return firestore_task ## Request a list of contents (documents and/or collections) inside a collection, specified by its [i]id[/i]. This method will return a [code]FirestoreTask[/code] object, representing a reference to the request issued. If saved into a variable, the [code]FirestoreTask[/code] object can be used to yield checked the [code]result_query(result)[/code] signal, or the more generic [code]task_finished(result)[/code] signal. @@ -170,152 +169,152 @@ func query(query : FirestoreQuery) -> FirestoreTask: ## @arg-defaults , 0, "", "" ## @return FirestoreTask func list(path : String = "", page_size : int = 0, page_token : String = "", order_by : String = "") -> FirestoreTask: - var firestore_task : FirestoreTask = FirestoreTask.new() - firestore_task.listed_documents.connect(_on_listed_documents) # Same as above with one shot connections - firestore_task.task_error.connect(_on_task_error) - firestore_task.action = FirestoreTask.Task.TASK_LIST - var url : String = _base_url + _extended_url + path - if page_size != 0: - url+="?pageSize="+str(page_size) - if page_token != "": - url+="&pageToken="+page_token - if order_by != "": - url+="&orderBy="+order_by - - firestore_task.data = [path, page_size, page_token, order_by] - firestore_task._url = url - _pooled_request(firestore_task) - return firestore_task + var firestore_task : FirestoreTask = FirestoreTask.new() + firestore_task.listed_documents.connect(_on_listed_documents) # Same as above with one shot connections + firestore_task.task_error.connect(_on_task_error) + firestore_task.action = FirestoreTask.Task.TASK_LIST + var url : String = _base_url + _extended_url + path + if page_size != 0: + url+="?pageSize="+str(page_size) + if page_token != "": + url+="&pageToken="+page_token + if order_by != "": + url+="&orderBy="+order_by + + firestore_task.data = [path, page_size, page_token, order_by] + firestore_task._url = url + _pooled_request(firestore_task) + return firestore_task func set_networking(value: bool) -> void: - if value: - enable_networking() - else: - disable_networking() + if value: + enable_networking() + else: + disable_networking() func enable_networking() -> void: - if networking: - return - networking = true - _base_url = _base_url.replace("storeoffline", "firestore") - for key in collections: - collections[key]._base_url = _base_url + if networking: + return + networking = true + _base_url = _base_url.replace("storeoffline", "firestore") + for key in collections: + collections[key]._base_url = _base_url func disable_networking() -> void: - if not networking: - return - networking = false - # Pointing to an invalid url should do the trick. - _base_url = _base_url.replace("firestore", "storeoffline") - for key in collections: - collections[key]._base_url = _base_url + if not networking: + return + networking = false + # Pointing to an invalid url should do the trick. + _base_url = _base_url.replace("firestore", "storeoffline") + for key in collections: + collections[key]._base_url = _base_url func _set_offline(value: bool) -> void: - return # Since caching is causing a lot of issues, I'm turning it off for now. We will revisit this in the future, once we have some time to investigate why the cache is being corrupted. + return # Since caching is causing a lot of issues, I'm turning it off for now. We will revisit this in the future, once we have some time to investigate why the cache is being corrupted. func _set_config(config_json : Dictionary) -> void: - _config = config_json - _cache_loc = _config["cacheLocation"] - _extended_url = _extended_url.replace("[PROJECT_ID]", _config.projectId) + _config = config_json + _cache_loc = _config["cacheLocation"] + _extended_url = _extended_url.replace("[PROJECT_ID]", _config.projectId) - # Since caching is causing a lot of issues, I'm removing this check for now. We will revisit this in the future, once we have some time to investigate why the cache is being corrupted. + # Since caching is causing a lot of issues, I'm removing this check for now. We will revisit this in the future, once we have some time to investigate why the cache is being corrupted. - _check_emulating() + _check_emulating() func _check_emulating() -> void : - ## Check emulating - if not Firebase.emulating: - _base_url = "https://firestore.googleapis.com/{version}/".format({ version = _API_VERSION }) - else: - var port : String = _config.emulators.ports.firestore - if port == "": - Firebase._printerr("You are in 'emulated' mode, but the port for Firestore has not been configured.") - else: - _base_url = "http://localhost:{port}/{version}/".format({ version = _API_VERSION, port = port }) + ## Check emulating + if not Firebase.emulating: + _base_url = "https://firestore.googleapis.com/{version}/".format({ version = _API_VERSION }) + else: + var port : String = _config.emulators.ports.firestore + if port == "": + Firebase._printerr("You are in 'emulated' mode, but the port for Firestore has not been configured.") + else: + _base_url = "http://localhost:{port}/{version}/".format({ version = _API_VERSION, port = port }) func _pooled_request(task : FirestoreTask) -> void: - if _offline: - task._on_request_completed(HTTPRequest.RESULT_CANT_CONNECT, 404, PackedStringArray(), PackedByteArray()) - return - - if (auth == null or auth.is_empty()) and not Firebase.emulating: - Firebase._print("Unauthenticated request issued...") - Firebase.Auth.login_anonymous() - var result : Array = await Firebase.Auth.auth_request - if result[0] != 1: - _check_auth_error(result[0], result[1]) - Firebase._print("Client connected as Anonymous") - - if not Firebase.emulating: - task._headers = PackedStringArray([_AUTHORIZATION_HEADER + auth.idtoken]) - - var http_request : HTTPRequest - for request in _http_request_pool: - if not request.get_meta("requesting"): - http_request = request - break - - if not http_request: - http_request = HTTPRequest.new() - http_request.timeout = 5 - Utilities.fix_http_request(http_request) - _http_request_pool.append(http_request) - add_child(http_request) - http_request.request_completed.connect(_on_pooled_request_completed.bind(http_request)) - - http_request.set_meta("requesting", true) - http_request.set_meta("lifetime", 0.0) - http_request.set_meta("task", task) - http_request.request(task._url, task._headers, task._method, task._fields) + if _offline: + task._on_request_completed(HTTPRequest.RESULT_CANT_CONNECT, 404, PackedStringArray(), PackedByteArray()) + return + + if (auth == null or auth.is_empty()) and not Firebase.emulating: + Firebase._print("Unauthenticated request issued...") + Firebase.Auth.login_anonymous() + var result : Array = await Firebase.Auth.auth_request + if result[0] != 1: + _check_auth_error(result[0], result[1]) + Firebase._print("Client connected as Anonymous") + + if not Firebase.emulating: + task._headers = PackedStringArray([_AUTHORIZATION_HEADER + auth.idtoken]) + + var http_request : HTTPRequest + for request in _http_request_pool: + if not request.get_meta("requesting"): + http_request = request + break + + if not http_request: + http_request = HTTPRequest.new() + http_request.timeout = 5 + Utilities.fix_http_request(http_request) + _http_request_pool.append(http_request) + add_child(http_request) + http_request.request_completed.connect(_on_pooled_request_completed.bind(http_request)) + + http_request.set_meta("requesting", true) + http_request.set_meta("lifetime", 0.0) + http_request.set_meta("task", task) + http_request.request(task._url, task._headers, task._method, task._fields) # ------------- func _on_listed_documents(_listed_documents : Array): - listed_documents.emit(_listed_documents) + listed_documents.emit(_listed_documents) func _on_result_query(result : Array): - result_query.emit(result) + result_query.emit(result) func _on_task_error(code : int, status : String, message : String, task : int): - task_error.emit(code, status, message) - Firebase._printerr(message) + task_error.emit(code, status, message) + Firebase._printerr(message) func _on_FirebaseAuth_login_succeeded(auth_result : Dictionary) -> void: - auth = auth_result - for key in collections: - collections[key].auth = auth + auth = auth_result + for key in collections: + collections[key].auth = auth func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void: - auth = auth_result - for key in collections: - collections[key].auth = auth + auth = auth_result + for key in collections: + collections[key].auth = auth func _on_pooled_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray, request : HTTPRequest) -> void: - request.get_meta("task")._on_request_completed(result, response_code, headers, body) - request.set_meta("requesting", false) + request.get_meta("task")._on_request_completed(result, response_code, headers, body) + request.set_meta("requesting", false) func _on_connect_check_request_completed(result : int, _response_code, _headers, _body) -> void: - _set_offline(result != HTTPRequest.RESULT_SUCCESS) - #_connect_check_node.request(_base_url) + _set_offline(result != HTTPRequest.RESULT_SUCCESS) + #_connect_check_node.request(_base_url) func _on_FirebaseAuth_logout() -> void: - auth = {} + auth = {} func _check_auth_error(code : int, message : String) -> void: - var err : String - match code: - 400: err = "Please enable the Anonymous Sign-in method, or Authenticate the Client before issuing a request" - Firebase._printerr(err) - Firebase._printerr(message) + var err : String + match code: + 400: err = "Please enable the Anonymous Sign-in method, or Authenticate the Client before issuing a request" + Firebase._printerr(err) + Firebase._printerr(message) diff --git a/addons/godot-firebase/firestore/firestore_collection.gd b/addons/godot-firebase/firestore/firestore_collection.gd index 8808112..56d8bc0 100644 --- a/addons/godot-firebase/firestore/firestore_collection.gd +++ b/addons/godot-firebase/firestore/firestore_collection.gd @@ -10,7 +10,8 @@ extends RefCounted signal add_document(doc) signal get_document(doc) signal update_document(doc) -signal delete_document() +signal commit_document(result) +signal delete_document(deleted) signal error(code,status,message) const _AUTHORIZATION_HEADER : String = "Authorization: Bearer " @@ -21,7 +22,6 @@ const _documentId_tag : String = "documentId=" var auth : Dictionary var collection_name : String -var firestore # FirebaseFirestore (can't static type due to cyclic reference) var _base_url : String var _extended_url : String @@ -36,107 +36,135 @@ var _request_queues := {} ## @return FirestoreTask ## used to GET a document from the collection, specify @document_id func get_doc(document_id : String) -> FirestoreTask: - var task : FirestoreTask = FirestoreTask.new() - task.action = FirestoreTask.Task.TASK_GET - task.data = collection_name + "/" + document_id - var url = _get_request_url() + _separator + document_id.replace(" ", "%20") + var task : FirestoreTask = FirestoreTask.new() + task.action = FirestoreTask.Task.TASK_GET + task.data = collection_name + "/" + document_id + var url = _get_request_url() + _separator + document_id.replace(" ", "%20") - task.get_document.connect(_on_get_document) - task.task_finished.connect(_on_task_finished.bind(document_id), CONNECT_DEFERRED) - _process_request(task, document_id, url) - return task + task.get_document.connect(_on_get_document) + task.task_finished.connect(_on_task_finished.bind(document_id), CONNECT_DEFERRED) + _process_request(task, document_id, url) + return task ## @args document_id, fields ## @arg-defaults , {} ## @return FirestoreTask ## used to SAVE/ADD a new document to the collection, specify @documentID and @fields func add(document_id : String, fields : Dictionary = {}) -> FirestoreTask: - var task : FirestoreTask = FirestoreTask.new() - task.action = FirestoreTask.Task.TASK_POST - task.data = collection_name + "/" + document_id - var url = _get_request_url() + _query_tag + _documentId_tag + document_id + var task : FirestoreTask = FirestoreTask.new() + task.action = FirestoreTask.Task.TASK_POST + task.data = collection_name + "/" + document_id + var url = _get_request_url() + _query_tag + _documentId_tag + document_id - task.add_document.connect(_on_add_document) - task.task_finished.connect(_on_task_finished.bind(document_id), CONNECT_DEFERRED) - _process_request(task, document_id, url, JSON.stringify(FirestoreDocument.dict2fields(fields))) - return task + task.add_document.connect(_on_add_document) + task.task_finished.connect(_on_task_finished.bind(document_id), CONNECT_DEFERRED) + _process_request(task, document_id, url, JSON.stringify(FirestoreDocument.dict2fields(fields))) + return task ## @args document_id, fields ## @arg-defaults , {} ## @return FirestoreTask -# used to UPDATE a document, specify @documentID and @fields +# used to UPDATE a document, specify @documentID, @fields func update(document_id : String, fields : Dictionary = {}) -> FirestoreTask: - var task : FirestoreTask = FirestoreTask.new() - task.action = FirestoreTask.Task.TASK_PATCH - task.data = collection_name + "/" + document_id - var url = _get_request_url() + _separator + document_id.replace(" ", "%20") + "?" - for key in fields.keys(): - url+="updateMask.fieldPaths={key}&".format({key = key}) - url = url.rstrip("&") - - task.update_document.connect(_on_update_document) - task.task_finished.connect(_on_task_finished.bind(document_id), CONNECT_DEFERRED) - _process_request(task, document_id, url, JSON.stringify(FirestoreDocument.dict2fields(fields))) - return task + var task : FirestoreTask = FirestoreTask.new() + task.action = FirestoreTask.Task.TASK_PATCH + task.data = collection_name + "/" + document_id + var url = _get_request_url() + _separator + document_id.replace(" ", "%20") + "?" + for key in fields.keys(): + url+="updateMask.fieldPaths={key}&".format({key = key}) + + url = url.rstrip("&") + + for key in fields.keys(): + if fields[key] == null: + fields.erase(key) + + task.update_document.connect(_on_update_document) + task.task_finished.connect(_on_task_finished.bind(document_id), CONNECT_DEFERRED) + var body = FirestoreDocument.dict2fields(fields) + + _process_request(task, document_id, url, JSON.stringify(body)) + return task + +func commit(document : FirestoreDocument) -> FirestoreTask: + var task : FirestoreTask = FirestoreTask.new() + task.action = FirestoreTask.Task.TASK_COMMIT + var url = _base_url + _extended_url.rstrip("/") + ":commit" + task.commit_document.connect(_on_commit_document) + task.task_finished.connect(_on_task_finished.bind(document.doc_name), CONNECT_DEFERRED) + + document._transforms.set_config( + { + "extended_url": _extended_url, + "collection_name": collection_name + } + ) # Only place we can set this is here, oofness + + var body = document._transforms.serialize() + _process_request(task, document.doc_name, url, JSON.stringify(body)) + return task ## @args document_id ## @return FirestoreTask # used to DELETE a document, specify @document_id func delete(document_id : String) -> FirestoreTask: - var task : FirestoreTask = FirestoreTask.new() - task.action = FirestoreTask.Task.TASK_DELETE - task.data = collection_name + "/" + document_id - var url = _get_request_url() + _separator + document_id.replace(" ", "%20") + var task : FirestoreTask = FirestoreTask.new() + task.action = FirestoreTask.Task.TASK_DELETE + task.data = collection_name + "/" + document_id + var url = _get_request_url() + _separator + document_id.replace(" ", "%20") - task.delete_document.connect(_on_delete_document) - task.task_finished.connect(_on_task_finished.bind(document_id), CONNECT_DEFERRED) - _process_request(task, document_id, url) - return task + task.delete_document.connect(_on_delete_document) + task.task_finished.connect(_on_task_finished.bind(document_id), CONNECT_DEFERRED) + _process_request(task, document_id, url) + return task # ----------------- Functions func _get_request_url() -> String: - return _base_url + _extended_url + collection_name + return _base_url + _extended_url + collection_name func _process_request(task : FirestoreTask, document_id : String, url : String, fields := "") -> void: - if not task.task_error.is_connected(_on_error): - task.task_error.connect(_on_error) - - if auth == null or auth.is_empty(): - Firebase._print("Unauthenticated request issued...") - Firebase.Auth.login_anonymous() - var result : Array = await Firebase.Auth.auth_request - if result[0] != 1: - Firebase.Firestore._check_auth_error(result[0], result[1]) - return - Firebase._print("Client authenticated as Anonymous User.") - - task._url = url - task._fields = fields - task._headers = PackedStringArray([_AUTHORIZATION_HEADER + auth.idtoken]) - if _request_queues.has(document_id) and not _request_queues[document_id].is_empty(): - _request_queues[document_id].append(task) - else: - _request_queues[document_id] = [] - firestore._pooled_request(task) + if not task.task_error.is_connected(_on_error): + task.task_error.connect(_on_error) + + if auth == null or auth.is_empty(): + Firebase._print("Unauthenticated request issued...") + Firebase.Auth.login_anonymous() + var result : Array = await Firebase.Auth.auth_request + if result[0] != 1: + Firebase.Firestore._check_auth_error(result[0], result[1]) + return + Firebase._print("Client authenticated as Anonymous User.") + + task._url = url + task._fields = fields + task._headers = PackedStringArray([_AUTHORIZATION_HEADER + auth.idtoken]) + if _request_queues.has(document_id) and not _request_queues[document_id].is_empty(): + _request_queues[document_id].append(task) + else: + _request_queues[document_id] = [] + Firebase.Firestore._pooled_request(task) func _on_task_finished(task : FirestoreTask, document_id : String) -> void: - if not _request_queues[document_id].is_empty(): - task._push_request(task._url, _AUTHORIZATION_HEADER + auth.idtoken, task._fields) + if not _request_queues[document_id].is_empty(): + task._push_request(task._url, _AUTHORIZATION_HEADER + auth.idtoken, task._fields) # -------------------- Higher level of communication with signals func _on_get_document(document : FirestoreDocument): - get_document.emit(document) + get_document.emit(document) func _on_add_document(document : FirestoreDocument): - add_document.emit(document) + add_document.emit(document) func _on_update_document(document : FirestoreDocument): - update_document.emit(document) + update_document.emit(document) -func _on_delete_document(): - delete_document.emit() +func _on_delete_document(deleted): + delete_document.emit(deleted) func _on_error(code, status, message, task): - error.emit(code, status, message) - Firebase._printerr(message) + error.emit(code, status, message) + Firebase._printerr(message) + +func _on_commit_document(result): + commit_document.emit(result) diff --git a/addons/godot-firebase/firestore/firestore_document.gd b/addons/godot-firebase/firestore/firestore_document.gd index 12039b6..f31520f 100644 --- a/addons/godot-firebase/firestore/firestore_document.gd +++ b/addons/godot-firebase/firestore/firestore_document.gd @@ -15,147 +15,166 @@ var document : Dictionary # the Document itself var doc_fields : Dictionary # only .fields var doc_name : String # only .name var create_time : String # createTime +var _transforms : FieldTransformArray # The transforms to apply func _init(doc : Dictionary = {},_doc_name : String = "",_doc_fields : Dictionary = {}): - self.document = doc - self.doc_name = doc.name - if self.doc_name.count("/") > 2: - self.doc_name = (self.doc_name.split("/") as Array).back() - self.doc_fields = fields2dict(self.document) - self.create_time = doc.createTime + _transforms = FieldTransformArray.new() + + document = doc + doc_name = doc.name + if doc_name.count("/") > 2: + doc_name = (doc_name.split("/") as Array).back() + + doc_fields = fields2dict(self.document) + + self.create_time = doc.createTime # Pass a dictionary { 'key' : 'value' } to format it in a APIs usable .fields # Field Path3D using the "dot" (`.`) notation are supported: # ex. { "PATH.TO.SUBKEY" : "VALUE" } ==> { "PATH" : { "TO" : { "SUBKEY" : "VALUE" } } } static func dict2fields(dict : Dictionary) -> Dictionary: - var fields : Dictionary = {} - var var_type : String = "" - for field in dict.keys(): - var field_value = dict[field] - if "." in field: - var keys: Array = field.split(".") - field = keys.pop_front() - keys.reverse() - for key in keys: - field_value = { key : field_value } - match typeof(field_value): - TYPE_NIL: var_type = "nullValue" - TYPE_BOOL: var_type = "booleanValue" - TYPE_INT: var_type = "integerValue" - TYPE_FLOAT: var_type = "doubleValue" - TYPE_STRING: var_type = "stringValue" - TYPE_DICTIONARY: - if is_field_timestamp(field_value): - var_type = "timestampValue" - field_value = dict2timestamp(field_value) - else: - var_type = "mapValue" - field_value = dict2fields(field_value) - TYPE_ARRAY: - var_type = "arrayValue" - field_value = {"values": array2fields(field_value)} + var fields = {} + var var_type : String = "" + for field in dict.keys(): + var field_value = dict[field] + if "." in field: + var keys: Array = field.split(".") + field = keys.pop_front() + keys.reverse() + for key in keys: + field_value = { key : field_value } + + match typeof(field_value): + TYPE_NIL: var_type = "nullValue" + TYPE_BOOL: var_type = "booleanValue" + TYPE_INT: var_type = "integerValue" + TYPE_FLOAT: var_type = "doubleValue" + TYPE_STRING: var_type = "stringValue" + TYPE_DICTIONARY: + if is_field_timestamp(field_value): + var_type = "timestampValue" + field_value = dict2timestamp(field_value) + else: + var_type = "mapValue" + field_value = dict2fields(field_value) + TYPE_ARRAY: + var_type = "arrayValue" + field_value = {"values": array2fields(field_value)} + + if fields.has(field) and fields[field].has("mapValue") and field_value.has("fields"): + for key in field_value["fields"].keys(): + fields[field]["mapValue"]["fields"][key] = field_value["fields"][key] + else: + fields[field] = { var_type : field_value } + + return {'fields' : fields} + +func add_field_transform(transform : FieldTransform) -> void: + _transforms.push_back(transform) - if fields.has(field) and fields[field].has("mapValue") and field_value.has("fields"): - for key in field_value["fields"].keys(): - fields[field]["mapValue"]["fields"][key] = field_value["fields"][key] - else: - fields[field] = { var_type : field_value } - return {'fields' : fields} +func remove_field(field_path : String) -> void: + if document.has(field_path): + document[field_path] = null + + if doc_fields.has(field_path): + doc_fields[field_path] = null # Pass the .fields inside a Firestore Document to print out the Dictionary { 'key' : 'value' } -static func fields2dict(doc : Dictionary) -> Dictionary: - var dict : Dictionary = {} - if doc.has("fields"): - for field in (doc.fields).keys(): - if (doc.fields)[field].has("mapValue"): - dict[field] = fields2dict((doc.fields)[field].mapValue) - elif (doc.fields)[field].has("timestampValue"): - dict[field] = timestamp2dict((doc.fields)[field].timestampValue) - elif (doc.fields)[field].has("arrayValue"): - dict[field] = fields2array((doc.fields)[field].arrayValue) - elif (doc.fields)[field].has("integerValue"): - dict[field] = (doc.fields)[field].values()[0] as int - elif (doc.fields)[field].has("doubleValue"): - dict[field] = (doc.fields)[field].values()[0] as float - elif (doc.fields)[field].has("booleanValue"): - dict[field] = (doc.fields)[field].values()[0] as bool - elif (doc.fields)[field].has("nullValue"): - dict[field] = null - else: - dict[field] = (doc.fields)[field].values()[0] - return dict +static func fields2dict(doc) -> Dictionary: + var dict = {} + if doc.has("fields"): + var fields = doc["fields"] + print(fields) + for field in fields.keys(): + if fields[field].has("mapValue"): + dict[field] = (fields2dict(fields[field].mapValue)) + elif fields[field].has("timestampValue"): + dict[field] = timestamp2dict(fields[field].timestampValue) + elif fields[field].has("arrayValue"): + dict[field] = fields2array(fields[field].arrayValue) + elif fields[field].has("integerValue"): + dict[field] = fields[field].values()[0] as int + elif fields[field].has("doubleValue"): + dict[field] = fields[field].values()[0] as float + elif fields[field].has("booleanValue"): + dict[field] = fields[field].values()[0] as bool + elif fields[field].has("nullValue"): + dict[field] = null + else: + dict[field] = fields[field].values()[0] + return dict # Pass an Array to parse it to a Firebase arrayValue static func array2fields(array : Array) -> Array: - var fields : Array = [] - var var_type : String = "" - for field in array: - match typeof(field): - TYPE_DICTIONARY: - if is_field_timestamp(field): - var_type = "timestampValue" - field = dict2timestamp(field) - else: - var_type = "mapValue" - field = dict2fields(field) - TYPE_NIL: var_type = "nullValue" - TYPE_BOOL: var_type = "booleanValue" - TYPE_INT: var_type = "integerValue" - TYPE_FLOAT: var_type = "doubleValue" - TYPE_STRING: var_type = "stringValue" - TYPE_ARRAY: var_type = "arrayValue" - - fields.append({ var_type : field }) - return fields + var fields : Array = [] + var var_type : String = "" + for field in array: + match typeof(field): + TYPE_DICTIONARY: + if is_field_timestamp(field): + var_type = "timestampValue" + field = dict2timestamp(field) + else: + var_type = "mapValue" + field = dict2fields(field) + TYPE_NIL: var_type = "nullValue" + TYPE_BOOL: var_type = "booleanValue" + TYPE_INT: var_type = "integerValue" + TYPE_FLOAT: var_type = "doubleValue" + TYPE_STRING: var_type = "stringValue" + TYPE_ARRAY: var_type = "arrayValue" + _: var_type = "FieldTransform" + fields.append({ var_type : field }) + return fields # Pass a Firebase arrayValue Dictionary to convert it back to an Array static func fields2array(array : Dictionary) -> Array: - var fields : Array = [] - if array.has("values"): - for field in array.values: - var item - match field.keys()[0]: - "mapValue": - item = fields2dict(field.mapValue) - "arrayValue": - item = fields2array(field.arrayValue) - "integerValue": - item = field.values()[0] as int - "doubleValue": - item = field.values()[0] as float - "booleanValue": - item = field.values()[0] as bool - "timestampValue": - item = timestamp2dict(field.timestampValue) - "nullValue": - item = null - _: - item = field.values()[0] - fields.append(item) - return fields + var fields : Array = [] + if array.has("values"): + for field in array.values: + var item + match field.keys()[0]: + "mapValue": + item = fields2dict(field.mapValue) + "arrayValue": + item = fields2array(field.arrayValue) + "integerValue": + item = field.values()[0] as int + "doubleValue": + item = field.values()[0] as float + "booleanValue": + item = field.values()[0] as bool + "timestampValue": + item = timestamp2dict(field.timestampValue) + "nullValue": + item = null + _: + item = field.values()[0] + fields.append(item) + return fields # Converts a gdscript Dictionary (most likely obtained with Time.get_datetime_dict_from_system()) to a Firebase Timestamp static func dict2timestamp(dict : Dictionary) -> String: - dict.erase('weekday') - dict.erase('dst') - var dict_values : Array = dict.values() - return "%04d-%02d-%02dT%02d:%02d:%02d.00Z" % dict_values + dict.erase('weekday') + dict.erase('dst') + var dict_values : Array = dict.values() + return "%04d-%02d-%02dT%02d:%02d:%02d.00Z" % dict_values # Converts a Firebase Timestamp back to a gdscript Dictionary static func timestamp2dict(timestamp : String) -> Dictionary: - var datetime : Dictionary = {year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0} - var dict : PackedStringArray = timestamp.split("T")[0].split("-") - dict.append_array(timestamp.split("T")[1].split(":")) - for value in dict.size() : - datetime[datetime.keys()[value]] = int(dict[value]) - return datetime + var datetime : Dictionary = {year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0} + var dict : PackedStringArray = timestamp.split("T")[0].split("-") + dict.append_array(timestamp.split("T")[1].split(":")) + for value in dict.size() : + datetime[datetime.keys()[value]] = int(dict[value]) + return datetime static func is_field_timestamp(field : Dictionary) -> bool: - return field.has_all(['year','month','day','hour','minute','second']) + return field.has_all(['year','month','day','hour','minute','second']) # Call print(document) to return directly this document formatted func _to_string() -> String: - return ("doc_name: {doc_name}, \ndoc_fields: {doc_fields}, \ncreate_time: {create_time}\n").format( - {doc_name = self.doc_name, - doc_fields = self.doc_fields, - create_time = self.create_time}) + return ("doc_name: {doc_name}, \ndoc_fields: {doc_fields}, \ncreate_time: {create_time}\n").format( + {doc_name = self.doc_name, + doc_fields = self.doc_fields, + create_time = self.create_time}) diff --git a/addons/godot-firebase/firestore/firestore_query.gd b/addons/godot-firebase/firestore/firestore_query.gd index 6a22e81..fc6d034 100644 --- a/addons/godot-firebase/firestore/firestore_query.gd +++ b/addons/godot-firebase/firestore/firestore_query.gd @@ -7,60 +7,60 @@ extends RefCounted class_name FirestoreQuery class Order: - var obj : Dictionary + var obj : Dictionary class Cursor: - var values : Array - var before : bool + var values : Array + var before : bool - func _init(v : Array,b : bool): - values = v - before = b + func _init(v : Array,b : bool): + values = v + before = b signal query_result(query_result) const TEMPLATE_QUERY : Dictionary = { - select = {}, - from = [], - where = {}, - orderBy = [], - startAt = {}, - endAt = {}, - offset = 0, - limit = 0 + select = {}, + from = [], + where = {}, + orderBy = [], + startAt = {}, + endAt = {}, + offset = 0, + limit = 0 } var query : Dictionary = {} enum OPERATOR { - # Standard operators - OPERATOR_NSPECIFIED, - LESS_THAN, - LESS_THAN_OR_EQUAL, - GREATER_THAN, - GREATER_THAN_OR_EQUAL, - EQUAL, - NOT_EQUAL, - ARRAY_CONTAINS, - ARRAY_CONTAINS_ANY, - IN, - NOT_IN, - - # Unary operators - IS_NAN, - IS_NULL, - IS_NOT_NAN, - IS_NOT_NULL, - - # Complex operators - AND, - OR + # Standard operators + OPERATOR_NSPECIFIED, + LESS_THAN, + LESS_THAN_OR_EQUAL, + GREATER_THAN, + GREATER_THAN_OR_EQUAL, + EQUAL, + NOT_EQUAL, + ARRAY_CONTAINS, + ARRAY_CONTAINS_ANY, + IN, + NOT_IN, + + # Unary operators + IS_NAN, + IS_NULL, + IS_NOT_NAN, + IS_NOT_NULL, + + # Complex operators + AND, + OR } enum DIRECTION { - DIRECTION_UNSPECIFIED, - ASCENDING, - DESCENDING + DIRECTION_UNSPECIFIED, + ASCENDING, + DESCENDING } @@ -68,35 +68,35 @@ enum DIRECTION { # Fields must be added inside a list. Only a field is accepted inside the list # Leave the Array empty if you want to return the whole document func select(fields) -> FirestoreQuery: - match typeof(fields): - TYPE_STRING: - query["select"] = { fields = { fieldPath = fields } } - TYPE_ARRAY: - for field in fields: - field = ({ fieldPath = field }) - query["select"] = { fields = fields } - _: - print("Type of 'fields' is not accepted.") - return self + match typeof(fields): + TYPE_STRING: + query["select"] = { fields = { fieldPath = fields } } + TYPE_ARRAY: + for field in fields: + field = ({ fieldPath = field }) + query["select"] = { fields = fields } + _: + print("Type of 'fields' is not accepted.") + return self # Select the collection you want to return the query result from # if @all_descendants also sub-collections will be returned. If false, only documents will be returned func from(collection_id : String, all_descendants : bool = true) -> FirestoreQuery: - query["from"] = [{collectionId = collection_id, allDescendants = all_descendants}] - return self + query["from"] = [{collectionId = collection_id, allDescendants = all_descendants}] + return self # @collections_array MUST be an Array of Arrays with this structure # [ ["collection_id", true/false] ] func from_many(collections_array : Array) -> FirestoreQuery: - var collections : Array = [] - for collection in collections_array: - collections.append({collectionId = collection[0], allDescendants = collection[1]}) - query["from"] = collections.duplicate(true) - return self + var collections : Array = [] + for collection in collections_array: + collections.append({collectionId = collection[0], allDescendants = collection[1]}) + query["from"] = collections.duplicate(true) + return self # Query the value of a field you want to match @@ -106,43 +106,43 @@ func from_many(collections_array : Array) -> FirestoreQuery: # @chain : from FirestoreQuery.OPERATOR.[OR/AND], use it only if you want to chain "AND" or "OR" logic with futher where() calls # eg. super.where("name", OPERATOR.EQUAL, "Matt", OPERATOR.AND).where("age", OPERATOR.LESS_THAN, 20) func where(field : String, operator : int, value = null, chain : int = -1): - if operator in [OPERATOR.IS_NAN, OPERATOR.IS_NULL, OPERATOR.IS_NOT_NAN, OPERATOR.IS_NOT_NULL]: - if (chain in [OPERATOR.AND, OPERATOR.OR]) or (query.has("where") and query.where.has("compositeFilter")): - var filters : Array = [] - if query.has("where") and query.where.has("compositeFilter"): - if chain == -1: - filters = query.where.compositeFilter.filters.duplicate(true) - chain = OPERATOR.get(query.where.compositeFilter.op) - else: - filters.append(query.where) - filters.append(create_unary_filter(field, operator)) - query["where"] = create_composite_filter(chain, filters) - else: - query["where"] = create_unary_filter(field, operator) - else: - if value == null: - print("A value must be defined to match the field: {field}".format({field = field})) - else: - if (chain in [OPERATOR.AND, OPERATOR.OR]) or (query.has("where") and query.where.has("compositeFilter")): - var filters : Array = [] - if query.has("where") and query.where.has("compositeFilter"): - if chain == -1: - filters = query.where.compositeFilter.filters.duplicate(true) - chain = OPERATOR.get(query.where.compositeFilter.op) - else: - filters.append(query.where) - filters.append(create_field_filter(field, operator, value)) - query["where"] = create_composite_filter(chain, filters) - else: - query["where"] = create_field_filter(field, operator, value) - return self + if operator in [OPERATOR.IS_NAN, OPERATOR.IS_NULL, OPERATOR.IS_NOT_NAN, OPERATOR.IS_NOT_NULL]: + if (chain in [OPERATOR.AND, OPERATOR.OR]) or (query.has("where") and query.where.has("compositeFilter")): + var filters : Array = [] + if query.has("where") and query.where.has("compositeFilter"): + if chain == -1: + filters = query.where.compositeFilter.filters.duplicate(true) + chain = OPERATOR.get(query.where.compositeFilter.op) + else: + filters.append(query.where) + filters.append(create_unary_filter(field, operator)) + query["where"] = create_composite_filter(chain, filters) + else: + query["where"] = create_unary_filter(field, operator) + else: + if value == null: + print("A value must be defined to match the field: {field}".format({field = field})) + else: + if (chain in [OPERATOR.AND, OPERATOR.OR]) or (query.has("where") and query.where.has("compositeFilter")): + var filters : Array = [] + if query.has("where") and query.where.has("compositeFilter"): + if chain == -1: + filters = query.where.compositeFilter.filters.duplicate(true) + chain = OPERATOR.get(query.where.compositeFilter.op) + else: + filters.append(query.where) + filters.append(create_field_filter(field, operator, value)) + query["where"] = create_composite_filter(chain, filters) + else: + query["where"] = create_field_filter(field, operator, value) + return self # Order by a field, defining its name and the order direction # default directoin = Ascending func order_by(field : String, direction : int = DIRECTION.ASCENDING) -> FirestoreQuery: - query["orderBy"] = [_order_object(field, direction).obj] - return self + query["orderBy"] = [_order_object(field, direction).obj] + return self # Order by a set of fields and directions @@ -150,88 +150,88 @@ func order_by(field : String, direction : int = DIRECTION.ASCENDING) -> Firestor # [@field_name , @DIRECTION.[direction]] # else, order_object() can be called to return an already parsed Dictionary func order_by_fields(order_field_list : Array) -> FirestoreQuery: - var order_list : Array = [] - for order in order_field_list: - if order is Array: - order_list.append(_order_object(order[0], order[1]).obj) - elif order is Order: - order_list.append(order.obj) - query["orderBy"] = order_list - return self + var order_list : Array = [] + for order in order_field_list: + if order is Array: + order_list.append(_order_object(order[0], order[1]).obj) + elif order is Order: + order_list.append(order.obj) + query["orderBy"] = order_list + return self func start_at(value, before : bool) -> FirestoreQuery: - var cursor : Cursor = _cursor_object(value, before) - query["startAt"] = { values = cursor.values, before = cursor.before } - print(query["startAt"]) - return self + var cursor : Cursor = _cursor_object(value, before) + query["startAt"] = { values = cursor.values, before = cursor.before } + print(query["startAt"]) + return self func end_at(value, before : bool) -> FirestoreQuery: - var cursor : Cursor = _cursor_object(value, before) - query["startAt"] = { values = cursor.values, before = cursor.before } - print(query["startAt"]) - return self + var cursor : Cursor = _cursor_object(value, before) + query["startAt"] = { values = cursor.values, before = cursor.before } + print(query["startAt"]) + return self func offset(offset : int) -> FirestoreQuery: - if offset < 0: - print("If specified, offset must be >= 0") - else: - query["offset"] = offset - return self + if offset < 0: + print("If specified, offset must be >= 0") + else: + query["offset"] = offset + return self func limit(limit : int) -> FirestoreQuery: - if limit < 0: - print("If specified, offset must be >= 0") - else: - query["limit"] = limit - return self + if limit < 0: + print("If specified, offset must be >= 0") + else: + query["limit"] = limit + return self # UTILITIES ---------------------------------------- static func _cursor_object(value, before : bool) -> Cursor: - var parse : Dictionary = FirestoreDocument.dict2fields({value = value}).fields.value - var cursor : Cursor = Cursor.new(parse.arrayValue.values if parse.has("arrayValue") else [parse], before) - return cursor + var parse : Dictionary = FirestoreDocument.dict2fields({value = value}).fields.value + var cursor : Cursor = Cursor.new(parse.arrayValue.values if parse.has("arrayValue") else [parse], before) + return cursor static func _order_object(field : String, direction : int) -> Order: - var order : Order = Order.new() - order.obj = { field = { fieldPath = field }, direction = DIRECTION.keys()[direction] } - return order + var order : Order = Order.new() + order.obj = { field = { fieldPath = field }, direction = DIRECTION.keys()[direction] } + return order func create_field_filter(field : String, operator : int, value) -> Dictionary: - return { - fieldFilter = { - field = { fieldPath = field }, - op = OPERATOR.keys()[operator], - value = FirestoreDocument.dict2fields({value = value}).fields.value - } } + return { + fieldFilter = { + field = { fieldPath = field }, + op = OPERATOR.keys()[operator], + value = FirestoreDocument.dict2fields({value = value}).fields.value + } } func create_unary_filter(field : String, operator : int) -> Dictionary: - return { - unaryFilter = { - field = { fieldPath = field }, - op = OPERATOR.keys()[operator], - } } + return { + unaryFilter = { + field = { fieldPath = field }, + op = OPERATOR.keys()[operator], + } } func create_composite_filter(operator : int, filters : Array) -> Dictionary: - return { - compositeFilter = { - op = OPERATOR.keys()[operator], - filters = filters - } } + return { + compositeFilter = { + op = OPERATOR.keys()[operator], + filters = filters + } } func clean() -> void: - query = { } + query = { } func _to_string() -> String: - var pretty : String = "QUERY:\n" - for key in query.keys(): - pretty += "- {key} = {value}\n".format({key = key, value = query.get(key)}) - return pretty + var pretty : String = "QUERY:\n" + for key in query.keys(): + pretty += "- {key} = {value}\n".format({key = key, value = query.get(key)}) + return pretty diff --git a/addons/godot-firebase/firestore/firestore_task.gd b/addons/godot-firebase/firestore/firestore_task.gd index ec24974..8fd659f 100644 --- a/addons/godot-firebase/firestore/firestore_task.gd +++ b/addons/godot-firebase/firestore/firestore_task.gd @@ -33,6 +33,9 @@ signal get_document(doc) ## Emitted when a [code]update(document)[/code] request checked a [class FirebaseCollection] is successfully completed. [code]error()[/code] signal will be emitted otherwise and [code]null[/code] will be passed as a result. ## @arg-types FirestoreDocument signal update_document(doc) +## Emitted when a [code]write(document)[/code] request for a document is successfully completed. [code]error()[/code] signal will be emitted otherwise and [code]result[/code] will be passed as a result. +## @arg-types FirestoreDocument +signal commit_document(result) ## Emitted when a [code]delete(document)[/code] request checked a [class FirebaseCollection] is successfully completed and [code]true[/code] will be passed. [code]error()[/code] signal will be emitted otherwise and [code]false[/code] will be passed as a result. ## @arg-types bool signal delete_document(success) @@ -47,22 +50,24 @@ signal result_query(result) signal task_error(code, status, message, task) enum Task { - TASK_GET, ## A GET Request Task, processing a get() request - TASK_POST, ## A POST Request Task, processing add() request - TASK_PATCH, ## A PATCH Request Task, processing a update() request - TASK_DELETE, ## A DELETE Request Task, processing a delete() request - TASK_QUERY, ## A POST Request Task, processing a query() request - TASK_LIST ## A POST Request Task, processing a list() request + TASK_GET, ## A GET Request Task, processing a get() request + TASK_POST, ## A POST Request Task, processing add() request + TASK_PATCH, ## A PATCH Request Task, processing a update() request + TASK_DELETE, ## A DELETE Request Task, processing a delete() request + TASK_QUERY, ## A POST Request Task, processing a query() request + TASK_LIST, ## A POST Request Task, processing a list() request + TASK_COMMIT ## A POST Request Task that hits the write api } ## Mapping of Task enum values to descriptions for use in printing user-friendly error codes. const TASK_MAP = { - Task.TASK_GET: "GET DOCUMENT", - Task.TASK_POST: "ADD DOCUMENT", - Task.TASK_PATCH: "UPDATE DOCUMENT", - Task.TASK_DELETE: "DELETE DOCUMENT", - Task.TASK_QUERY: "QUERY COLLECTION", - Task.TASK_LIST: "LIST DOCUMENTS" + Task.TASK_GET: "GET DOCUMENT", + Task.TASK_POST: "ADD DOCUMENT", + Task.TASK_PATCH: "UPDATE DOCUMENT", + Task.TASK_DELETE: "DELETE DOCUMENT", + Task.TASK_QUERY: "QUERY COLLECTION", + Task.TASK_LIST: "LIST DOCUMENTS", + Task.TASK_COMMIT: "COMMIT DOCUMENT" } ## The code indicating the request Firestore is processing. @@ -86,128 +91,134 @@ var _fields : String = "" var _headers : PackedStringArray = [] func _on_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: - var bod = body.get_string_from_utf8() - if bod != "": - bod = Utilities.get_json_data(bod) - - var offline: bool = typeof(bod) == TYPE_NIL - var failed: bool = bod is Dictionary and bod.has("error") and response_code != HTTPClient.RESPONSE_OK - from_cache = offline - - # Probably going to regret this... - if response_code == HTTPClient.RESPONSE_OK: - data = bod - match action: - Task.TASK_POST: - document = FirestoreDocument.new(bod) - add_document.emit(document) - Task.TASK_GET: - document = FirestoreDocument.new(bod) - get_document.emit(document) - Task.TASK_PATCH: - document = FirestoreDocument.new(bod) - update_document.emit(document) - Task.TASK_DELETE: - delete_document.emit(true) - Task.TASK_QUERY: - data = [] - for doc in bod: - if doc.has('document'): - data.append(FirestoreDocument.new(doc.document)) - result_query.emit(data) - Task.TASK_LIST: - data = [] - if bod.has('documents'): - for doc in bod.documents: - data.append(FirestoreDocument.new(doc)) - if bod.has("nextPageToken"): - data.append(bod.nextPageToken) - listed_documents.emit(data) - else: - var description = "" - if TASK_MAP.has(action): - description = "(" + TASK_MAP[action] + ")" - - Firebase._printerr("Action in error was: " + str(action) + " " + description) - emit_error(task_error, bod, action) - match action: - Task.TASK_POST: - add_document.emit(null) - Task.TASK_GET: - get_document.emit(null) - Task.TASK_PATCH: - update_document.emit(null) - Task.TASK_DELETE: - delete_document.emit(false) - Task.TASK_QUERY: - data = [] - result_query.emit(data) - Task.TASK_LIST: - data = [] - listed_documents.emit(data) - - task_finished.emit(self) + var bod = body.get_string_from_utf8() + if bod != "": + bod = Utilities.get_json_data(bod) + + var offline: bool = typeof(bod) == TYPE_NIL + var failed: bool = bod is Dictionary and bod.has("error") and response_code != HTTPClient.RESPONSE_OK + from_cache = offline + + # Probably going to regret this... + if response_code == HTTPClient.RESPONSE_OK: + data = bod + match action: + Task.TASK_POST: + document = FirestoreDocument.new(bod) + add_document.emit(document) + Task.TASK_GET: + document = FirestoreDocument.new(bod) + get_document.emit(document) + Task.TASK_PATCH: + document = FirestoreDocument.new(bod) + update_document.emit(document) + Task.TASK_DELETE: + delete_document.emit(true) + Task.TASK_QUERY: + data = [] + for doc in bod: + if doc.has('document'): + data.append(FirestoreDocument.new(doc.document)) + result_query.emit(data) + Task.TASK_LIST: + data = [] + if bod.has('documents'): + for doc in bod.documents: + data.append(FirestoreDocument.new(doc)) + if bod.has("nextPageToken"): + data.append(bod.nextPageToken) + listed_documents.emit(data) + Task.TASK_COMMIT: + commit_document.emit(bod) + else: + var description = "" + if TASK_MAP.has(action): + description = "(" + TASK_MAP[action] + ")" + + Firebase._printerr("Action in error was: " + str(action) + " " + description) + emit_error(task_error, bod, action) + match action: + Task.TASK_POST: + add_document.emit(null) + Task.TASK_GET: + get_document.emit(null) + Task.TASK_PATCH: + update_document.emit(null) + Task.TASK_DELETE: + delete_document.emit(false) + Task.TASK_QUERY: + data = [] + result_query.emit(data) + Task.TASK_LIST: + data = [] + listed_documents.emit(data) + Task.TASK_COMMIT: + commit_document.emit(null) + + task_finished.emit(self) func emit_error(_signal, bod, task) -> void: - if bod: - if bod is Array and bod.size() > 0 and bod[0].has("error"): - error = bod[0].error - elif bod is Dictionary and bod.keys().size() > 0 and bod.has("error"): - error = bod.error + if bod: + if bod is Array and bod.size() > 0 and bod[0].has("error"): + error = bod[0].error + elif bod is Dictionary and bod.keys().size() > 0 and bod.has("error"): + error = bod.error - _signal.emit(error.code, error.status, error.message, task) + _signal.emit(error.code, error.status, error.message, task) - return + return - _signal.emit(1, 0, "Unknown error", task) + _signal.emit(1, 0, "Unknown error", task) func set_action(value : int) -> void: - action = value - match action: - Task.TASK_GET, Task.TASK_LIST: - _method = HTTPClient.METHOD_GET - Task.TASK_POST, Task.TASK_QUERY: - _method = HTTPClient.METHOD_POST - Task.TASK_PATCH: - _method = HTTPClient.METHOD_PATCH - Task.TASK_DELETE: - _method = HTTPClient.METHOD_DELETE + action = value + match action: + Task.TASK_GET, Task.TASK_LIST: + _method = HTTPClient.METHOD_GET + Task.TASK_POST, Task.TASK_QUERY: + _method = HTTPClient.METHOD_POST + Task.TASK_PATCH: + _method = HTTPClient.METHOD_PATCH + Task.TASK_DELETE: + _method = HTTPClient.METHOD_DELETE + Task.TASK_COMMIT: + _method = HTTPClient.METHOD_POST func _handle_cache(offline : bool, data, encrypt_key : String, cache_path : String, body) -> Dictionary: - return body # Removing caching for now, hopefully this works without killing everyone and everything + return body # Removing caching for now, hopefully this works without killing everyone and everything func _merge_dict(dic_a : Dictionary, dic_b : Dictionary, nullify := false) -> Dictionary: - var ret := dic_a.duplicate(true) - for key in dic_b: - var val = dic_b[key] + var ret := dic_a.duplicate(true) + for key in dic_b: + var val = dic_b[key] - if val == null and nullify: - ret.erase(key) - elif val is Array: - ret[key] = _merge_array(ret.get(key) if ret.get(key) else [], val) - elif val is Dictionary: - ret[key] = _merge_dict(ret.get(key) if ret.get(key) else {}, val) - else: - ret[key] = val - return ret + if val == null and nullify: + ret.erase(key) + elif val is Array: + ret[key] = _merge_array(ret.get(key) if ret.get(key) else [], val) + elif val is Dictionary: + ret[key] = _merge_dict(ret.get(key) if ret.get(key) else {}, val) + else: + ret[key] = val + return ret func _merge_array(arr_a : Array, arr_b : Array, nullify := false) -> Array: - var ret := arr_a.duplicate(true) - ret.resize(len(arr_b)) - - var deletions := 0 - for i in len(arr_b): - var index : int = i - deletions - var val = arr_b[index] - if val == null and nullify: - ret.remove_at(index) - deletions += i - elif val is Array: - ret[index] = _merge_array(ret[index] if ret[index] else [], val) - elif val is Dictionary: - ret[index] = _merge_dict(ret[index] if ret[index] else {}, val) - else: - ret[index] = val - return ret + var ret := arr_a.duplicate(true) + ret.resize(len(arr_b)) + + var deletions := 0 + for i in len(arr_b): + var index : int = i - deletions + var val = arr_b[index] + if val == null and nullify: + ret.remove_at(index) + deletions += i + elif val is Array: + ret[index] = _merge_array(ret[index] if ret[index] else [], val) + elif val is Dictionary: + ret[index] = _merge_dict(ret[index] if ret[index] else {}, val) + else: + ret[index] = val + return ret diff --git a/addons/godot-firebase/firestore/firestore_transform.gd b/addons/godot-firebase/firestore/firestore_transform.gd new file mode 100644 index 0000000..6de6597 --- /dev/null +++ b/addons/godot-firebase/firestore/firestore_transform.gd @@ -0,0 +1,3 @@ +class_name FirestoreTransform +extends RefCounted + diff --git a/addons/godot-firebase/functions/function_task.gd b/addons/godot-firebase/functions/function_task.gd index ba35b09..96a6222 100644 --- a/addons/godot-firebase/functions/function_task.gd +++ b/addons/godot-firebase/functions/function_task.gd @@ -42,18 +42,18 @@ var _fields : String = "" var _headers : PackedStringArray = [] func _on_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: - var bod = Utilities.get_json_data(body) - if bod == null: - bod = {content = body.get_string_from_utf8()} # I don't understand what this line does at all. What the hell?! + var bod = Utilities.get_json_data(body) + if bod == null: + bod = {content = body.get_string_from_utf8()} # I don't understand what this line does at all. What the hell?! - var offline: bool = typeof(bod) == TYPE_NIL - from_cache = offline + var offline: bool = typeof(bod) == TYPE_NIL + from_cache = offline - data = bod - if response_code == HTTPClient.RESPONSE_OK and data!=null: - function_executed.emit(result, data) - else: - error = {result=result, response_code=response_code, data=data} - task_error.emit(result, response_code, str(data)) + data = bod + if response_code == HTTPClient.RESPONSE_OK and data!=null: + function_executed.emit(result, data) + else: + error = {result=result, response_code=response_code, data=data} + task_error.emit(result, response_code, str(data)) - task_finished.emit(data) + task_finished.emit(data) diff --git a/addons/godot-firebase/functions/functions.gd b/addons/godot-firebase/functions/functions.gd index b1567e6..605a180 100644 --- a/addons/godot-firebase/functions/functions.gd +++ b/addons/godot-firebase/functions/functions.gd @@ -51,168 +51,168 @@ var _http_request_pool : Array = [] var _offline: bool = false : set = _set_offline func _ready() -> void: - pass + pass func _process(delta : float) -> void: - for i in range(_http_request_pool.size() - 1, -1, -1): - var request = _http_request_pool[i] - if not request.get_meta("requesting"): - var lifetime: float = request.get_meta("lifetime") + delta - if lifetime > _MAX_POOLED_REQUEST_AGE: - request.queue_free() - _http_request_pool.remove_at(i) - return # Prevent setting a value on request after it's already been queue_freed - request.set_meta("lifetime", lifetime) + for i in range(_http_request_pool.size() - 1, -1, -1): + var request = _http_request_pool[i] + if not request.get_meta("requesting"): + var lifetime: float = request.get_meta("lifetime") + delta + if lifetime > _MAX_POOLED_REQUEST_AGE: + request.queue_free() + _http_request_pool.remove_at(i) + return # Prevent setting a value on request after it's already been queue_freed + request.set_meta("lifetime", lifetime) ## @args ## @return FunctionTask func execute(function: String, method: int, params: Dictionary = {}, body: Dictionary = {}) -> FunctionTask: - var function_task : FunctionTask = FunctionTask.new() - function_task.task_error.connect(_on_task_error) - function_task.task_finished.connect(_on_task_finished) - function_task.function_executed.connect(_on_function_executed) + var function_task : FunctionTask = FunctionTask.new() + function_task.task_error.connect(_on_task_error) + function_task.task_finished.connect(_on_task_finished) + function_task.function_executed.connect(_on_function_executed) - function_task._method = method + function_task._method = method - var url : String = _base_url + ("/" if not _base_url.ends_with("/") else "") + function - function_task._url = url + var url : String = _base_url + ("/" if not _base_url.ends_with("/") else "") + function + function_task._url = url - if not params.is_empty(): - url += "?" - for key in params.keys(): - url += key + "=" + params[key] + "&" + if not params.is_empty(): + url += "?" + for key in params.keys(): + url += key + "=" + params[key] + "&" - if not body.is_empty(): - function_task._headers = PackedStringArray(["Content-Type: application/json"]) - function_task._fields = JSON.stringify(body) + if not body.is_empty(): + function_task._fields = JSON.stringify(body) - _pooled_request(function_task) - return function_task + _pooled_request(function_task) + return function_task func set_networking(value: bool) -> void: - if value: - enable_networking() - else: - disable_networking() + if value: + enable_networking() + else: + disable_networking() func enable_networking() -> void: - if networking: - return - networking = true - _base_url = _base_url.replace("storeoffline", "functions") + if networking: + return + networking = true + _base_url = _base_url.replace("storeoffline", "functions") func disable_networking() -> void: - if not networking: - return - networking = false - # Pointing to an invalid url should do the trick. - _base_url = _base_url.replace("functions", "storeoffline") + if not networking: + return + networking = false + # Pointing to an invalid url should do the trick. + _base_url = _base_url.replace("functions", "storeoffline") func _set_offline(value: bool) -> void: - if value == _offline: - return + if value == _offline: + return - _offline = value - if not persistence_enabled: - return + _offline = value + if not persistence_enabled: + return - return + return func _set_config(config_json : Dictionary) -> void: - _config = config_json - _cache_loc = _config["cacheLocation"] + _config = config_json + _cache_loc = _config["cacheLocation"] - if _encrypt_key == "": _encrypt_key = _config.apiKey - _check_emulating() + if _encrypt_key == "": _encrypt_key = _config.apiKey + _check_emulating() func _check_emulating() -> void : - ## Check emulating - if not Firebase.emulating: - _base_url = "https://{zone}-{projectId}.cloudfunctions.net/".format({ zone = _config.functionsGeoZone, projectId = _config.projectId }) - else: - var port : String = _config.emulators.ports.functions - if port == "": - Firebase._printerr("You are in 'emulated' mode, but the port for Cloud Functions has not been configured.") - else: - _base_url = "http://localhost:{port}/{projectId}/{zone}/".format({ port = port, zone = _config.functionsGeoZone, projectId = _config.projectId }) + ## Check emulating + if not Firebase.emulating: + _base_url = "https://{zone}-{projectId}.cloudfunctions.net/".format({ zone = _config.functionsGeoZone, projectId = _config.projectId }) + else: + var port : String = _config.emulators.ports.functions + if port == "": + Firebase._printerr("You are in 'emulated' mode, but the port for Cloud Functions has not been configured.") + else: + _base_url = "http://localhost:{port}/{projectId}/{zone}/".format({ port = port, zone = _config.functionsGeoZone, projectId = _config.projectId }) func _pooled_request(task : FunctionTask) -> void: - if _offline: - task._on_request_completed(HTTPRequest.RESULT_CANT_CONNECT, 404, PackedStringArray(), PackedByteArray()) - return + if _offline: + task._on_request_completed(HTTPRequest.RESULT_CANT_CONNECT, 404, PackedStringArray(), PackedByteArray()) + return - if auth == null or auth.is_empty(): - Firebase._print("Unauthenticated request issued...") - Firebase.Auth.login_anonymous() - var result : Array = await Firebase.Auth.auth_request - if result[0] != 1: - _check_auth_error(result[0], result[1]) - Firebase._print("Client connected as Anonymous") + if auth == null or auth.is_empty(): + Firebase._print("Unauthenticated request issued...") + Firebase.Auth.login_anonymous() + var result : Array = await Firebase.Auth.auth_request + if result[0] != 1: + _check_auth_error(result[0], result[1]) + Firebase._print("Client connected as Anonymous") - task._headers = Array(task._headers) + [_AUTHORIZATION_HEADER + auth.idtoken] + task._headers = ["Content-Type: application/json", _AUTHORIZATION_HEADER + auth.idtoken] - var http_request : HTTPRequest - for request in _http_request_pool: - if not request.get_meta("requesting"): - http_request = request - break + var http_request : HTTPRequest + for request in _http_request_pool: + if not request.get_meta("requesting"): + http_request = request + break - if not http_request: - http_request = HTTPRequest.new() - Utilities.fix_http_request(http_request) - _http_request_pool.append(http_request) - add_child(http_request) - http_request.request_completed.connect(_on_pooled_request_completed.bind(http_request)) + if not http_request: + http_request = HTTPRequest.new() + Utilities.fix_http_request(http_request) + http_request.accept_gzip = false + _http_request_pool.append(http_request) + add_child(http_request) + http_request.request_completed.connect(_on_pooled_request_completed.bind(http_request)) - http_request.set_meta("requesting", true) - http_request.set_meta("lifetime", 0.0) - http_request.set_meta("task", task) - http_request.request(task._url, task._headers, task._method, task._fields) + http_request.set_meta("requesting", true) + http_request.set_meta("lifetime", 0.0) + http_request.set_meta("task", task) + http_request.request(task._url, task._headers, task._method, task._fields) # ------------- func _on_task_finished(data : Dictionary) : - pass + pass func _on_function_executed(result : int, data : Dictionary) : - pass + pass func _on_task_error(code : int, status : int, message : String): - task_error.emit(code, status, message) - Firebase._printerr(message) + task_error.emit(code, status, message) + Firebase._printerr(message) func _on_FirebaseAuth_login_succeeded(auth_result : Dictionary) -> void: - auth = auth_result + auth = auth_result func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void: - auth = auth_result + auth = auth_result func _on_pooled_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray, request : HTTPRequest) -> void: - request.get_meta("task")._on_request_completed(result, response_code, headers, body) - request.set_meta("requesting", false) + request.get_meta("task")._on_request_completed(result, response_code, headers, body) + request.set_meta("requesting", false) func _on_connect_check_request_completed(result : int, _response_code, _headers, _body) -> void: - _set_offline(result != HTTPRequest.RESULT_SUCCESS) + _set_offline(result != HTTPRequest.RESULT_SUCCESS) func _on_FirebaseAuth_logout() -> void: - auth = {} + auth = {} func _check_auth_error(code : int, message : String) -> void: - var err : String - match code: - 400: err = "Please, enable Anonymous Sign-in method or Authenticate the Client before issuing a request (best option)" - Firebase._printerr(err) + var err : String + match code: + 400: err = "Please, enable Anonymous Sign-in method or Authenticate the Client before issuing a request (best option)" + Firebase._printerr(err) diff --git a/addons/godot-firebase/queues/queueable_http_request.gd b/addons/godot-firebase/queues/queueable_http_request.gd new file mode 100644 index 0000000..0143a78 --- /dev/null +++ b/addons/godot-firebase/queues/queueable_http_request.gd @@ -0,0 +1,30 @@ +class_name QueueableHTTPRequest +extends HTTPRequest + +signal queue_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) + +var _queue := [] + +# Determine if we need to set Use Threads to true; it can cause collisions with get_http_client_status() due to a thread returning the data _after_ having checked the connection status and result in double-requests. + +func _ready() -> void: + request_completed.connect( + func(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray): + queue_request_completed.emit(result, response_code, headers, body) + + if not _queue.is_empty(): + var req = _queue.pop_front() + self.request(req.url, req.headers, req.method, req.data) + ) + +func request(url : String, headers : PackedStringArray = PackedStringArray(), method := HTTPClient.METHOD_GET, data : String = "") -> Error: + var status = get_http_client_status() + var result = OK + + if status != HTTPClient.STATUS_DISCONNECTED: + _queue.push_back({url=url, headers=headers, method=method, data=data}) + return result + + result = super.request(url, headers, method, data) + + return result diff --git a/addons/godot-firebase/queues/queueable_http_request.tscn b/addons/godot-firebase/queues/queueable_http_request.tscn new file mode 100644 index 0000000..d166941 --- /dev/null +++ b/addons/godot-firebase/queues/queueable_http_request.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://ctb4l7plg8kqg"] + +[ext_resource type="Script" path="res://addons/godot-firebase/queues/queueable_http_request.gd" id="1_2rucc"] + +[node name="QueueableHTTPRequest" type="HTTPRequest"] +script = ExtResource("1_2rucc") diff --git a/addons/godot-firebase/remote_config/firebase_remote_config.gd b/addons/godot-firebase/remote_config/firebase_remote_config.gd new file mode 100644 index 0000000..ee3653e --- /dev/null +++ b/addons/godot-firebase/remote_config/firebase_remote_config.gd @@ -0,0 +1,36 @@ +@tool +class_name FirebaseRemoteConfig +extends Node + +const RemoteConfigFunctionId = "getRemoteConfig" + +signal remote_config_received(config) +signal remote_config_error(error) + +var _project_config = {} +var _headers : PackedStringArray = [ +] +var _auth : Dictionary + +func _set_config(config_json : Dictionary) -> void: + _project_config = config_json # This may get confusing, hoping the variable name makes it easier to understand + +func _on_FirebaseAuth_login_succeeded(auth_result : Dictionary) -> void: + _auth = auth_result + +func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void: + _auth = auth_result + +func _on_FirebaseAuth_logout() -> void: + _auth = {} + +func get_remote_config() -> void: + var function_task = Firebase.Functions.execute("getRemoteConfig", HTTPClient.METHOD_GET, {}, {}) as FunctionTask + var result = await function_task.task_finished + Firebase._print("Config request result: " + str(result)) + if result.has("error"): + remote_config_error.emit(result) + return + + var config = RemoteConfig.new(result) + remote_config_received.emit(config) diff --git a/addons/godot-firebase/remote_config/firebase_remote_config.tscn b/addons/godot-firebase/remote_config/firebase_remote_config.tscn new file mode 100644 index 0000000..5c42d3f --- /dev/null +++ b/addons/godot-firebase/remote_config/firebase_remote_config.tscn @@ -0,0 +1,7 @@ +[gd_scene load_steps=2 format=3 uid="uid://5xa6ulbllkjk"] + +[ext_resource type="Script" path="res://addons/godot-firebase/remote_config/firebase_remote_config.gd" id="1_wx4ds"] + +[node name="FirebaseRemoteConfig" type="HTTPRequest"] +use_threads = true +script = ExtResource("1_wx4ds") diff --git a/addons/godot-firebase/remote_config/remote_config.gd b/addons/godot-firebase/remote_config/remote_config.gd new file mode 100644 index 0000000..2b72cc6 --- /dev/null +++ b/addons/godot-firebase/remote_config/remote_config.gd @@ -0,0 +1,14 @@ +class_name RemoteConfig +extends RefCounted + +var default_config = {} + +func _init(values : Dictionary) -> void: + default_config = values + +func get_value(key : String) -> Variant: + if default_config.has(key): + return default_config[key] + + Firebase._printerr("Remote config does not contain key: " + key) + return null diff --git a/addons/godot-firebase/storage/storage.gd b/addons/godot-firebase/storage/storage.gd index a4307c0..82a8017 100644 --- a/addons/godot-firebase/storage/storage.gd +++ b/addons/godot-firebase/storage/storage.gd @@ -47,311 +47,311 @@ var _content_length : int var _reading_body : bool func _notification(what : int) -> void: - if what == NOTIFICATION_INTERNAL_PROCESS: - _internal_process(get_process_delta_time()) + if what == NOTIFICATION_INTERNAL_PROCESS: + _internal_process(get_process_delta_time()) func _internal_process(_delta : float) -> void: - if not requesting: - set_process_internal(false) - return - - var task = _current_task - - match _http_client.get_status(): - HTTPClient.STATUS_DISCONNECTED: - _http_client.connect_to_host(_base_url, 443, TLSOptions.client()) # Uhh, check if this is going to work. I assume not. - - HTTPClient.STATUS_RESOLVING, \ - HTTPClient.STATUS_REQUESTING, \ - HTTPClient.STATUS_CONNECTING: - _http_client.poll() - - HTTPClient.STATUS_CONNECTED: - var err := _http_client.request_raw(task._method, task._url, task._headers, task.data) - if err: - _finish_request(HTTPRequest.RESULT_CONNECTION_ERROR) - - HTTPClient.STATUS_BODY: - if _http_client.has_response() or _reading_body: - _reading_body = true - - # If there is a response... - if _response_headers.is_empty(): - _response_headers = _http_client.get_response_headers() # Get response headers. - _response_code = _http_client.get_response_code() - - for header in _response_headers: - if "Content-Length" in header: - _content_length = header.trim_prefix("Content-Length: ").to_int() - - _http_client.poll() - var chunk = _http_client.read_response_body_chunk() # Get a chunk. - if chunk.size() == 0: - # Got nothing, wait for buffers to fill a bit. - pass - else: - _response_data += chunk # Append to read buffer. - if _content_length != 0: - task.progress = float(_response_data.size()) / _content_length - - if _http_client.get_status() != HTTPClient.STATUS_BODY: - task.progress = 1.0 - _finish_request(HTTPRequest.RESULT_SUCCESS) - else: - task.progress = 1.0 - _finish_request(HTTPRequest.RESULT_SUCCESS) - - HTTPClient.STATUS_CANT_CONNECT: - _finish_request(HTTPRequest.RESULT_CANT_CONNECT) - HTTPClient.STATUS_CANT_RESOLVE: - _finish_request(HTTPRequest.RESULT_CANT_RESOLVE) - HTTPClient.STATUS_CONNECTION_ERROR: - _finish_request(HTTPRequest.RESULT_CONNECTION_ERROR) - HTTPClient.STATUS_TLS_HANDSHAKE_ERROR: - _finish_request(HTTPRequest.RESULT_TLS_HANDSHAKE_ERROR) + if not requesting: + set_process_internal(false) + return + + var task = _current_task + + match _http_client.get_status(): + HTTPClient.STATUS_DISCONNECTED: + _http_client.connect_to_host(_base_url, 443, TLSOptions.client()) # Uhh, check if this is going to work. I assume not. + + HTTPClient.STATUS_RESOLVING, \ + HTTPClient.STATUS_REQUESTING, \ + HTTPClient.STATUS_CONNECTING: + _http_client.poll() + + HTTPClient.STATUS_CONNECTED: + var err := _http_client.request_raw(task._method, task._url, task._headers, task.data) + if err: + _finish_request(HTTPRequest.RESULT_CONNECTION_ERROR) + + HTTPClient.STATUS_BODY: + if _http_client.has_response() or _reading_body: + _reading_body = true + + # If there is a response... + if _response_headers.is_empty(): + _response_headers = _http_client.get_response_headers() # Get response headers. + _response_code = _http_client.get_response_code() + + for header in _response_headers: + if "Content-Length" in header: + _content_length = header.trim_prefix("Content-Length: ").to_int() + + _http_client.poll() + var chunk = _http_client.read_response_body_chunk() # Get a chunk. + if chunk.size() == 0: + # Got nothing, wait for buffers to fill a bit. + pass + else: + _response_data += chunk # Append to read buffer. + if _content_length != 0: + task.progress = float(_response_data.size()) / _content_length + + if _http_client.get_status() != HTTPClient.STATUS_BODY: + task.progress = 1.0 + _finish_request(HTTPRequest.RESULT_SUCCESS) + else: + task.progress = 1.0 + _finish_request(HTTPRequest.RESULT_SUCCESS) + + HTTPClient.STATUS_CANT_CONNECT: + _finish_request(HTTPRequest.RESULT_CANT_CONNECT) + HTTPClient.STATUS_CANT_RESOLVE: + _finish_request(HTTPRequest.RESULT_CANT_RESOLVE) + HTTPClient.STATUS_CONNECTION_ERROR: + _finish_request(HTTPRequest.RESULT_CONNECTION_ERROR) + HTTPClient.STATUS_TLS_HANDSHAKE_ERROR: + _finish_request(HTTPRequest.RESULT_TLS_HANDSHAKE_ERROR) ## @args path ## @arg-defaults "" ## @return StorageReference ## Returns a reference to a file or folder in the storage bucket. It's this reference that should be used to control the file/folder checked the server end. func ref(path := "") -> StorageReference: - if _config == null or _config.is_empty(): - return null - - # Create a root storage reference if there's none - # and we're not making one. - if path != "" and not _root_ref: - _root_ref = ref() - - path = _simplify_path(path) - if not _references.has(path): - var ref := StorageReference.new() - _references[path] = ref - ref.valid = true - ref.bucket = bucket - ref.full_path = path - ref.name = path.get_file() - ref.parent = ref(path.path_join("..")) - ref.root = _root_ref - ref.storage = self - return ref - else: - return _references[path] + if _config == null or _config.is_empty(): + return null + + # Create a root storage reference if there's none + # and we're not making one. + if path != "" and not _root_ref: + _root_ref = ref() + + path = _simplify_path(path) + if not _references.has(path): + var ref := StorageReference.new() + _references[path] = ref + ref.valid = true + ref.bucket = bucket + ref.full_path = path + ref.name = path.get_file() + ref.parent = ref(path.path_join("..")) + ref.root = _root_ref + ref.storage = self + return ref + else: + return _references[path] func _set_config(config_json : Dictionary) -> void: - _config = config_json - if bucket != _config.storageBucket: - bucket = _config.storageBucket - _http_client.close() - _check_emulating() + _config = config_json + if bucket != _config.storageBucket: + bucket = _config.storageBucket + _http_client.close() + _check_emulating() func _check_emulating() -> void : - ## Check emulating - if not Firebase.emulating: - _base_url = "https://firebasestorage.googleapis.com" - else: - var port : String = _config.emulators.ports.storage - if port == "": - Firebase._printerr("You are in 'emulated' mode, but the port for Storage has not been configured.") - else: - _base_url = "http://localhost:{port}/{version}/".format({ version = _API_VERSION, port = port }) + ## Check emulating + if not Firebase.emulating: + _base_url = "https://firebasestorage.googleapis.com" + else: + var port : String = _config.emulators.ports.storage + if port == "": + Firebase._printerr("You are in 'emulated' mode, but the port for Storage has not been configured.") + else: + _base_url = "http://localhost:{port}/{version}/".format({ version = _API_VERSION, port = port }) func _upload(data : PackedByteArray, headers : PackedStringArray, ref : StorageReference, meta_only : bool) -> StorageTask: - if _is_invalid_authentication(): - return null - - var task := StorageTask.new() - task.ref = ref - task._url = _get_file_url(ref) - task.action = StorageTask.Task.TASK_UPLOAD_META if meta_only else StorageTask.Task.TASK_UPLOAD - task._headers = headers - task.data = data - _process_request(task) - return task + if _is_invalid_authentication(): + return null + + var task := StorageTask.new() + task.ref = ref + task._url = _get_file_url(ref) + task.action = StorageTask.Task.TASK_UPLOAD_META if meta_only else StorageTask.Task.TASK_UPLOAD + task._headers = headers + task.data = data + _process_request(task) + return task func _download(ref : StorageReference, meta_only : bool, url_only : bool) -> StorageTask: - if _is_invalid_authentication(): - return null - - var info_task := StorageTask.new() - info_task.ref = ref - info_task._url = _get_file_url(ref) - info_task.action = StorageTask.Task.TASK_DOWNLOAD_URL if url_only else StorageTask.Task.TASK_DOWNLOAD_META - _process_request(info_task) - - if url_only or meta_only: - return info_task - - var task := StorageTask.new() - task.ref = ref - task._url = _get_file_url(ref) + "?alt=media&token=" - task.action = StorageTask.Task.TASK_DOWNLOAD - _pending_tasks.append(task) - - var data = await info_task.task_finished - if info_task.result == OK: - task._url += info_task.data.downloadTokens - else: - task.data = info_task.data - task.response_headers = info_task.response_headers - task.response_code = info_task.response_code - task.result = info_task.result - task.finished = true - task.task_finished.emit() - task_failed.emit(task.result, task.response_code, task.data) - _pending_tasks.erase(task) - - return task + if _is_invalid_authentication(): + return null + + var info_task := StorageTask.new() + info_task.ref = ref + info_task._url = _get_file_url(ref) + info_task.action = StorageTask.Task.TASK_DOWNLOAD_URL if url_only else StorageTask.Task.TASK_DOWNLOAD_META + _process_request(info_task) + + if url_only or meta_only: + return info_task + + var task := StorageTask.new() + task.ref = ref + task._url = _get_file_url(ref) + "?alt=media&token=" + task.action = StorageTask.Task.TASK_DOWNLOAD + _pending_tasks.append(task) + + var data = await info_task.task_finished + if info_task.result == OK: + task._url += info_task.data.downloadTokens + else: + task.data = info_task.data + task.response_headers = info_task.response_headers + task.response_code = info_task.response_code + task.result = info_task.result + task.finished = true + task.task_finished.emit() + task_failed.emit(task.result, task.response_code, task.data) + _pending_tasks.erase(task) + + return task func _list(ref : StorageReference, list_all : bool) -> StorageTask: - if _is_invalid_authentication(): - return null + if _is_invalid_authentication(): + return null - var task := StorageTask.new() - task.ref = ref - task._url = _get_file_url(_root_ref).trim_suffix("/") - task.action = StorageTask.Task.TASK_LIST_ALL if list_all else StorageTask.Task.TASK_LIST - _process_request(task) - return task + var task := StorageTask.new() + task.ref = ref + task._url = _get_file_url(_root_ref).trim_suffix("/") + task.action = StorageTask.Task.TASK_LIST_ALL if list_all else StorageTask.Task.TASK_LIST + _process_request(task) + return task func _delete(ref : StorageReference) -> StorageTask: - if _is_invalid_authentication(): - return null + if _is_invalid_authentication(): + return null - var task := StorageTask.new() - task.ref = ref - task._url = _get_file_url(ref) - task.action = StorageTask.Task.TASK_DELETE - _process_request(task) - return task + var task := StorageTask.new() + task.ref = ref + task._url = _get_file_url(ref) + task.action = StorageTask.Task.TASK_DELETE + _process_request(task) + return task func _process_request(task : StorageTask) -> void: - if requesting: - _pending_tasks.append(task) - return - requesting = true - - var headers = Array(task._headers) - headers.append("Authorization: Bearer " + _auth.idtoken) - task._headers = PackedStringArray(headers) - - _current_task = task - _response_code = 0 - _response_headers = PackedStringArray() - _response_data = PackedByteArray() - _content_length = 0 - _reading_body = false - - if not _http_client.get_status() in [HTTPClient.STATUS_CONNECTED, HTTPClient.STATUS_DISCONNECTED]: - _http_client.close() - set_process_internal(true) + if requesting: + _pending_tasks.append(task) + return + requesting = true + + var headers = Array(task._headers) + headers.append("Authorization: Bearer " + _auth.idtoken) + task._headers = PackedStringArray(headers) + + _current_task = task + _response_code = 0 + _response_headers = PackedStringArray() + _response_data = PackedByteArray() + _content_length = 0 + _reading_body = false + + if not _http_client.get_status() in [HTTPClient.STATUS_CONNECTED, HTTPClient.STATUS_DISCONNECTED]: + _http_client.close() + set_process_internal(true) func _finish_request(result : int) -> void: - var task := _current_task - requesting = false - - task.result = result - task.response_code = _response_code - task.response_headers = _response_headers - - match task.action: - StorageTask.Task.TASK_DOWNLOAD: - task.data = _response_data - - StorageTask.Task.TASK_DELETE: - _references.erase(task.ref.full_path) - task.ref.valid = false - if typeof(task.data) == TYPE_PACKED_BYTE_ARRAY: - task.data = null - - StorageTask.Task.TASK_DOWNLOAD_URL: - var json = Utilities.get_json_data(_response_data) - if json != null and json.has("downloadTokens"): - task.data = _base_url + _get_file_url(task.ref) + "?alt=media&token=" + json.downloadTokens - else: - task.data = "" - - StorageTask.Task.TASK_LIST, StorageTask.Task.TASK_LIST_ALL: - var json = Utilities.get_json_data(_response_data) - var items := [] - if json != null and json.has("items"): - for item in json.items: - var item_name : String = item.name - if item.bucket != bucket: - continue - if not item_name.begins_with(task.ref.full_path): - continue - if task.action == StorageTask.Task.TASK_LIST: - var dir_path : Array = item_name.split("/") - var slash_count : int = task.ref.full_path.count("/") - item_name = "" - for i in slash_count + 1: - item_name += dir_path[i] - if i != slash_count and slash_count != 0: - item_name += "/" - if item_name in items: - continue - - items.append(item_name) - task.data = items - - _: - var json = Utilities.get_json_data(_response_data) - task.data = json - - var next_task : StorageTask - if not _pending_tasks.is_empty(): - next_task = _pending_tasks.pop_front() - - task.finished = true - task.task_finished.emit(task.data) # I believe this parameter has been missing all along, but not sure. Caused weird results at times with a yield/await returning null, but the task containing data. - if typeof(task.data) == TYPE_DICTIONARY and task.data.has("error"): - task_failed.emit(task.result, task.response_code, task.data) - else: - task_successful.emit(task.result, task.response_code, task.data) - - while true: - if next_task and not next_task.finished: - _process_request(next_task) - break - elif not _pending_tasks.is_empty(): - next_task = _pending_tasks.pop_front() - else: - break + var task := _current_task + requesting = false + + task.result = result + task.response_code = _response_code + task.response_headers = _response_headers + + match task.action: + StorageTask.Task.TASK_DOWNLOAD: + task.data = _response_data + + StorageTask.Task.TASK_DELETE: + _references.erase(task.ref.full_path) + task.ref.valid = false + if typeof(task.data) == TYPE_PACKED_BYTE_ARRAY: + task.data = null + + StorageTask.Task.TASK_DOWNLOAD_URL: + var json = Utilities.get_json_data(_response_data) + if json != null and json.has("downloadTokens"): + task.data = _base_url + _get_file_url(task.ref) + "?alt=media&token=" + json.downloadTokens + else: + task.data = "" + + StorageTask.Task.TASK_LIST, StorageTask.Task.TASK_LIST_ALL: + var json = Utilities.get_json_data(_response_data) + var items := [] + if json != null and json.has("items"): + for item in json.items: + var item_name : String = item.name + if item.bucket != bucket: + continue + if not item_name.begins_with(task.ref.full_path): + continue + if task.action == StorageTask.Task.TASK_LIST: + var dir_path : Array = item_name.split("/") + var slash_count : int = task.ref.full_path.count("/") + item_name = "" + for i in slash_count + 1: + item_name += dir_path[i] + if i != slash_count and slash_count != 0: + item_name += "/" + if item_name in items: + continue + + items.append(item_name) + task.data = items + + _: + var json = Utilities.get_json_data(_response_data) + task.data = json + + var next_task : StorageTask + if not _pending_tasks.is_empty(): + next_task = _pending_tasks.pop_front() + + task.finished = true + task.task_finished.emit(task.data) # I believe this parameter has been missing all along, but not sure. Caused weird results at times with a yield/await returning null, but the task containing data. + if typeof(task.data) == TYPE_DICTIONARY and task.data.has("error"): + task_failed.emit(task.result, task.response_code, task.data) + else: + task_successful.emit(task.result, task.response_code, task.data) + + while true: + if next_task and not next_task.finished: + _process_request(next_task) + break + elif not _pending_tasks.is_empty(): + next_task = _pending_tasks.pop_front() + else: + break func _get_file_url(ref : StorageReference) -> String: - var url := _extended_url.replace("[APP_ID]", ref.bucket) - url = url.replace("[API_VERSION]", _API_VERSION) - return url.replace("[FILE_PATH]", ref.full_path.uri_encode()) + var url := _extended_url.replace("[APP_ID]", ref.bucket) + url = url.replace("[API_VERSION]", _API_VERSION) + return url.replace("[FILE_PATH]", ref.full_path.uri_encode()) # Removes any "../" or "./" in the file path. func _simplify_path(path : String) -> String: - var dirs := path.split("/") - var new_dirs := [] - for dir in dirs: - if dir == "..": - new_dirs.pop_back() - elif dir == ".": - pass - else: - new_dirs.push_back(dir) - - var new_path := "/".join(PackedStringArray(new_dirs)) - new_path = new_path.replace("//", "/") - new_path = new_path.replace("\\", "/") - return new_path + var dirs := path.split("/") + var new_dirs := [] + for dir in dirs: + if dir == "..": + new_dirs.pop_back() + elif dir == ".": + pass + else: + new_dirs.push_back(dir) + + var new_path := "/".join(PackedStringArray(new_dirs)) + new_path = new_path.replace("//", "/") + new_path = new_path.replace("\\", "/") + return new_path func _on_FirebaseAuth_login_succeeded(auth_token : Dictionary) -> void: - _auth = auth_token + _auth = auth_token func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void: - _auth = auth_result + _auth = auth_result func _on_FirebaseAuth_logout() -> void: - _auth = {} - + _auth = {} + func _is_invalid_authentication() -> bool: - return (_config == null or _config.is_empty()) or (_auth == null or _auth.is_empty()) + return (_config == null or _config.is_empty()) or (_auth == null or _auth.is_empty()) diff --git a/addons/godot-firebase/storage/storage_reference.gd b/addons/godot-firebase/storage/storage_reference.gd index 7f13ae2..2ce2446 100644 --- a/addons/godot-firebase/storage/storage_reference.gd +++ b/addons/godot-firebase/storage/storage_reference.gd @@ -13,31 +13,31 @@ const DEFAULT_MIME_TYPE = "application/octet-stream" ## A dictionary of common MIME types based checked a file extension. ## Example: [code]MIME_TYPES.png[/code] will return [code]image/png[/code]. const MIME_TYPES = { - "bmp": "image/bmp", - "css": "text/css", - "csv": "text/csv", - "gd": "text/plain", - "htm": "text/html", - "html": "text/html", - "jpeg": "image/jpeg", - "jpg": "image/jpeg", - "json": "application/json", - "mp3": "audio/mpeg", - "mpeg": "video/mpeg", - "ogg": "audio/ogg", - "ogv": "video/ogg", - "png": "image/png", - "shader": "text/plain", - "svg": "image/svg+xml", - "tif": "image/tiff", - "tiff": "image/tiff", - "tres": "text/plain", - "tscn": "text/plain", - "txt": "text/plain", - "wav": "audio/wav", - "webm": "video/webm", - "webp": "video/webm", - "xml": "text/xml", + "bmp": "image/bmp", + "css": "text/css", + "csv": "text/csv", + "gd": "text/plain", + "htm": "text/html", + "html": "text/html", + "jpeg": "image/jpeg", + "jpg": "image/jpeg", + "json": "application/json", + "mp3": "audio/mpeg", + "mpeg": "video/mpeg", + "ogg": "audio/ogg", + "ogv": "video/ogg", + "png": "image/png", + "shader": "text/plain", + "svg": "image/svg+xml", + "tif": "image/tiff", + "tiff": "image/tiff", + "tres": "text/plain", + "tscn": "text/plain", + "txt": "text/plain", + "wav": "audio/wav", + "webm": "video/webm", + "webp": "video/webm", + "xml": "text/xml", } ## @default "" @@ -73,111 +73,111 @@ var valid : bool = false ## @return StorageReference ## Returns a reference to another [StorageReference] relative to this one. func child(path : String) -> StorageReference: - if not valid: - return null - return storage.ref(full_path.path_join(path)) + if not valid: + return null + return storage.ref(full_path.path_join(path)) ## @args data, metadata ## @return StorageTask ## Makes an attempt to upload data to the referenced file location. Status checked this task is found in the returned [StorageTask]. func put_data(data : PackedByteArray, metadata := {}) -> StorageTask: - if not valid: - return null - if not "Content-Length" in metadata and not Utilities.is_web(): - metadata["Content-Length"] = data.size() + if not valid: + return null + if not "Content-Length" in metadata and not Utilities.is_web(): + metadata["Content-Length"] = data.size() - var headers := [] - for key in metadata: - headers.append("%s: %s" % [key, metadata[key]]) + var headers := [] + for key in metadata: + headers.append("%s: %s" % [key, metadata[key]]) - return storage._upload(data, headers, self, false) + return storage._upload(data, headers, self, false) ## @args data, metadata ## @return StorageTask ## Like [method put_data], but [code]data[/code] is a [String]. func put_string(data : String, metadata := {}) -> StorageTask: - return put_data(data.to_utf8_buffer(), metadata) + return put_data(data.to_utf8_buffer(), metadata) ## @args file_path, metadata ## @return StorageTask ## Like [method put_data], but the data comes from a file at [code]file_path[/code]. func put_file(file_path : String, metadata := {}) -> StorageTask: - var file := FileAccess.open(file_path, FileAccess.READ) - var data := file.get_buffer(file.get_length()) + var file := FileAccess.open(file_path, FileAccess.READ) + var data := file.get_buffer(file.get_length()) - if "Content-Type" in metadata: - metadata["Content-Type"] = MIME_TYPES.get(file_path.get_extension(), DEFAULT_MIME_TYPE) + if "Content-Type" in metadata: + metadata["Content-Type"] = MIME_TYPES.get(file_path.get_extension(), DEFAULT_MIME_TYPE) - return put_data(data, metadata) + return put_data(data, metadata) ## @return StorageTask ## Makes an attempt to download the files from the referenced file location. Status checked this task is found in the returned [StorageTask]. func get_data() -> StorageTask: - if not valid: - return null - storage._download(self, false, false) - return storage._pending_tasks[-1] + if not valid: + return null + storage._download(self, false, false) + return storage._pending_tasks[-1] ## @return StorageTask ## Like [method get_data], but the data in the returned [StorageTask] comes in the form of a [String]. func get_string() -> StorageTask: - var task := get_data() - task.task_finished.connect(_on_task_finished.bind(task, "stringify")) - return task + var task := get_data() + task.task_finished.connect(_on_task_finished.bind(task, "stringify")) + return task ## @return StorageTask ## Attempts to get the download url that points to the referenced file's data. Using the url directly may require an authentication header. Status checked this task is found in the returned [StorageTask]. func get_download_url() -> StorageTask: - if not valid: - return null - return storage._download(self, false, true) + if not valid: + return null + return storage._download(self, false, true) ## @return StorageTask ## Attempts to get the metadata of the referenced file. Status checked this task is found in the returned [StorageTask]. func get_metadata() -> StorageTask: - if not valid: - return null - return storage._download(self, true, false) + if not valid: + return null + return storage._download(self, true, false) ## @args metadata ## @return StorageTask ## Attempts to update the metadata of the referenced file. Any field with a value of [code]null[/code] will be deleted checked the server end. Status checked this task is found in the returned [StorageTask]. func update_metadata(metadata : Dictionary) -> StorageTask: - if not valid: - return null - var data := JSON.stringify(metadata).to_utf8_buffer() - var headers := PackedStringArray(["Accept: application/json"]) - return storage._upload(data, headers, self, true) + if not valid: + return null + var data := JSON.stringify(metadata).to_utf8_buffer() + var headers := PackedStringArray(["Accept: application/json"]) + return storage._upload(data, headers, self, true) ## @return StorageTask ## Attempts to get the list of files and/or folders under the referenced folder This function is not nested unlike [method list_all]. Status checked this task is found in the returned [StorageTask]. func list() -> StorageTask: - if not valid: - return null - return storage._list(self, false) + if not valid: + return null + return storage._list(self, false) ## @return StorageTask ## Attempts to get the list of files and/or folders under the referenced folder This function is nested unlike [method list]. Status checked this task is found in the returned [StorageTask]. func list_all() -> StorageTask: - if not valid: - return null - return storage._list(self, true) + if not valid: + return null + return storage._list(self, true) ## @return StorageTask ## Attempts to delete the referenced file/folder. If successful, the reference will become invalid And can no longer be used. If you need to reference this location again, make a new reference with [method StorageTask.ref]. Status checked this task is found in the returned [StorageTask]. func delete() -> StorageTask: - if not valid: - return null - return storage._delete(self) + if not valid: + return null + return storage._delete(self) func _to_string() -> String: - var string := "gs://%s/%s" % [bucket, full_path] - if not valid: - string += " [Invalid RefCounted]" - return string + var string := "gs://%s/%s" % [bucket, full_path] + if not valid: + string += " [Invalid RefCounted]" + return string func _on_task_finished(task : StorageTask, action : String) -> void: - match action: - "stringify": - if typeof(task.data) == TYPE_PACKED_BYTE_ARRAY: - task.data = task.data.get_string_from_utf8() + match action: + "stringify": + if typeof(task.data) == TYPE_PACKED_BYTE_ARRAY: + task.data = task.data.get_string_from_utf8() From 49e608a1ad6f472e58b41f2a97d05892c882395e Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Thu, 30 May 2024 06:09:44 -0400 Subject: [PATCH 32/49] Fix tabs --- addons/godot-firebase/auth/auth.gd | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/addons/godot-firebase/auth/auth.gd b/addons/godot-firebase/auth/auth.gd index 1cd4923..255d456 100644 --- a/addons/godot-firebase/auth/auth.gd +++ b/addons/godot-firebase/auth/auth.gd @@ -437,9 +437,9 @@ func _on_FirebaseAuth_request_completed(result : int, response_code : int, heade # Refresh token countdown auth_request.emit(1, auth) - if _needs_refresh: - _needs_refresh = false - login_succeeded.emit(auth) + if _needs_refresh: + _needs_refresh = false + login_succeeded.emit(auth) else: match res.kind: RESPONSE_SIGNUP: From 908eb9cb7ddffef8f863eb2a1057ae86037eb78a Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Sat, 1 Jun 2024 07:53:08 -0400 Subject: [PATCH 33/49] Fix completed.emit issue --- addons/godot-firebase/Utilities.gd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/godot-firebase/Utilities.gd b/addons/godot-firebase/Utilities.gd index bd6c26f..3000511 100644 --- a/addons/godot-firebase/Utilities.gd +++ b/addons/godot-firebase/Utilities.gd @@ -58,7 +58,7 @@ class SignalReducer extends RefCounted: # No need for a node, as this deals stri var awaiters : Array[Signal] = [] var reducers = { - 0 : completed.emit, + 0 : func(): completed.emit(), 1 : func(p): completed.emit(), 2 : func(p1, p2): completed.emit(), 3 : func(p1, p2, p3): completed.emit(), @@ -75,7 +75,7 @@ class SignalReducerWithResult extends RefCounted: # No need for a node, as this var awaiters : Array[Signal] = [] var reducers = { - 0 : completed.emit, + 0 : func(): completed.emit(), 1 : func(p): completed.emit({1 : p}), 2 : func(p1, p2): completed.emit({ 1 : p1, 2 : p2 }), 3 : func(p1, p2, p3): completed.emit({ 1 : p1, 2 : p2, 3 : p3 }), From 7dca31849d4454df3090782284e57d8323d420c4 Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Sat, 1 Jun 2024 08:03:31 -0400 Subject: [PATCH 34/49] Fix other tabs, ugh. --- addons/godot-firebase/auth/auth.gd | 67 +++++++++++++++--------------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/addons/godot-firebase/auth/auth.gd b/addons/godot-firebase/auth/auth.gd index 255d456..c40a38a 100644 --- a/addons/godot-firebase/auth/auth.gd +++ b/addons/godot-firebase/auth/auth.gd @@ -441,31 +441,32 @@ func _on_FirebaseAuth_request_completed(result : int, response_code : int, heade _needs_refresh = false login_succeeded.emit(auth) else: - match res.kind: - RESPONSE_SIGNUP: - auth = get_clean_keys(res) - signup_succeeded.emit(auth) - begin_refresh_countdown() - RESPONSE_SIGNIN, RESPONSE_ASSERTION, RESPONSE_CUSTOM_TOKEN: - auth = get_clean_keys(res) - login_succeeded.emit(auth) - begin_refresh_countdown() - RESPONSE_USERDATA: - var userdata = FirebaseUserData.new(res.users[0]) - userdata_received.emit(userdata) - auth_request.emit(1, auth) - else: - # error message would be INVALID_EMAIL, EMAIL_NOT_FOUND, INVALID_PASSWORD, USER_DISABLED or WEAK_PASSWORD - if requesting == Requests.EXCHANGE_TOKEN: - token_exchanged.emit(false) - login_failed.emit(res.error, res.error_description) - auth_request.emit(res.error, res.error_description) - else: - var sig = signup_failed if auth_request_type == Auth_Type.SIGNUP_EP else login_failed - sig.emit(res.error.code, res.error.message) - auth_request.emit(res.error.code, res.error.message) - requesting = Requests.NONE - auth_request_type = Auth_Type.NONE + match res.kind: + RESPONSE_SIGNUP: + auth = get_clean_keys(res) + signup_succeeded.emit(auth) + begin_refresh_countdown() + RESPONSE_SIGNIN, RESPONSE_ASSERTION, RESPONSE_CUSTOM_TOKEN: + auth = get_clean_keys(res) + login_succeeded.emit(auth) + begin_refresh_countdown() + RESPONSE_USERDATA: + var userdata = FirebaseUserData.new(res.users[0]) + userdata_received.emit(userdata) + auth_request.emit(1, auth) + else: + # error message would be INVALID_EMAIL, EMAIL_NOT_FOUND, INVALID_PASSWORD, USER_DISABLED or WEAK_PASSWORD + if requesting == Requests.EXCHANGE_TOKEN: + token_exchanged.emit(false) + login_failed.emit(res.error, res.error_description) + auth_request.emit(res.error, res.error_description) + else: + var sig = signup_failed if auth_request_type == Auth_Type.SIGNUP_EP else login_failed + sig.emit(res.error.code, res.error.message) + auth_request.emit(res.error.code, res.error.message) + + requesting = Requests.NONE + auth_request_type = Auth_Type.NONE # Function used to save the auth data provided by Firebase into an encrypted file @@ -595,14 +596,14 @@ func get_user_data() -> void: # Function used to delete the account of the currently authenticated user func delete_user_account() -> void: - if _is_ready(): - is_busy = true - var err = request(_base_url + _delete_account_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify({"idToken":auth.idtoken})) - if err != OK: - is_busy = false - Firebase._printerr("Error deleting user: %s" % err) - else: - remove_auth() + if _is_ready(): + is_busy = true + var err = request(_base_url + _delete_account_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify({"idToken":auth.idtoken})) + if err != OK: + is_busy = false + Firebase._printerr("Error deleting user: %s" % err) + else: + remove_auth() # Function is called when a new token is issued to a user. The function will yield until the token has expired, and then request a new one. From 9f0586ed04e3040432f9e220e41698b8081b6d6e Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Sun, 2 Jun 2024 14:53:29 -0400 Subject: [PATCH 35/49] Fix last tabs forever hopefully. --- addons/godot-firebase/auth/auth.gd | 40 +++++++++++++++--------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/addons/godot-firebase/auth/auth.gd b/addons/godot-firebase/auth/auth.gd index c40a38a..ec6c24c 100644 --- a/addons/godot-firebase/auth/auth.gd +++ b/addons/godot-firebase/auth/auth.gd @@ -437,25 +437,25 @@ func _on_FirebaseAuth_request_completed(result : int, response_code : int, heade # Refresh token countdown auth_request.emit(1, auth) - if _needs_refresh: - _needs_refresh = false - login_succeeded.emit(auth) - else: - match res.kind: - RESPONSE_SIGNUP: - auth = get_clean_keys(res) - signup_succeeded.emit(auth) - begin_refresh_countdown() - RESPONSE_SIGNIN, RESPONSE_ASSERTION, RESPONSE_CUSTOM_TOKEN: - auth = get_clean_keys(res) + if _needs_refresh: + _needs_refresh = false login_succeeded.emit(auth) - begin_refresh_countdown() - RESPONSE_USERDATA: - var userdata = FirebaseUserData.new(res.users[0]) - userdata_received.emit(userdata) - auth_request.emit(1, auth) + else: + match res.kind: + RESPONSE_SIGNUP: + auth = get_clean_keys(res) + signup_succeeded.emit(auth) + begin_refresh_countdown() + RESPONSE_SIGNIN, RESPONSE_ASSERTION, RESPONSE_CUSTOM_TOKEN: + auth = get_clean_keys(res) + login_succeeded.emit(auth) + begin_refresh_countdown() + RESPONSE_USERDATA: + var userdata = FirebaseUserData.new(res.users[0]) + userdata_received.emit(userdata) + auth_request.emit(1, auth) else: - # error message would be INVALID_EMAIL, EMAIL_NOT_FOUND, INVALID_PASSWORD, USER_DISABLED or WEAK_PASSWORD + # error message would be INVALID_EMAIL, EMAIL_NOT_FOUND, INVALID_PASSWORD, USER_DISABLED or WEAK_PASSWORD if requesting == Requests.EXCHANGE_TOKEN: token_exchanged.emit(false) login_failed.emit(res.error, res.error_description) @@ -464,9 +464,9 @@ func _on_FirebaseAuth_request_completed(result : int, response_code : int, heade var sig = signup_failed if auth_request_type == Auth_Type.SIGNUP_EP else login_failed sig.emit(res.error.code, res.error.message) auth_request.emit(res.error.code, res.error.message) - - requesting = Requests.NONE - auth_request_type = Auth_Type.NONE + requesting = Requests.NONE + auth_request_type = Auth_Type.NONE + # Function used to save the auth data provided by Firebase into an encrypted file From 19bc818e5a3bed4d58294d314fbfca27466ebdbd Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Tue, 4 Jun 2024 08:16:25 -0400 Subject: [PATCH 36/49] Massive Firestore refactor --- addons/godot-firebase/Utilities.gd | 191 ++++++++++++++ addons/godot-firebase/firestore/firestore.gd | 164 +++++------- .../firestore/firestore_collection.gd | 174 +++++++------ .../firestore/firestore_document.gd | 238 ++++++++---------- .../firestore/firestore_listener.gd | 47 ++++ .../firestore/firestore_listener.tscn | 6 + .../firestore/firestore_query.gd | 4 +- .../firestore/firestore_task.gd | 105 ++------ addons/godot-firebase/functions/functions.gd | 3 +- 9 files changed, 527 insertions(+), 405 deletions(-) create mode 100644 addons/godot-firebase/firestore/firestore_listener.gd create mode 100644 addons/godot-firebase/firestore/firestore_listener.tscn diff --git a/addons/godot-firebase/Utilities.gd b/addons/godot-firebase/Utilities.gd index 3000511..ed26a71 100644 --- a/addons/godot-firebase/Utilities.gd +++ b/addons/godot-firebase/Utilities.gd @@ -12,6 +12,189 @@ static func get_json_data(value): return null +# Pass a dictionary { 'key' : 'value' } to format it in a APIs usable .fields +# Field Path3D using the "dot" (`.`) notation are supported: +# ex. { "PATH.TO.SUBKEY" : "VALUE" } ==> { "PATH" : { "TO" : { "SUBKEY" : "VALUE" } } } +static func dict2fields(dict : Dictionary) -> Dictionary: + var fields = {} + var var_type : String = "" + for field in dict.keys(): + var field_value = dict[field] + if field is String and "." in field: + var keys: Array = field.split(".") + field = keys.pop_front() + keys.reverse() + for key in keys: + field_value = { key : field_value } + + match typeof(field_value): + TYPE_NIL: var_type = "nullValue" + TYPE_BOOL: var_type = "booleanValue" + TYPE_INT: var_type = "integerValue" + TYPE_FLOAT: var_type = "doubleValue" + TYPE_STRING: var_type = "stringValue" + TYPE_DICTIONARY: + if is_field_timestamp(field_value): + var_type = "timestampValue" + field_value = dict2timestamp(field_value) + else: + var_type = "mapValue" + field_value = dict2fields(field_value) + TYPE_ARRAY: + var_type = "arrayValue" + field_value = {"values": array2fields(field_value)} + + if fields.has(field) and fields[field].has("mapValue") and field_value.has("fields"): + for key in field_value["fields"].keys(): + fields[field]["mapValue"]["fields"][key] = field_value["fields"][key] + else: + fields[field] = { var_type : field_value } + + return {'fields' : fields} + +static func from_firebase_type(value : Variant) -> Variant: + if value == null: + return null + + if value.has("mapValue"): + value = _from_firebase_type_recursive(value.values()[0].fields) + elif value.has("timestampValue"): + value = Time.get_datetime_dict_from_datetime_string(value.values()[0], false) + else: + value = value.values()[0] + + return value + +static func _from_firebase_type_recursive(value : Variant) -> Variant: + if value == null: + return null + + if value.has("mapValue") or value.has("timestampValue"): + value = _from_firebase_type_recursive(value.value()[0].fields) + else: + value = value.values()[0] + + return value + +static func to_firebase_type(value : Variant) -> Dictionary: + var var_type : String = "" + + match typeof(value): + TYPE_NIL: var_type = "nullValue" + TYPE_BOOL: var_type = "booleanValue" + TYPE_INT: var_type = "integerValue" + TYPE_FLOAT: var_type = "doubleValue" + TYPE_STRING: var_type = "stringValue" + TYPE_DICTIONARY: + if is_field_timestamp(value): + var_type = "timestampValue" + value = dict2timestamp(value) + else: + var_type = "mapValue" + value = dict2fields(value) + TYPE_ARRAY: + var_type = "arrayValue" + value = {"values": array2fields(value)} + + return { var_type : value } + +# Pass the .fields inside a Firestore Document to print out the Dictionary { 'key' : 'value' } +static func fields2dict(doc) -> Dictionary: + var dict = {} + if doc.has("fields"): + var fields = doc["fields"] + print(fields) + for field in fields.keys(): + if fields[field].has("mapValue"): + dict[field] = (fields2dict(fields[field].mapValue)) + elif fields[field].has("timestampValue"): + dict[field] = timestamp2dict(fields[field].timestampValue) + elif fields[field].has("arrayValue"): + dict[field] = fields2array(fields[field].arrayValue) + elif fields[field].has("integerValue"): + dict[field] = fields[field].values()[0] as int + elif fields[field].has("doubleValue"): + dict[field] = fields[field].values()[0] as float + elif fields[field].has("booleanValue"): + dict[field] = fields[field].values()[0] as bool + elif fields[field].has("nullValue"): + dict[field] = null + else: + dict[field] = fields[field].values()[0] + return dict + +# Pass an Array to parse it to a Firebase arrayValue +static func array2fields(array : Array) -> Array: + var fields : Array = [] + var var_type : String = "" + for field in array: + match typeof(field): + TYPE_DICTIONARY: + if is_field_timestamp(field): + var_type = "timestampValue" + field = dict2timestamp(field) + else: + var_type = "mapValue" + field = dict2fields(field) + TYPE_NIL: var_type = "nullValue" + TYPE_BOOL: var_type = "booleanValue" + TYPE_INT: var_type = "integerValue" + TYPE_FLOAT: var_type = "doubleValue" + TYPE_STRING: var_type = "stringValue" + TYPE_ARRAY: var_type = "arrayValue" + _: var_type = "FieldTransform" + fields.append({ var_type : field }) + return fields + +# Pass a Firebase arrayValue Dictionary to convert it back to an Array +static func fields2array(array : Dictionary) -> Array: + var fields : Array = [] + if array.has("values"): + for field in array.values: + var item + match field.keys()[0]: + "mapValue": + item = fields2dict(field.mapValue) + "arrayValue": + item = fields2array(field.arrayValue) + "integerValue": + item = field.values()[0] as int + "doubleValue": + item = field.values()[0] as float + "booleanValue": + item = field.values()[0] as bool + "timestampValue": + item = timestamp2dict(field.timestampValue) + "nullValue": + item = null + _: + item = field.values()[0] + fields.append(item) + return fields + +# Converts a gdscript Dictionary (most likely obtained with Time.get_datetime_dict_from_system()) to a Firebase Timestamp +static func dict2timestamp(dict : Dictionary) -> String: + #dict.erase('weekday') + #dict.erase('dst') + #var dict_values : Array = dict.values() + var time = Time.get_datetime_string_from_datetime_dict(dict, false) + return time + #return "%04d-%02d-%02dT%02d:%02d:%02d.00Z" % dict_values + +# Converts a Firebase Timestamp back to a gdscript Dictionary +static func timestamp2dict(timestamp : String) -> Dictionary: + return Time.get_datetime_dict_from_datetime_string(timestamp, false) + #var datetime : Dictionary = {year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0} + #var dict : PackedStringArray = timestamp.split("T")[0].split("-") + #dict.append_array(timestamp.split("T")[1].split(":")) + #for value in dict.size(): + #datetime[datetime.keys()[value]] = int(dict[value]) + #return datetime + +static func is_field_timestamp(field : Dictionary) -> bool: + return field.has_all(['year','month','day','hour','minute','second']) + + # HTTPRequeust seems to have an issue in Web exports where the body returns empty # This appears to be caused by the gzip compression being unsupported, so we # disable it when web export is detected. @@ -133,3 +316,11 @@ class ObservableDictionary extends RefCounted: func _set(property: StringName, value: Variant) -> bool: update(property, value) return true + +class AwaitDetachable extends Node2D: + var awaiter : Signal + + func _init(freeable_node, await_signal : Signal) -> void: + awaiter = await_signal + add_child(freeable_node) + awaiter.connect(queue_free) diff --git a/addons/godot-firebase/firestore/firestore.gd b/addons/godot-firebase/firestore/firestore.gd index 979006d..772a228 100644 --- a/addons/godot-firebase/firestore/firestore.gd +++ b/addons/godot-firebase/firestore/firestore.gd @@ -18,16 +18,8 @@ extends Node const _API_VERSION : String = "v1" -## Emitted when a [code]list()[/code] request is successfully completed. [code]error()[/code] signal will be emitted otherwise. -## @arg-types Array -signal listed_documents(documents) -## Emitted when a [code]query()[/code] request is successfully completed. [code]error()[/code] signal will be emitted otherwise. -## @arg-types Array -signal result_query(result) -## Emitted when a [code]query()[/code] request is successfully completed. [code]error()[/code] signal will be emitted otherwise. -## @arg-types Array ## Emitted when a [code]list()[/code] or [code]query()[/code] request is [b]not[/b] successfully completed. -signal task_error(code,status,message) +signal error(code, status, message) enum Requests { NONE = -1, ## Firestore is not processing any request. @@ -58,10 +50,6 @@ var persistence_enabled : bool = false ## @default true var networking: bool = true : set = set_networking -## A Dictionary containing all collections currently referenced. -## @type Dictionary -var collections : Dictionary = {} - ## A Dictionary containing all authentication fields for the current logged user. ## @type Dictionary var auth : Dictionary @@ -81,25 +69,11 @@ var _request_list_node : HTTPRequest var _requests_queue : Array = [] var _current_query : FirestoreQuery -var _http_request_pool := [] - var _offline: bool = false : set = _set_offline func _ready() -> void: pass -func _process(delta : float) -> void: - for i in range(_http_request_pool.size() - 1, -1, -1): - var request = _http_request_pool[i] - if not request.get_meta("requesting"): - var lifetime: float = request.get_meta("lifetime") + delta - if lifetime > _MAX_POOLED_REQUEST_AGE: - request.queue_free() - _http_request_pool.remove_at(i) - continue # Just to skip set_meta on a queue_freed request - request.set_meta("lifetime", lifetime) - - ## Returns a reference collection by its [i]path[/i]. ## ## The returned object will be of [code]FirestoreCollection[/code] type. @@ -107,17 +81,19 @@ func _process(delta : float) -> void: ## @args path ## @return FirestoreCollection func collection(path : String) -> FirestoreCollection: - if not collections.has(path): - var coll : FirestoreCollection = FirestoreCollection.new() - coll._extended_url = _extended_url - coll._base_url = _base_url - coll._config = _config - coll.auth = auth - coll.collection_name = path - collections[path] = coll - return coll - else: - return collections[path] + for coll in get_children(): + if coll is FirestoreCollection: + if coll.collection_name == path: + return coll + + var coll : FirestoreCollection = FirestoreCollection.new() + coll._extended_url = _extended_url + coll._base_url = _base_url + coll._config = _config + coll.auth = auth + coll.collection_name = path + add_child(coll) + return coll ## Issue a query checked your Firestore database. @@ -139,19 +115,17 @@ func collection(path : String) -> FirestoreCollection: ## @args query ## @arg-types FirestoreQuery ## @return FirestoreTask -func query(query : FirestoreQuery) -> FirestoreTask: - var firestore_task : FirestoreTask = FirestoreTask.new() - firestore_task.result_query.connect(_on_result_query) # In theory, this and the following could be a CONNECT_ONE_SHOT, but I'm iffy on whether or not that might break any client code, so leaving this for now. - firestore_task.task_error.connect(_on_task_error) - firestore_task.action = FirestoreTask.Task.TASK_QUERY +func query(query : FirestoreQuery) -> Array: + var task : FirestoreTask = FirestoreTask.new() + task.action = FirestoreTask.Task.TASK_QUERY var body : Dictionary = { structuredQuery = query.query } var url : String = _base_url + _extended_url + _query_suffix - firestore_task.data = query - firestore_task._fields = JSON.stringify(body) - firestore_task._url = url - _pooled_request(firestore_task) - return firestore_task + task.data = query + task._fields = JSON.stringify(body) + task._url = url + _pooled_request(task) + return await _handle_task_finished(task) ## Request a list of contents (documents and/or collections) inside a collection, specified by its [i]id[/i]. This method will return a [code]FirestoreTask[/code] object, representing a reference to the request issued. If saved into a variable, the [code]FirestoreTask[/code] object can be used to yield checked the [code]result_query(result)[/code] signal, or the more generic [code]task_finished(result)[/code] signal. @@ -168,11 +142,9 @@ func query(query : FirestoreQuery) -> FirestoreTask: ## @arg-types String, int, String, String ## @arg-defaults , 0, "", "" ## @return FirestoreTask -func list(path : String = "", page_size : int = 0, page_token : String = "", order_by : String = "") -> FirestoreTask: - var firestore_task : FirestoreTask = FirestoreTask.new() - firestore_task.listed_documents.connect(_on_listed_documents) # Same as above with one shot connections - firestore_task.task_error.connect(_on_task_error) - firestore_task.action = FirestoreTask.Task.TASK_LIST +func list(path : String = "", page_size : int = 0, page_token : String = "", order_by : String = "") -> Array: + var task : FirestoreTask = FirestoreTask.new() + task.action = FirestoreTask.Task.TASK_LIST var url : String = _base_url + _extended_url + path if page_size != 0: url+="?pageSize="+str(page_size) @@ -181,10 +153,11 @@ func list(path : String = "", page_size : int = 0, page_token : String = "", ord if order_by != "": url+="&orderBy="+order_by - firestore_task.data = [path, page_size, page_token, order_by] - firestore_task._url = url - _pooled_request(firestore_task) - return firestore_task + task.data = [path, page_size, page_token, order_by] + task._url = url + _pooled_request(task) + + return await _handle_task_finished(task) func set_networking(value: bool) -> void: @@ -199,8 +172,9 @@ func enable_networking() -> void: return networking = true _base_url = _base_url.replace("storeoffline", "firestore") - for key in collections: - collections[key]._base_url = _base_url + for coll in get_children(): + if coll is FirestoreCollection: + coll._base_url = _base_url func disable_networking() -> void: @@ -209,8 +183,9 @@ func disable_networking() -> void: networking = false # Pointing to an invalid url should do the trick. _base_url = _base_url.replace("firestore", "storeoffline") - for key in collections: - collections[key]._base_url = _base_url + for coll in get_children(): + if coll is FirestoreCollection: + coll._base_url = _base_url func _set_offline(value: bool) -> void: @@ -226,7 +201,6 @@ func _set_config(config_json : Dictionary) -> void: _check_emulating() - func _check_emulating() -> void : ## Check emulating if not Firebase.emulating: @@ -254,55 +228,29 @@ func _pooled_request(task : FirestoreTask) -> void: if not Firebase.emulating: task._headers = PackedStringArray([_AUTHORIZATION_HEADER + auth.idtoken]) - var http_request : HTTPRequest - for request in _http_request_pool: - if not request.get_meta("requesting"): - http_request = request - break - - if not http_request: - http_request = HTTPRequest.new() - http_request.timeout = 5 - Utilities.fix_http_request(http_request) - _http_request_pool.append(http_request) - add_child(http_request) - http_request.request_completed.connect(_on_pooled_request_completed.bind(http_request)) - - http_request.set_meta("requesting", true) - http_request.set_meta("lifetime", 0.0) - http_request.set_meta("task", task) + var http_request = HTTPRequest.new() + http_request.timeout = 5 + Utilities.fix_http_request(http_request) + add_child(http_request) + http_request.request_completed.connect( + func(result, response_code, headers, body): + task._on_request_completed(result, response_code, headers, body) + http_request.queue_free() + ) + http_request.request(task._url, task._headers, task._method, task._fields) - -# ------------- - - -func _on_listed_documents(_listed_documents : Array): - listed_documents.emit(_listed_documents) - -func _on_result_query(result : Array): - result_query.emit(result) - -func _on_task_error(code : int, status : String, message : String, task : int): - task_error.emit(code, status, message) - Firebase._printerr(message) - func _on_FirebaseAuth_login_succeeded(auth_result : Dictionary) -> void: auth = auth_result - for key in collections: - collections[key].auth = auth - + for coll in get_children(): + if coll is FirestoreCollection: + coll.auth = auth func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void: auth = auth_result - for key in collections: - collections[key].auth = auth - - -func _on_pooled_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray, request : HTTPRequest) -> void: - request.get_meta("task")._on_request_completed(result, response_code, headers, body) - request.set_meta("requesting", false) - + for coll in get_children(): + if coll is FirestoreCollection: + coll.auth = auth func _on_connect_check_request_completed(result : int, _response_code, _headers, _body) -> void: _set_offline(result != HTTPRequest.RESULT_SUCCESS) @@ -318,3 +266,11 @@ func _check_auth_error(code : int, message : String) -> void: 400: err = "Please enable the Anonymous Sign-in method, or Authenticate the Client before issuing a request" Firebase._printerr(err) Firebase._printerr(message) + +func _handle_task_finished(task : FirestoreTask): + await task.task_finished + + if task.error.keys().size() > 0: + error.emit(task.error) + + return task.data diff --git a/addons/godot-firebase/firestore/firestore_collection.gd b/addons/godot-firebase/firestore/firestore_collection.gd index 56d8bc0..0dae95f 100644 --- a/addons/godot-firebase/firestore/firestore_collection.gd +++ b/addons/godot-firebase/firestore/firestore_collection.gd @@ -5,14 +5,9 @@ ## Documentation TODO. @tool class_name FirestoreCollection -extends RefCounted +extends Node -signal add_document(doc) -signal get_document(doc) -signal update_document(doc) -signal commit_document(result) -signal delete_document(deleted) -signal error(code,status,message) +signal error(error_result) const _AUTHORIZATION_HEADER : String = "Authorization: Bearer " @@ -28,70 +23,104 @@ var _extended_url : String var _config : Dictionary var _documents := {} -var _request_queues := {} # ----------------------- Requests ## @args document_id ## @return FirestoreTask ## used to GET a document from the collection, specify @document_id -func get_doc(document_id : String) -> FirestoreTask: +func get_doc(document_id : String, from_cache : bool = false, is_listener : bool = false) -> FirestoreDocument: + if from_cache: + # for now, just return the child directly; in the future, make it smarter so there's a default, if long, polling time for this + for child in get_children(): + if child.doc_name == document_id: + return child + var task : FirestoreTask = FirestoreTask.new() task.action = FirestoreTask.Task.TASK_GET task.data = collection_name + "/" + document_id var url = _get_request_url() + _separator + document_id.replace(" ", "%20") - task.get_document.connect(_on_get_document) - task.task_finished.connect(_on_task_finished.bind(document_id), CONNECT_DEFERRED) _process_request(task, document_id, url) - return task + var result = await Firebase.Firestore._handle_task_finished(task) + if result != null: + for child in get_children(): + if child.doc_name == document_id: + child.replace(result, true) + result = child + break + else: + print("get_document returned null for %s %s" % [collection_name, document_id]) + + return result ## @args document_id, fields ## @arg-defaults , {} -## @return FirestoreTask -## used to SAVE/ADD a new document to the collection, specify @documentID and @fields -func add(document_id : String, fields : Dictionary = {}) -> FirestoreTask: +## @return FirestoreDocument +## used to ADD a new document to the collection, specify @documentID and @data +func add(document_id : String, data : Dictionary = {}) -> FirestoreDocument: var task : FirestoreTask = FirestoreTask.new() task.action = FirestoreTask.Task.TASK_POST task.data = collection_name + "/" + document_id var url = _get_request_url() + _query_tag + _documentId_tag + document_id - task.add_document.connect(_on_add_document) - task.task_finished.connect(_on_task_finished.bind(document_id), CONNECT_DEFERRED) - _process_request(task, document_id, url, JSON.stringify(FirestoreDocument.dict2fields(fields))) - return task - -## @args document_id, fields -## @arg-defaults , {} -## @return FirestoreTask -# used to UPDATE a document, specify @documentID, @fields -func update(document_id : String, fields : Dictionary = {}) -> FirestoreTask: + _process_request(task, document_id, url, JSON.stringify(Utilities.dict2fields(data))) + var result = await Firebase.Firestore._handle_task_finished(task) + if result != null: + for child in get_children(): + if child.doc_name == document_id: + child.free() # Consider throwing an error for this since it shouldn't already exist + break + + result.collection_name = collection_name + add_child(result, true) + return result + +## @args document +## @return FirestoreDocument +# used to UPDATE a document, specify the document +func update(document : FirestoreDocument) -> FirestoreDocument: var task : FirestoreTask = FirestoreTask.new() task.action = FirestoreTask.Task.TASK_PATCH - task.data = collection_name + "/" + document_id - var url = _get_request_url() + _separator + document_id.replace(" ", "%20") + "?" - for key in fields.keys(): + task.data = collection_name + "/" + document.doc_name + var url = _get_request_url() + _separator + document.doc_name.replace(" ", "%20") + "?" + for key in document.keys(): url+="updateMask.fieldPaths={key}&".format({key = key}) url = url.rstrip("&") - for key in fields.keys(): - if fields[key] == null: - fields.erase(key) + for key in document.keys(): + if document.get_value(key) == null: + document._erase(key) - task.update_document.connect(_on_update_document) - task.task_finished.connect(_on_task_finished.bind(document_id), CONNECT_DEFERRED) - var body = FirestoreDocument.dict2fields(fields) + var temp_transforms + if document._transforms != null: + temp_transforms = document._transforms + document._transforms = null - _process_request(task, document_id, url, JSON.stringify(body)) - return task + var body = JSON.stringify({"fields": document.document}) + + _process_request(task, document.doc_name, url, body) + var result = await Firebase.Firestore._handle_task_finished(task) + if result != null: + for child in get_children(): + if child.doc_name == result.doc_name: + child.replace(result, true) + break -func commit(document : FirestoreDocument) -> FirestoreTask: + if temp_transforms != null: + result._transforms = temp_transforms + + return result + + +## @args document +## @return Dictionary +# Used to commit changes from transforms, specify the document with the transforms +func commit(document : FirestoreDocument) -> Dictionary: var task : FirestoreTask = FirestoreTask.new() task.action = FirestoreTask.Task.TASK_COMMIT - var url = _base_url + _extended_url.rstrip("/") + ":commit" - task.commit_document.connect(_on_commit_document) - task.task_finished.connect(_on_task_finished.bind(document.doc_name), CONNECT_DEFERRED) + var url = get_database_url("commit") document._transforms.set_config( { @@ -101,32 +130,36 @@ func commit(document : FirestoreDocument) -> FirestoreTask: ) # Only place we can set this is here, oofness var body = document._transforms.serialize() + document.clear_field_transforms() _process_request(task, document.doc_name, url, JSON.stringify(body)) - return task + + return await Firebase.Firestore._handle_task_finished(task) # Not implementing the follow-up get here as user may have a listener that's already listening for changes, but user should call get if they don't ## @args document_id ## @return FirestoreTask -# used to DELETE a document, specify @document_id -func delete(document_id : String) -> FirestoreTask: +# used to DELETE a document, specify the document +func delete(document : FirestoreDocument) -> bool: + var doc_name = document.doc_name var task : FirestoreTask = FirestoreTask.new() task.action = FirestoreTask.Task.TASK_DELETE - task.data = collection_name + "/" + document_id - var url = _get_request_url() + _separator + document_id.replace(" ", "%20") - - task.delete_document.connect(_on_delete_document) - task.task_finished.connect(_on_task_finished.bind(document_id), CONNECT_DEFERRED) - _process_request(task, document_id, url) - return task + task.data = document.collection_name + "/" + doc_name + var url = _get_request_url() + _separator + doc_name.replace(" ", "%20") + _process_request(task, doc_name, url) + var result = await Firebase.Firestore._handle_task_finished(task) + + # Clean up the cache + if result: + for node in get_children(): + if node.doc_name == doc_name: + node.free() # Should be only one + break + + return result -# ----------------- Functions func _get_request_url() -> String: return _base_url + _extended_url + collection_name - func _process_request(task : FirestoreTask, document_id : String, url : String, fields := "") -> void: - if not task.task_error.is_connected(_on_error): - task.task_error.connect(_on_error) - if auth == null or auth.is_empty(): Firebase._print("Unauthenticated request issued...") Firebase.Auth.login_anonymous() @@ -139,32 +172,7 @@ func _process_request(task : FirestoreTask, document_id : String, url : String, task._url = url task._fields = fields task._headers = PackedStringArray([_AUTHORIZATION_HEADER + auth.idtoken]) - if _request_queues.has(document_id) and not _request_queues[document_id].is_empty(): - _request_queues[document_id].append(task) - else: - _request_queues[document_id] = [] - Firebase.Firestore._pooled_request(task) - -func _on_task_finished(task : FirestoreTask, document_id : String) -> void: - if not _request_queues[document_id].is_empty(): - task._push_request(task._url, _AUTHORIZATION_HEADER + auth.idtoken, task._fields) - -# -------------------- Higher level of communication with signals -func _on_get_document(document : FirestoreDocument): - get_document.emit(document) - -func _on_add_document(document : FirestoreDocument): - add_document.emit(document) - -func _on_update_document(document : FirestoreDocument): - update_document.emit(document) - -func _on_delete_document(deleted): - delete_document.emit(deleted) - -func _on_error(code, status, message, task): - error.emit(code, status, message) - Firebase._printerr(message) + Firebase.Firestore._pooled_request(task) -func _on_commit_document(result): - commit_document.emit(result) +func get_database_url(append) -> String: + return _base_url + _extended_url.rstrip("/") + ":" + append diff --git a/addons/godot-firebase/firestore/firestore_document.gd b/addons/godot-firebase/firestore/firestore_document.gd index f31520f..f0f9249 100644 --- a/addons/godot-firebase/firestore/firestore_document.gd +++ b/addons/godot-firebase/firestore/firestore_document.gd @@ -4,7 +4,7 @@ ## Documentation TODO. @tool class_name FirestoreDocument -extends RefCounted +extends Node # A FirestoreDocument objects that holds all important values for a Firestore Document, # @doc_name = name of the Firestore Document, which is the request PATH @@ -12,169 +12,133 @@ extends RefCounted # created when requested from a `collection().get()` call var document : Dictionary # the Document itself -var doc_fields : Dictionary # only .fields var doc_name : String # only .name var create_time : String # createTime +var collection_name : String # Name of the collection to which it belongs var _transforms : FieldTransformArray # The transforms to apply +signal changed(changes) -func _init(doc : Dictionary = {},_doc_name : String = "",_doc_fields : Dictionary = {}): +func _init(doc : Dictionary = {}): _transforms = FieldTransformArray.new() - document = doc + document = doc.fields doc_name = doc.name if doc_name.count("/") > 2: doc_name = (doc_name.split("/") as Array).back() - doc_fields = fields2dict(self.document) - self.create_time = doc.createTime -# Pass a dictionary { 'key' : 'value' } to format it in a APIs usable .fields -# Field Path3D using the "dot" (`.`) notation are supported: -# ex. { "PATH.TO.SUBKEY" : "VALUE" } ==> { "PATH" : { "TO" : { "SUBKEY" : "VALUE" } } } -static func dict2fields(dict : Dictionary) -> Dictionary: - var fields = {} - var var_type : String = "" - for field in dict.keys(): - var field_value = dict[field] - if "." in field: - var keys: Array = field.split(".") - field = keys.pop_front() - keys.reverse() - for key in keys: - field_value = { key : field_value } - - match typeof(field_value): - TYPE_NIL: var_type = "nullValue" - TYPE_BOOL: var_type = "booleanValue" - TYPE_INT: var_type = "integerValue" - TYPE_FLOAT: var_type = "doubleValue" - TYPE_STRING: var_type = "stringValue" - TYPE_DICTIONARY: - if is_field_timestamp(field_value): - var_type = "timestampValue" - field_value = dict2timestamp(field_value) - else: - var_type = "mapValue" - field_value = dict2fields(field_value) - TYPE_ARRAY: - var_type = "arrayValue" - field_value = {"values": array2fields(field_value)} - - if fields.has(field) and fields[field].has("mapValue") and field_value.has("fields"): - for key in field_value["fields"].keys(): - fields[field]["mapValue"]["fields"][key] = field_value["fields"][key] +func replace(with : FirestoreDocument, is_listener := false) -> void: + var current = document.duplicate() + document = with.document + + var changes = { + "added": [], "removed": [], "updated": [], "is_listener": is_listener + } + + for key in current.keys(): + if not document.has(key): + changes.removed.push_back({ "key" : key }) else: - fields[field] = { var_type : field_value } - - return {'fields' : fields} + var new_value = Utilities.from_firebase_type(document[key]) + var old_value = Utilities.from_firebase_type(current[key]) + if new_value != old_value: + if old_value == null: + changes.removed.push_back({ "key" : key }) # ?? + else: + changes.updated.push_back({ "key" : key, "old": old_value, "new" : new_value }) + + for key in document.keys(): + if not current.has(key): + changes.added.push_back({ "key" : key, "new" : Utilities.from_firebase_type(document[key]) }) + if not (changes.added.is_empty() and changes.removed.is_empty() and changes.updated.is_empty()): + changed.emit(changes) + +func is_null_value(key) -> bool: + return document.has(key) and Utilities.from_firebase_type(document[key]) == null + +# As of right now, we do not track these with track changes; instead, they'll come back when the document updates from the server. +# Until that time, it's expected if you want to track these types of changes that you commit for the transforms and then get the document yourself. func add_field_transform(transform : FieldTransform) -> void: _transforms.push_back(transform) +func remove_field_transform(transform : FieldTransform) -> void: + _transforms.erase(transform) + +func clear_field_transforms() -> void: + _transforms.transforms.clear() + func remove_field(field_path : String) -> void: if document.has(field_path): - document[field_path] = null + document[field_path] = Utilities.to_firebase_type(null) - if doc_fields.has(field_path): - doc_fields[field_path] = null - -# Pass the .fields inside a Firestore Document to print out the Dictionary { 'key' : 'value' } -static func fields2dict(doc) -> Dictionary: - var dict = {} - if doc.has("fields"): - var fields = doc["fields"] - print(fields) - for field in fields.keys(): - if fields[field].has("mapValue"): - dict[field] = (fields2dict(fields[field].mapValue)) - elif fields[field].has("timestampValue"): - dict[field] = timestamp2dict(fields[field].timestampValue) - elif fields[field].has("arrayValue"): - dict[field] = fields2array(fields[field].arrayValue) - elif fields[field].has("integerValue"): - dict[field] = fields[field].values()[0] as int - elif fields[field].has("doubleValue"): - dict[field] = fields[field].values()[0] as float - elif fields[field].has("booleanValue"): - dict[field] = fields[field].values()[0] as bool - elif fields[field].has("nullValue"): - dict[field] = null - else: - dict[field] = fields[field].values()[0] - return dict + var changes = { + "added": [], "removed": [], "updated": [], "is_listener": false + } + + changes.removed.push_back({ "key" : field_path }) + changed.emit(changes) + +func _erase(field_path : String) -> void: + document.erase(field_path) -# Pass an Array to parse it to a Firebase arrayValue -static func array2fields(array : Array) -> Array: - var fields : Array = [] - var var_type : String = "" - for field in array: - match typeof(field): - TYPE_DICTIONARY: - if is_field_timestamp(field): - var_type = "timestampValue" - field = dict2timestamp(field) - else: - var_type = "mapValue" - field = dict2fields(field) - TYPE_NIL: var_type = "nullValue" - TYPE_BOOL: var_type = "booleanValue" - TYPE_INT: var_type = "integerValue" - TYPE_FLOAT: var_type = "doubleValue" - TYPE_STRING: var_type = "stringValue" - TYPE_ARRAY: var_type = "arrayValue" - _: var_type = "FieldTransform" - fields.append({ var_type : field }) - return fields +func add_or_update_field(field_path : String, value : Variant) -> void: + var changes = { + "added": [], "removed": [], "updated": [], "is_listener": false + } + + var existing_value = get_value(field_path) + var has_field_path = existing_value != null and not is_null_value(field_path) + + var converted_value = Utilities.to_firebase_type(value) + document[field_path] = converted_value + + if has_field_path: + changes.updated.push_back({ "key" : field_path, "old" : existing_value, "new" : value }) + else: + changes.added.push_back({ "key" : field_path, "new" : value }) -# Pass a Firebase arrayValue Dictionary to convert it back to an Array -static func fields2array(array : Dictionary) -> Array: - var fields : Array = [] - if array.has("values"): - for field in array.values: - var item - match field.keys()[0]: - "mapValue": - item = fields2dict(field.mapValue) - "arrayValue": - item = fields2array(field.arrayValue) - "integerValue": - item = field.values()[0] as int - "doubleValue": - item = field.values()[0] as float - "booleanValue": - item = field.values()[0] as bool - "timestampValue": - item = timestamp2dict(field.timestampValue) - "nullValue": - item = null - _: - item = field.values()[0] - fields.append(item) - return fields + changed.emit(changes) + +func on_snapshot(when_called : Callable, poll_time : float = 1.0) -> FirestoreListener.FirestoreListenerConnection: + if get_child_count() >= 1: # Only one listener per + assert(false, "Multiple listeners not allowed for the same document yet") + return + + changed.connect(when_called, CONNECT_REFERENCE_COUNTED) + var listener = preload("res://addons/godot-firebase/firestore/firestore_listener.tscn").instantiate() + add_child(listener) + listener.initialize_listener(collection_name, doc_name, poll_time) + listener.owner = self + var result = listener.enable_connection() + return result -# Converts a gdscript Dictionary (most likely obtained with Time.get_datetime_dict_from_system()) to a Firebase Timestamp -static func dict2timestamp(dict : Dictionary) -> String: - dict.erase('weekday') - dict.erase('dst') - var dict_values : Array = dict.values() - return "%04d-%02d-%02dT%02d:%02d:%02d.00Z" % dict_values +func get_value(property : StringName) -> Variant: + if property == "doc_name": + return doc_name + elif property == "collection_name": + return collection_name + elif property == "create_time": + return create_time + + if document.has(property): + var result = Utilities.from_firebase_type(document[property]) + + return result + + return null -# Converts a Firebase Timestamp back to a gdscript Dictionary -static func timestamp2dict(timestamp : String) -> Dictionary: - var datetime : Dictionary = {year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0} - var dict : PackedStringArray = timestamp.split("T")[0].split("-") - dict.append_array(timestamp.split("T")[1].split(":")) - for value in dict.size() : - datetime[datetime.keys()[value]] = int(dict[value]) - return datetime +func _set(property: StringName, value: Variant) -> bool: + document[property] = Utilities.to_firebase_type(value) + return true -static func is_field_timestamp(field : Dictionary) -> bool: - return field.has_all(['year','month','day','hour','minute','second']) +func keys(): + return document.keys() # Call print(document) to return directly this document formatted func _to_string() -> String: - return ("doc_name: {doc_name}, \ndoc_fields: {doc_fields}, \ncreate_time: {create_time}\n").format( + return ("doc_name: {doc_name}, \ndata: {data}, \ncreate_time: {create_time}\n").format( {doc_name = self.doc_name, - doc_fields = self.doc_fields, + data = document, create_time = self.create_time}) diff --git a/addons/godot-firebase/firestore/firestore_listener.gd b/addons/godot-firebase/firestore/firestore_listener.gd new file mode 100644 index 0000000..7808a5c --- /dev/null +++ b/addons/godot-firebase/firestore/firestore_listener.gd @@ -0,0 +1,47 @@ +class_name FirestoreListener +extends Node + +const MinPollTime = 60 * 2 # seconds, so 2 minutes + +var _doc_name : String +var _poll_time : float +var _collection : FirestoreCollection + +var _total_time = 0.0 +var _enabled := false + +func initialize_listener(collection_name : String, doc_name : String, poll_time : float) -> void: + _poll_time = max(poll_time, MinPollTime) + _doc_name = doc_name + _collection = Firebase.Firestore.collection(collection_name) + +func enable_connection() -> FirestoreListenerConnection: + _enabled = true + set_process(true) + return FirestoreListenerConnection.new(self) + +func _process(delta: float) -> void: + if _enabled: + _total_time += delta + if _total_time >= _poll_time: + _check_for_server_updates() + _total_time = 0.0 + +func _check_for_server_updates() -> void: + var executor = func(): + var doc = await _collection.get_doc(_doc_name, false, true) + if doc == null: + set_process(false) # Document was deleted out from under us, so stop updating + + executor.call() # Hack to work around the await here, otherwise would have to call with await in _process and that's no bueno + +class FirestoreListenerConnection extends RefCounted: + var connection + + func _init(connection_node): + connection = connection_node + + func stop(): + if connection != null and is_instance_valid(connection): + connection.set_process(false) + connection.free() diff --git a/addons/godot-firebase/firestore/firestore_listener.tscn b/addons/godot-firebase/firestore/firestore_listener.tscn new file mode 100644 index 0000000..9f5e246 --- /dev/null +++ b/addons/godot-firebase/firestore/firestore_listener.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://bwv7vtgssc0n5"] + +[ext_resource type="Script" path="res://addons/godot-firebase/firestore/firestore_listener.gd" id="1_qlaei"] + +[node name="FirestoreListener" type="Node"] +script = ExtResource("1_qlaei") diff --git a/addons/godot-firebase/firestore/firestore_query.gd b/addons/godot-firebase/firestore/firestore_query.gd index fc6d034..6e4b764 100644 --- a/addons/godot-firebase/firestore/firestore_query.gd +++ b/addons/godot-firebase/firestore/firestore_query.gd @@ -195,7 +195,7 @@ func limit(limit : int) -> FirestoreQuery: # UTILITIES ---------------------------------------- static func _cursor_object(value, before : bool) -> Cursor: - var parse : Dictionary = FirestoreDocument.dict2fields({value = value}).fields.value + var parse : Dictionary = Utilities.dict2fields({value = value}).fields.value var cursor : Cursor = Cursor.new(parse.arrayValue.values if parse.has("arrayValue") else [parse], before) return cursor @@ -210,7 +210,7 @@ func create_field_filter(field : String, operator : int, value) -> Dictionary: fieldFilter = { field = { fieldPath = field }, op = OPERATOR.keys()[operator], - value = FirestoreDocument.dict2fields({value = value}).fields.value + value = Utilities.dict2fields({value = value}).fields.value } } func create_unary_filter(field : String, operator : int) -> Dictionary: diff --git a/addons/godot-firebase/firestore/firestore_task.gd b/addons/godot-firebase/firestore/firestore_task.gd index 8fd659f..9fad023 100644 --- a/addons/godot-firebase/firestore/firestore_task.gd +++ b/addons/godot-firebase/firestore/firestore_task.gd @@ -23,31 +23,7 @@ extends RefCounted ## Emitted when a request is completed. The request can be successful or not successful: if not, an [code]error[/code] Dictionary will be passed as a result. ## @arg-types Variant -signal task_finished(task) -## Emitted when a [code]add(document)[/code] request checked a [class FirebaseCollection] is successfully completed. [code]error()[/code] signal will be emitted otherwise and [code]null[/code] will be passed as a result.. -## @arg-types FirestoreDocument -signal add_document(doc) -## Emitted when a [code]get(document)[/code] request checked a [class FirebaseCollection] is successfully completed. [code]error()[/code] signal will be emitted otherwise and [code]null[/code] will be passed as a result. -## @arg-types FirestoreDocument -signal get_document(doc) -## Emitted when a [code]update(document)[/code] request checked a [class FirebaseCollection] is successfully completed. [code]error()[/code] signal will be emitted otherwise and [code]null[/code] will be passed as a result. -## @arg-types FirestoreDocument -signal update_document(doc) -## Emitted when a [code]write(document)[/code] request for a document is successfully completed. [code]error()[/code] signal will be emitted otherwise and [code]result[/code] will be passed as a result. -## @arg-types FirestoreDocument -signal commit_document(result) -## Emitted when a [code]delete(document)[/code] request checked a [class FirebaseCollection] is successfully completed and [code]true[/code] will be passed. [code]error()[/code] signal will be emitted otherwise and [code]false[/code] will be passed as a result. -## @arg-types bool -signal delete_document(success) -## Emitted when a [code]list(collection_id)[/code] request checked [class FirebaseFirestore] is successfully completed. [code]error()[/code] signal will be emitted otherwise and [code][][/code] will be passed as a result.. -## @arg-types Array -signal listed_documents(documents) -## Emitted when a [code]query(collection_id)[/code] request checked [class FirebaseFirestore] is successfully completed. [code]error()[/code] signal will be emitted otherwise and [code][][/code] will be passed as a result. -## @arg-types Array -signal result_query(result) -## Emitted when a request is [b]not[/b] successfully completed. -## @arg-types Dictionary -signal task_error(code, status, message, task) +signal task_finished() enum Task { TASK_GET, ## A GET Request Task, processing a get() request @@ -79,8 +55,6 @@ var action : int = -1 : set = set_action var data var error : Dictionary var document : FirestoreDocument -## Whether the data came from cache. -var from_cache : bool = false var _response_headers : PackedStringArray = PackedStringArray() var _response_code : int = 0 @@ -95,31 +69,21 @@ func _on_request_completed(result : int, response_code : int, headers : PackedSt if bod != "": bod = Utilities.get_json_data(bod) - var offline: bool = typeof(bod) == TYPE_NIL var failed: bool = bod is Dictionary and bod.has("error") and response_code != HTTPClient.RESPONSE_OK - from_cache = offline # Probably going to regret this... if response_code == HTTPClient.RESPONSE_OK: - data = bod match action: - Task.TASK_POST: + Task.TASK_POST, Task.TASK_GET, Task.TASK_PATCH: document = FirestoreDocument.new(bod) - add_document.emit(document) - Task.TASK_GET: - document = FirestoreDocument.new(bod) - get_document.emit(document) - Task.TASK_PATCH: - document = FirestoreDocument.new(bod) - update_document.emit(document) + data = document Task.TASK_DELETE: - delete_document.emit(true) + data = true Task.TASK_QUERY: data = [] for doc in bod: if doc.has('document'): data.append(FirestoreDocument.new(doc.document)) - result_query.emit(data) Task.TASK_LIST: data = [] if bod.has('documents'): @@ -127,48 +91,36 @@ func _on_request_completed(result : int, response_code : int, headers : PackedSt data.append(FirestoreDocument.new(doc)) if bod.has("nextPageToken"): data.append(bod.nextPageToken) - listed_documents.emit(data) Task.TASK_COMMIT: - commit_document.emit(bod) + data = bod # Commit's response is not a full document, so don't treat it as such else: var description = "" if TASK_MAP.has(action): description = "(" + TASK_MAP[action] + ")" Firebase._printerr("Action in error was: " + str(action) + " " + description) - emit_error(task_error, bod, action) - match action: - Task.TASK_POST: - add_document.emit(null) - Task.TASK_GET: - get_document.emit(null) - Task.TASK_PATCH: - update_document.emit(null) - Task.TASK_DELETE: - delete_document.emit(false) - Task.TASK_QUERY: - data = [] - result_query.emit(data) - Task.TASK_LIST: - data = [] - listed_documents.emit(data) - Task.TASK_COMMIT: - commit_document.emit(null) - - task_finished.emit(self) - -func emit_error(_signal, bod, task) -> void: - if bod: - if bod is Array and bod.size() > 0 and bod[0].has("error"): - error = bod[0].error - elif bod is Dictionary and bod.keys().size() > 0 and bod.has("error"): - error = bod.error - - _signal.emit(error.code, error.status, error.message, task) - - return - - _signal.emit(1, 0, "Unknown error", task) + build_error(bod, action, description) + + task_finished.emit() + +func build_error(_error, action, description) -> void: + if _error: + if _error is Array and _error.size() > 0 and _error[0].has("error"): + _error = _error[0].error + elif _error is Dictionary and _error.keys().size() > 0 and _error.has("error"): + _error = _error.error + + error = _error + else: + #error.code, error.status, error.message + error = { "error": { + "code": 0, + "status": "Unknown Error", + "message": "Error: %s - %s" % [action, description] + } + } + + data = null func set_action(value : int) -> void: action = value @@ -185,9 +137,6 @@ func set_action(value : int) -> void: _method = HTTPClient.METHOD_POST -func _handle_cache(offline : bool, data, encrypt_key : String, cache_path : String, body) -> Dictionary: - return body # Removing caching for now, hopefully this works without killing everyone and everything - func _merge_dict(dic_a : Dictionary, dic_b : Dictionary, nullify := false) -> Dictionary: var ret := dic_a.duplicate(true) for key in dic_b: diff --git a/addons/godot-firebase/functions/functions.gd b/addons/godot-firebase/functions/functions.gd index 605a180..8ccfa97 100644 --- a/addons/godot-firebase/functions/functions.gd +++ b/addons/godot-firebase/functions/functions.gd @@ -51,7 +51,7 @@ var _http_request_pool : Array = [] var _offline: bool = false : set = _set_offline func _ready() -> void: - pass + set_process(false) func _process(delta : float) -> void: for i in range(_http_request_pool.size() - 1, -1, -1): @@ -68,6 +68,7 @@ func _process(delta : float) -> void: ## @args ## @return FunctionTask func execute(function: String, method: int, params: Dictionary = {}, body: Dictionary = {}) -> FunctionTask: + set_process(true) var function_task : FunctionTask = FunctionTask.new() function_task.task_error.connect(_on_task_error) function_task.task_finished.connect(_on_task_finished) From b307f50d2ac324efd77acdd9f160fcc22517add0 Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Mon, 10 Jun 2024 06:26:26 -0400 Subject: [PATCH 37/49] Update storage to make StorageTask fully transparent --- addons/godot-firebase/storage/storage.gd | 71 +++++++------- .../storage/storage_reference.gd | 92 +++++++------------ addons/godot-firebase/storage/storage_task.gd | 47 +++++----- 3 files changed, 92 insertions(+), 118 deletions(-) diff --git a/addons/godot-firebase/storage/storage.gd b/addons/godot-firebase/storage/storage.gd index 82a8017..28d016e 100644 --- a/addons/godot-firebase/storage/storage.gd +++ b/addons/godot-firebase/storage/storage.gd @@ -10,11 +10,6 @@ extends Node const _API_VERSION : String = "v0" -## @arg-types int, int, PackedStringArray -## @arg-enums HTTPRequest.Result, HTTPClient.ResponseCode -## Emitted when a [StorageTask] has finished successful. -signal task_successful(result, response_code, data) - ## @arg-types int, int, PackedStringArray ## @arg-enums HTTPRequest.Result, HTTPClient.ResponseCode ## Emitted when a [StorageTask] has finished with an error. @@ -83,6 +78,7 @@ func _internal_process(_delta : float) -> void: for header in _response_headers: if "Content-Length" in header: _content_length = header.trim_prefix("Content-Length: ").to_int() + break _http_client.poll() var chunk = _http_client.read_response_body_chunk() # Get a chunk. @@ -127,13 +123,13 @@ func ref(path := "") -> StorageReference: if not _references.has(path): var ref := StorageReference.new() _references[path] = ref - ref.valid = true ref.bucket = bucket ref.full_path = path - ref.name = path.get_file() + ref.file_name = path.get_file() ref.parent = ref(path.path_join("..")) ref.root = _root_ref ref.storage = self + add_child(ref) return ref else: return _references[path] @@ -158,9 +154,9 @@ func _check_emulating() -> void : _base_url = "http://localhost:{port}/{version}/".format({ version = _API_VERSION, port = port }) -func _upload(data : PackedByteArray, headers : PackedStringArray, ref : StorageReference, meta_only : bool) -> StorageTask: +func _upload(data : PackedByteArray, headers : PackedStringArray, ref : StorageReference, meta_only : bool) -> Variant: if _is_invalid_authentication(): - return null + return 0 var task := StorageTask.new() task.ref = ref @@ -169,11 +165,11 @@ func _upload(data : PackedByteArray, headers : PackedStringArray, ref : StorageR task._headers = headers task.data = data _process_request(task) - return task + return await task.task_finished -func _download(ref : StorageReference, meta_only : bool, url_only : bool) -> StorageTask: +func _download(ref : StorageReference, meta_only : bool, url_only : bool) -> Variant: if _is_invalid_authentication(): - return null + return 0 var info_task := StorageTask.new() info_task.ref = ref @@ -182,7 +178,7 @@ func _download(ref : StorageReference, meta_only : bool, url_only : bool) -> Sto _process_request(info_task) if url_only or meta_only: - return info_task + return await info_task.task_finished var task := StorageTask.new() task.ref = ref @@ -199,33 +195,36 @@ func _download(ref : StorageReference, meta_only : bool, url_only : bool) -> Sto task.response_code = info_task.response_code task.result = info_task.result task.finished = true - task.task_finished.emit() + task.task_finished.emit(null) task_failed.emit(task.result, task.response_code, task.data) _pending_tasks.erase(task) + return null - return task + return await task.task_finished -func _list(ref : StorageReference, list_all : bool) -> StorageTask: +func _list(ref : StorageReference, list_all : bool) -> Array: if _is_invalid_authentication(): - return null + return [] var task := StorageTask.new() task.ref = ref task._url = _get_file_url(_root_ref).trim_suffix("/") task.action = StorageTask.Task.TASK_LIST_ALL if list_all else StorageTask.Task.TASK_LIST _process_request(task) - return task + return await task.task_finished -func _delete(ref : StorageReference) -> StorageTask: +func _delete(ref : StorageReference) -> bool: if _is_invalid_authentication(): - return null + return false var task := StorageTask.new() task.ref = ref task._url = _get_file_url(ref) task.action = StorageTask.Task.TASK_DELETE _process_request(task) - return task + var data = await task.task_finished + + return data == null func _process_request(task : StorageTask) -> void: if requesting: @@ -262,7 +261,10 @@ func _finish_request(result : int) -> void: StorageTask.Task.TASK_DELETE: _references.erase(task.ref.full_path) - task.ref.valid = false + for child in get_children(): + if child.full_path == task.ref.full_path: + child.queue_free() + break if typeof(task.data) == TYPE_PACKED_BYTE_ARRAY: task.data = null @@ -301,26 +303,21 @@ func _finish_request(result : int) -> void: var json = Utilities.get_json_data(_response_data) task.data = json - var next_task : StorageTask - if not _pending_tasks.is_empty(): - next_task = _pending_tasks.pop_front() - + var next_task = _get_next_pending_task() + task.finished = true task.task_finished.emit(task.data) # I believe this parameter has been missing all along, but not sure. Caused weird results at times with a yield/await returning null, but the task containing data. if typeof(task.data) == TYPE_DICTIONARY and task.data.has("error"): task_failed.emit(task.result, task.response_code, task.data) - else: - task_successful.emit(task.result, task.response_code, task.data) - - while true: - if next_task and not next_task.finished: - _process_request(next_task) - break - elif not _pending_tasks.is_empty(): - next_task = _pending_tasks.pop_front() - else: - break + if next_task and not next_task.finished: + _process_request(next_task) + +func _get_next_pending_task() -> StorageTask: + if _pending_tasks.is_empty(): + return null + + return _pending_tasks.pop_front() func _get_file_url(ref : StorageReference) -> String: var url := _extended_url.replace("[APP_ID]", ref.bucket) diff --git a/addons/godot-firebase/storage/storage_reference.gd b/addons/godot-firebase/storage/storage_reference.gd index 2ce2446..2989d12 100644 --- a/addons/godot-firebase/storage/storage_reference.gd +++ b/addons/godot-firebase/storage/storage_reference.gd @@ -4,7 +4,7 @@ ## This object is used to interact with the cloud storage. You may get data from the server, as well as upload your own back to it. @tool class_name StorageReference -extends RefCounted +extends Node ## The default MIME type to use when uploading a file. ## Data sent with this type are interpreted as plain binary data. Note that firebase will generate an MIME type based checked the file extenstion if none is provided. @@ -36,7 +36,7 @@ const MIME_TYPES = { "txt": "text/plain", "wav": "audio/wav", "webm": "video/webm", - "webp": "video/webm", + "webp": "image/webp", "xml": "text/xml", } @@ -51,7 +51,7 @@ var full_path : String = "" ## @default "" ## The name of the file/folder, including any file extension. ## Example: If the [member full_path] is [code]images/user/image.png[/code], then the [member name] would be [code]image.png[/code]. -var name : String = "" +var file_name : String = "" ## The parent [StorageReference] one level up the file hierarchy. ## If the current [StorageReference] is the root (i.e. the [member full_path] is [code]""[/code]) then the [member parent] will be [code]null[/code]. @@ -64,25 +64,16 @@ var root : StorageReference ## The Storage API that created this [StorageReference] to begin with. var storage # FirebaseStorage (Can't static type due to cyclic reference) -## @default false -## Whether this [StorageReference] is valid. None of the functions will work when in an invalid state. -## It is set to false when [method delete] is called. -var valid : bool = false - ## @args path ## @return StorageReference ## Returns a reference to another [StorageReference] relative to this one. func child(path : String) -> StorageReference: - if not valid: - return null return storage.ref(full_path.path_join(path)) ## @args data, metadata -## @return StorageTask -## Makes an attempt to upload data to the referenced file location. Status checked this task is found in the returned [StorageTask]. -func put_data(data : PackedByteArray, metadata := {}) -> StorageTask: - if not valid: - return null +## @return int +## Makes an attempt to upload data to the referenced file location. Returns Variant +func put_data(data : PackedByteArray, metadata := {}) -> Variant: if not "Content-Length" in metadata and not Utilities.is_web(): metadata["Content-Length"] = data.size() @@ -90,90 +81,75 @@ func put_data(data : PackedByteArray, metadata := {}) -> StorageTask: for key in metadata: headers.append("%s: %s" % [key, metadata[key]]) - return storage._upload(data, headers, self, false) + return await storage._upload(data, headers, self, false) + ## @args data, metadata -## @return StorageTask +## @return int ## Like [method put_data], but [code]data[/code] is a [String]. -func put_string(data : String, metadata := {}) -> StorageTask: - return put_data(data.to_utf8_buffer(), metadata) +func put_string(data : String, metadata := {}) -> Variant: + return await put_data(data.to_utf8_buffer(), metadata) ## @args file_path, metadata -## @return StorageTask +## @return int ## Like [method put_data], but the data comes from a file at [code]file_path[/code]. -func put_file(file_path : String, metadata := {}) -> StorageTask: +func put_file(file_path : String, metadata := {}) -> Variant: var file := FileAccess.open(file_path, FileAccess.READ) var data := file.get_buffer(file.get_length()) if "Content-Type" in metadata: metadata["Content-Type"] = MIME_TYPES.get(file_path.get_extension(), DEFAULT_MIME_TYPE) - return put_data(data, metadata) + return await put_data(data, metadata) -## @return StorageTask +## @return Variant ## Makes an attempt to download the files from the referenced file location. Status checked this task is found in the returned [StorageTask]. -func get_data() -> StorageTask: - if not valid: - return null - storage._download(self, false, false) - return storage._pending_tasks[-1] +func get_data() -> Variant: + var result = await storage._download(self, false, false) + return result ## @return StorageTask ## Like [method get_data], but the data in the returned [StorageTask] comes in the form of a [String]. -func get_string() -> StorageTask: - var task := get_data() - task.task_finished.connect(_on_task_finished.bind(task, "stringify")) - return task +func get_string() -> String: + var task := await get_data() + _on_task_finished(task, "stringify") + return task.data ## @return StorageTask ## Attempts to get the download url that points to the referenced file's data. Using the url directly may require an authentication header. Status checked this task is found in the returned [StorageTask]. -func get_download_url() -> StorageTask: - if not valid: - return null - return storage._download(self, false, true) +func get_download_url() -> Variant: + return await storage._download(self, false, true) ## @return StorageTask ## Attempts to get the metadata of the referenced file. Status checked this task is found in the returned [StorageTask]. -func get_metadata() -> StorageTask: - if not valid: - return null - return storage._download(self, true, false) +func get_metadata() -> Variant: + return await storage._download(self, true, false) ## @args metadata ## @return StorageTask ## Attempts to update the metadata of the referenced file. Any field with a value of [code]null[/code] will be deleted checked the server end. Status checked this task is found in the returned [StorageTask]. -func update_metadata(metadata : Dictionary) -> StorageTask: - if not valid: - return null +func update_metadata(metadata : Dictionary) -> Variant: var data := JSON.stringify(metadata).to_utf8_buffer() var headers := PackedStringArray(["Accept: application/json"]) - return storage._upload(data, headers, self, true) + return await storage._upload(data, headers, self, true) ## @return StorageTask ## Attempts to get the list of files and/or folders under the referenced folder This function is not nested unlike [method list_all]. Status checked this task is found in the returned [StorageTask]. -func list() -> StorageTask: - if not valid: - return null - return storage._list(self, false) +func list() -> Array: + return await storage._list(self, false) ## @return StorageTask ## Attempts to get the list of files and/or folders under the referenced folder This function is nested unlike [method list]. Status checked this task is found in the returned [StorageTask]. -func list_all() -> StorageTask: - if not valid: - return null - return storage._list(self, true) +func list_all() -> Array: + return await storage._list(self, true) ## @return StorageTask ## Attempts to delete the referenced file/folder. If successful, the reference will become invalid And can no longer be used. If you need to reference this location again, make a new reference with [method StorageTask.ref]. Status checked this task is found in the returned [StorageTask]. -func delete() -> StorageTask: - if not valid: - return null - return storage._delete(self) +func delete() -> bool: + return await storage._delete(self) func _to_string() -> String: var string := "gs://%s/%s" % [bucket, full_path] - if not valid: - string += " [Invalid RefCounted]" return string func _on_task_finished(task : StorageTask, action : String) -> void: diff --git a/addons/godot-firebase/storage/storage_task.gd b/addons/godot-firebase/storage/storage_task.gd index ad810c5..fa5cefd 100644 --- a/addons/godot-firebase/storage/storage_task.gd +++ b/addons/godot-firebase/storage/storage_task.gd @@ -1,4 +1,4 @@ -## @meta-authors SIsilicon +## @meta-authors SIsilicon, Kyle 'backat50ft' Szklenski ## @meta-version 2.2 ## An object that keeps track of an operation performed by [StorageReference]. @tool @@ -6,23 +6,22 @@ class_name StorageTask extends RefCounted enum Task { - TASK_UPLOAD, - TASK_UPLOAD_META, - TASK_DOWNLOAD, - TASK_DOWNLOAD_META, - TASK_DOWNLOAD_URL, - TASK_LIST, - TASK_LIST_ALL, - TASK_DELETE, - TASK_MAX ## The number of [enum Task] constants. + TASK_UPLOAD, + TASK_UPLOAD_META, + TASK_DOWNLOAD, + TASK_DOWNLOAD_META, + TASK_DOWNLOAD_URL, + TASK_LIST, + TASK_LIST_ALL, + TASK_DELETE, + TASK_MAX ## The number of [enum Task] constants. } ## Emitted when the task is finished. Returns data depending checked the success and action of the task. signal task_finished(data) -## @type StorageReference -## The [StorageReference] that created this [StorageTask]. -var ref # Storage RefCounted (Can't static type due to cyclic reference) +## Boolean to determine if this request involves metadata only +var is_meta : bool ## @enum Task ## @default -1 @@ -30,6 +29,8 @@ var ref # Storage RefCounted (Can't static type due to cyclic reference) ## The kind of operation this [StorageTask] is keeping track of. var action : int = -1 : set = set_action +var ref # Should not be needed, damnit + ## @default PackedByteArray() ## Data that the tracked task will/has returned. var data = PackedByteArray() # data can be of any type. @@ -61,13 +62,13 @@ var _url : String = "" var _headers : PackedStringArray = PackedStringArray() func set_action(value : int) -> void: - action = value - match action: - Task.TASK_UPLOAD: - _method = HTTPClient.METHOD_POST - Task.TASK_UPLOAD_META: - _method = HTTPClient.METHOD_PATCH - Task.TASK_DELETE: - _method = HTTPClient.METHOD_DELETE - _: - _method = HTTPClient.METHOD_GET + action = value + match action: + Task.TASK_UPLOAD: + _method = HTTPClient.METHOD_POST + Task.TASK_UPLOAD_META: + _method = HTTPClient.METHOD_PATCH + Task.TASK_DELETE: + _method = HTTPClient.METHOD_DELETE + _: + _method = HTTPClient.METHOD_GET From 24a576f9dfd2b8d11f1e74dbd0ecd853180ee7bb Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Mon, 10 Jun 2024 06:31:23 -0400 Subject: [PATCH 38/49] Re-add fields2dict --- addons/godot-firebase/Utilities.gd | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/addons/godot-firebase/Utilities.gd b/addons/godot-firebase/Utilities.gd index ed26a71..e828d2a 100644 --- a/addons/godot-firebase/Utilities.gd +++ b/addons/godot-firebase/Utilities.gd @@ -57,24 +57,13 @@ static func from_firebase_type(value : Variant) -> Variant: return null if value.has("mapValue"): - value = _from_firebase_type_recursive(value.values()[0].fields) + value = fields2dict(value.values()[0]) elif value.has("timestampValue"): value = Time.get_datetime_dict_from_datetime_string(value.values()[0], false) else: value = value.values()[0] return value - -static func _from_firebase_type_recursive(value : Variant) -> Variant: - if value == null: - return null - - if value.has("mapValue") or value.has("timestampValue"): - value = _from_firebase_type_recursive(value.value()[0].fields) - else: - value = value.values()[0] - - return value static func to_firebase_type(value : Variant) -> Dictionary: var var_type : String = "" @@ -103,7 +92,7 @@ static func fields2dict(doc) -> Dictionary: var dict = {} if doc.has("fields"): var fields = doc["fields"] - print(fields) + for field in fields.keys(): if fields[field].has("mapValue"): dict[field] = (fields2dict(fields[field].mapValue)) From c27bf9e8b6418c95b02419a0e97815bf476a7b5c Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Mon, 10 Jun 2024 06:32:55 -0400 Subject: [PATCH 39/49] Update plugin version finally --- addons/godot-firebase/plugin.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/godot-firebase/plugin.cfg b/addons/godot-firebase/plugin.cfg index b8ffaac..9740be9 100644 --- a/addons/godot-firebase/plugin.cfg +++ b/addons/godot-firebase/plugin.cfg @@ -3,5 +3,5 @@ name="GodotFirebase" description="Google Firebase SDK written in GDScript for use in Godot Engine 4.0 projects." author="GodotNutsOrg" -version="1.1" +version="2.0" script="plugin.gd" From 04916a0b1ec6b62eef6eff6ea34a1220c44df1ee Mon Sep 17 00:00:00 2001 From: auntie Birdie Date: Wed, 19 Jun 2024 13:37:49 -0400 Subject: [PATCH 40/49] fix: add arrayValue conversion to from_firebase_type --- addons/godot-firebase/Utilities.gd | 2 ++ 1 file changed, 2 insertions(+) diff --git a/addons/godot-firebase/Utilities.gd b/addons/godot-firebase/Utilities.gd index e828d2a..efae477 100644 --- a/addons/godot-firebase/Utilities.gd +++ b/addons/godot-firebase/Utilities.gd @@ -58,6 +58,8 @@ static func from_firebase_type(value : Variant) -> Variant: if value.has("mapValue"): value = fields2dict(value.values()[0]) + elif value.has("arrayValue"): + value = fields2array(value.values()[0]) elif value.has("timestampValue"): value = Time.get_datetime_dict_from_datetime_string(value.values()[0], false) else: From fa306a52e2c84980912955dd00b91abe36027723 Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Thu, 27 Jun 2024 06:44:24 -0400 Subject: [PATCH 41/49] Update so fields have correct types --- addons/godot-firebase/Utilities.gd | 37 ++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/addons/godot-firebase/Utilities.gd b/addons/godot-firebase/Utilities.gd index efae477..d9ae3b5 100644 --- a/addons/godot-firebase/Utilities.gd +++ b/addons/godot-firebase/Utilities.gd @@ -52,10 +52,37 @@ static func dict2fields(dict : Dictionary) -> Dictionary: return {'fields' : fields} -static func from_firebase_type(value : Variant) -> Variant: + +class FirebaseTypeConverter extends RefCounted: + var converters = { + "nullValue": _to_null, + "booleanValue": _to_bool, + "integerValue": _to_int, + "doubleValue": _to_float + } + + func convert_value(type, value): + if converters.has(type): + return converters[type].call(value) + + return value + + func _to_null(value): + return null + + func _to_bool(value): + return bool(value) + + func _to_int(value): + return int(value) + + func _to_float(value): + return float(value) + +static func from_firebase_type(value): if value == null: return null - + if value.has("mapValue"): value = fields2dict(value.values()[0]) elif value.has("arrayValue"): @@ -63,10 +90,12 @@ static func from_firebase_type(value : Variant) -> Variant: elif value.has("timestampValue"): value = Time.get_datetime_dict_from_datetime_string(value.values()[0], false) else: - value = value.values()[0] - + var converter = FirebaseTypeConverter.new() + value = converter.convert_value(value.keys()[0], value.values()[0]) + return value + static func to_firebase_type(value : Variant) -> Dictionary: var var_type : String = "" From ea1276ee90e04a528962ddd916fd629fc18dd6e5 Mon Sep 17 00:00:00 2001 From: Sylvain Kamdem Date: Thu, 18 Jul 2024 16:00:52 +0000 Subject: [PATCH 42/49] fix: check that request result has fields property --- addons/godot-firebase/firestore/firestore_document.gd | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/addons/godot-firebase/firestore/firestore_document.gd b/addons/godot-firebase/firestore/firestore_document.gd index f0f9249..fd9f31c 100644 --- a/addons/godot-firebase/firestore/firestore_document.gd +++ b/addons/godot-firebase/firestore/firestore_document.gd @@ -21,7 +21,8 @@ signal changed(changes) func _init(doc : Dictionary = {}): _transforms = FieldTransformArray.new() - document = doc.fields + if doc.has("fields"): + document = doc.fields doc_name = doc.name if doc_name.count("/") > 2: doc_name = (doc_name.split("/") as Array).back() From 024ef5079d6e87ec1be347ee0c7137629c446b37 Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Thu, 18 Jul 2024 12:56:23 -0400 Subject: [PATCH 43/49] Fix null/empty doc issue --- addons/godot-firebase/firestore/firestore_document.gd | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/addons/godot-firebase/firestore/firestore_document.gd b/addons/godot-firebase/firestore/firestore_document.gd index fd9f31c..2475276 100644 --- a/addons/godot-firebase/firestore/firestore_document.gd +++ b/addons/godot-firebase/firestore/firestore_document.gd @@ -23,9 +23,10 @@ func _init(doc : Dictionary = {}): if doc.has("fields"): document = doc.fields - doc_name = doc.name - if doc_name.count("/") > 2: - doc_name = (doc_name.split("/") as Array).back() + if doc.has("name"): + doc_name = doc.name + if doc_name.count("/") > 2: + doc_name = (doc_name.split("/") as Array).back() self.create_time = doc.createTime From 6d32fbe5c8a24471eb32b81da910421b1b371e94 Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Fri, 19 Jul 2024 07:43:59 -0400 Subject: [PATCH 44/49] Add ability to do aggregation queries --- addons/godot-firebase/firestore/firestore.gd | 137 +++++++----------- .../firestore/firestore_query.gd | 39 +++-- .../firestore/firestore_task.gd | 41 ++++-- 3 files changed, 108 insertions(+), 109 deletions(-) diff --git a/addons/godot-firebase/firestore/firestore.gd b/addons/godot-firebase/firestore/firestore.gd index 772a228..839990d 100644 --- a/addons/godot-firebase/firestore/firestore.gd +++ b/addons/godot-firebase/firestore/firestore.gd @@ -40,39 +40,27 @@ const _MAX_POOLED_REQUEST_AGE = 30 ## The code indicating the request Firestore is processing. ## See @[enum FirebaseFirestore.Requests] to get a full list of codes identifiers. ## @enum Requests -var request : int = -1 - -## Whether cache files can be used and generated. -## @default true -var persistence_enabled : bool = false - -## Whether an internet connection can be used. -## @default true -var networking: bool = true : set = set_networking +var request: int = -1 ## A Dictionary containing all authentication fields for the current logged user. ## @type Dictionary -var auth : Dictionary +var auth: Dictionary -var _config : Dictionary = {} +var _config: Dictionary = {} var _cache_loc: String var _encrypt_key := "5vg76n90345f7w390346" if Utilities.is_web() else OS.get_unique_id() -var _base_url : String = "" -var _extended_url : String = "projects/[PROJECT_ID]/databases/(default)/documents/" -var _query_suffix : String = ":runQuery" +var _base_url: String = "" +var _extended_url: String = "projects/[PROJECT_ID]/databases/(default)/documents/" +var _query_suffix: String = ":runQuery" +var _agg_query_suffix: String = ":runAggregationQuery" #var _connect_check_node : HTTPRequest -var _request_list_node : HTTPRequest -var _requests_queue : Array = [] -var _current_query : FirestoreQuery - -var _offline: bool = false : set = _set_offline - -func _ready() -> void: - pass +var _request_list_node: HTTPRequest +var _requests_queue: Array = [] +var _current_query: FirestoreQuery ## Returns a reference collection by its [i]path[/i]. ## @@ -99,49 +87,69 @@ func collection(path : String) -> FirestoreCollection: ## Issue a query checked your Firestore database. ## ## [b]Note:[/b] a [code]FirestoreQuery[/code] object needs to be created to issue the query. -## This method will return a [code]FirestoreTask[/code] object, representing a reference to the request issued. -## If saved into a variable, the [code]FirestoreTask[/code] object can be used to yield checked the [code]result_query(result)[/code] signal, or the more generic [code]task_finished(result)[/code] signal. -## -## ex. -## [code]var query_task : FirestoreTask = Firebase.Firestore.query(FirestoreQuery.new())[/code] -## [code]await query_task.task_finished[/code] -## Since the emitted signal is holding an argument, it can be directly retrieved as a return variable from the [code]yield()[/code] function. +## When awaited, this function returns the resulting array from the query. ## ## ex. -## [code]var result : Array = await query_task.task_finished[/code] +## [code]var query_results = wait Firebase.Firestore.query(FirestoreQuery.new())[/code] ## ## [b]Warning:[/b] It currently does not work offline! ## ## @args query ## @arg-types FirestoreQuery -## @return FirestoreTask +## @return Array[FirestoreDocument] func query(query : FirestoreQuery) -> Array: + if query.aggregations.size() > 0: + Firebase._printerr("Aggregation query sent with normal query call: " + str(query)) + return [] + var task : FirestoreTask = FirestoreTask.new() task.action = FirestoreTask.Task.TASK_QUERY - var body : Dictionary = { structuredQuery = query.query } - var url : String = _base_url + _extended_url + _query_suffix - + var body: Dictionary = { structuredQuery = query.query } + var url: String = _base_url + _extended_url + _query_suffix + task.data = query task._fields = JSON.stringify(body) task._url = url _pooled_request(task) return await _handle_task_finished(task) - - -## Request a list of contents (documents and/or collections) inside a collection, specified by its [i]id[/i]. This method will return a [code]FirestoreTask[/code] object, representing a reference to the request issued. If saved into a variable, the [code]FirestoreTask[/code] object can be used to yield checked the [code]result_query(result)[/code] signal, or the more generic [code]task_finished(result)[/code] signal. -## [b]Note:[/b] [code]order_by[/code] does not work in offline mode. -## ex. -## [code]var query_task : FirestoreTask = Firebase.Firestore.query(FirestoreQuery.new())[/code] -## [code]await query_task.task_finished[/code] -## Since the emitted signal is holding an argument, it can be directly retrieved as a return variable from the [code]yield()[/code] function. + +## Issue an aggregation query (sum, average, count) against your Firestore database; +## cheaper than a normal query and counting (for instance) values directly. +## +## [b]Note:[/b] a [code]FirestoreQuery[/code] object needs to be created to issue the query. +## When awaited, this function returns the result from the aggregation query. ## ## ex. -## [code]var result : Array = await query_task.task_finished[/code] +## [code]var query_results = await Firebase.Firestore.query(FirestoreQuery.new())[/code] +## +## [b]Warning:[/b] It currently does not work offline! ## +## @args query +## @arg-types FirestoreQuery +## @return Variant representing the array results of the aggregation query +func aggregation_query(query : FirestoreQuery) -> Variant: + if query.aggregations.size() == 0: + Firebase._printerr("Aggregation query sent with no aggregation values: " + str(query)) + return 0 + + var task : FirestoreTask = FirestoreTask.new() + task.action = FirestoreTask.Task.TASK_AGG_QUERY + + var body: Dictionary = { structuredAggregationQuery = { structuredQuery = query.query, aggregations = query.aggregations } } + var url: String = _base_url + _extended_url + _agg_query_suffix + + task.data = query + task._fields = JSON.stringify(body) + task._url = url + _pooled_request(task) + var result = await _handle_task_finished(task) + return result + +## Request a list of contents (documents and/or collections) inside a collection, specified by its [i]id[/i]. This method will return an Array[FirestoreDocument] ## @args collection_id, page_size, page_token, order_by ## @arg-types String, int, String, String ## @arg-defaults , 0, "", "" -## @return FirestoreTask +## @return Array[FirestoreDocument] func list(path : String = "", page_size : int = 0, page_token : String = "", order_by : String = "") -> Array: var task : FirestoreTask = FirestoreTask.new() task.action = FirestoreTask.Task.TASK_LIST @@ -160,38 +168,6 @@ func list(path : String = "", page_size : int = 0, page_token : String = "", ord return await _handle_task_finished(task) -func set_networking(value: bool) -> void: - if value: - enable_networking() - else: - disable_networking() - - -func enable_networking() -> void: - if networking: - return - networking = true - _base_url = _base_url.replace("storeoffline", "firestore") - for coll in get_children(): - if coll is FirestoreCollection: - coll._base_url = _base_url - - -func disable_networking() -> void: - if not networking: - return - networking = false - # Pointing to an invalid url should do the trick. - _base_url = _base_url.replace("firestore", "storeoffline") - for coll in get_children(): - if coll is FirestoreCollection: - coll._base_url = _base_url - - -func _set_offline(value: bool) -> void: - return # Since caching is causing a lot of issues, I'm turning it off for now. We will revisit this in the future, once we have some time to investigate why the cache is being corrupted. - - func _set_config(config_json : Dictionary) -> void: _config = config_json _cache_loc = _config["cacheLocation"] @@ -213,10 +189,6 @@ func _check_emulating() -> void : _base_url = "http://localhost:{port}/{version}/".format({ version = _API_VERSION, port = port }) func _pooled_request(task : FirestoreTask) -> void: - if _offline: - task._on_request_completed(HTTPRequest.RESULT_CANT_CONNECT, 404, PackedStringArray(), PackedByteArray()) - return - if (auth == null or auth.is_empty()) and not Firebase.emulating: Firebase._print("Unauthenticated request issued...") Firebase.Auth.login_anonymous() @@ -252,11 +224,6 @@ func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void: if coll is FirestoreCollection: coll.auth = auth -func _on_connect_check_request_completed(result : int, _response_code, _headers, _body) -> void: - _set_offline(result != HTTPRequest.RESULT_SUCCESS) - #_connect_check_node.request(_base_url) - - func _on_FirebaseAuth_logout() -> void: auth = {} diff --git a/addons/godot-firebase/firestore/firestore_query.gd b/addons/godot-firebase/firestore/firestore_query.gd index 6e4b764..bdebbe6 100644 --- a/addons/godot-firebase/firestore/firestore_query.gd +++ b/addons/godot-firebase/firestore/firestore_query.gd @@ -1,4 +1,4 @@ -## @meta-authors Nicoló 'fenix' Santilio +## @meta-authors Nicoló 'fenix' Santilio, Kyle Szklenski ## @meta-version 1.4 ## A firestore query. ## Documentation TODO. @@ -7,11 +7,11 @@ extends RefCounted class_name FirestoreQuery class Order: - var obj : Dictionary + var obj: Dictionary class Cursor: - var values : Array - var before : bool + var values: Array + var before: bool func _init(v : Array,b : bool): values = v @@ -19,7 +19,7 @@ class Cursor: signal query_result(query_result) -const TEMPLATE_QUERY : Dictionary = { +const TEMPLATE_QUERY: Dictionary = { select = {}, from = [], where = {}, @@ -30,11 +30,12 @@ const TEMPLATE_QUERY : Dictionary = { limit = 0 } -var query : Dictionary = {} +var query: Dictionary = {} +var aggregations: Array[Dictionary] = [] enum OPERATOR { # Standard operators - OPERATOR_NSPECIFIED, + OPERATOR_UNSPECIFIED, LESS_THAN, LESS_THAN_OR_EQUAL, GREATER_THAN, @@ -87,8 +88,6 @@ func from(collection_id : String, all_descendants : bool = true) -> FirestoreQue query["from"] = [{collectionId = collection_id, allDescendants = all_descendants}] return self - - # @collections_array MUST be an Array of Arrays with this structure # [ ["collection_id", true/false] ] func from_many(collections_array : Array) -> FirestoreQuery: @@ -159,8 +158,6 @@ func order_by_fields(order_field_list : Array) -> FirestoreQuery: query["orderBy"] = order_list return self - - func start_at(value, before : bool) -> FirestoreQuery: var cursor : Cursor = _cursor_object(value, before) query["startAt"] = { values = cursor.values, before = cursor.before } @@ -191,6 +188,26 @@ func limit(limit : int) -> FirestoreQuery: return self +func aggregate() -> FirestoreAggregation: + return FirestoreAggregation.new(self) + +class FirestoreAggregation extends RefCounted: + var _query: FirestoreQuery + + func _init(query: FirestoreQuery) -> void: + _query = query + + func sum(field: String) -> FirestoreQuery: + _query.aggregations.push_back({ sum = { field = { fieldPath = field }}}) + return _query + + func count(up_to: int) -> FirestoreQuery: + _query.aggregations.push_back({ count = { upTo = up_to }}) + return _query + + func average(field: String) -> FirestoreQuery: + _query.aggregations.push_back({ avg = { field = { fieldPath = field }}}) + return _query # UTILITIES ---------------------------------------- diff --git a/addons/godot-firebase/firestore/firestore_task.gd b/addons/godot-firebase/firestore/firestore_task.gd index 9fad023..8122ae5 100644 --- a/addons/godot-firebase/firestore/firestore_task.gd +++ b/addons/godot-firebase/firestore/firestore_task.gd @@ -31,6 +31,7 @@ enum Task { TASK_PATCH, ## A PATCH Request Task, processing a update() request TASK_DELETE, ## A DELETE Request Task, processing a delete() request TASK_QUERY, ## A POST Request Task, processing a query() request + TASK_AGG_QUERY, ## A POST Request Task, processing an aggregation_query() request TASK_LIST, ## A POST Request Task, processing a list() request TASK_COMMIT ## A POST Request Task that hits the write api } @@ -43,7 +44,8 @@ const TASK_MAP = { Task.TASK_DELETE: "DELETE DOCUMENT", Task.TASK_QUERY: "QUERY COLLECTION", Task.TASK_LIST: "LIST DOCUMENTS", - Task.TASK_COMMIT: "COMMIT DOCUMENT" + Task.TASK_COMMIT: "COMMIT DOCUMENT", + Task.TASK_AGG_QUERY: "AGG QUERY COLLECTION" } ## The code indicating the request Firestore is processing. @@ -53,24 +55,23 @@ var action : int = -1 : set = set_action ## A variable, temporary holding the result of the request. var data -var error : Dictionary -var document : FirestoreDocument +var error: Dictionary +var document: FirestoreDocument -var _response_headers : PackedStringArray = PackedStringArray() -var _response_code : int = 0 +var _response_headers: PackedStringArray = PackedStringArray() +var _response_code: int = 0 -var _method : int = -1 -var _url : String = "" -var _fields : String = "" -var _headers : PackedStringArray = [] +var _method: int = -1 +var _url: String = "" +var _fields: String = "" +var _headers: PackedStringArray = [] -func _on_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: +func _on_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void: var bod = body.get_string_from_utf8() if bod != "": bod = Utilities.get_json_data(bod) - + var failed: bool = bod is Dictionary and bod.has("error") and response_code != HTTPClient.RESPONSE_OK - # Probably going to regret this... if response_code == HTTPClient.RESPONSE_OK: match action: @@ -84,6 +85,18 @@ func _on_request_completed(result : int, response_code : int, headers : PackedSt for doc in bod: if doc.has('document'): data.append(FirestoreDocument.new(doc.document)) + Task.TASK_AGG_QUERY: + var agg_results = [] + for agg_result in bod: + var idx = 0 + var query_results = {} + for field_value in agg_result.result.aggregateFields.keys(): + var agg = data.aggregations[idx] + var field = agg_result.result.aggregateFields[field_value] + query_results[agg.keys()[0]] = Utilities.from_firebase_type(field) + idx += 1 + agg_results.push_back(query_results) + data = agg_results Task.TASK_LIST: data = [] if bod.has('documents'): @@ -127,7 +140,7 @@ func set_action(value : int) -> void: match action: Task.TASK_GET, Task.TASK_LIST: _method = HTTPClient.METHOD_GET - Task.TASK_POST, Task.TASK_QUERY: + Task.TASK_POST, Task.TASK_QUERY, Task.TASK_AGG_QUERY: _method = HTTPClient.METHOD_POST Task.TASK_PATCH: _method = HTTPClient.METHOD_PATCH @@ -135,6 +148,8 @@ func set_action(value : int) -> void: _method = HTTPClient.METHOD_DELETE Task.TASK_COMMIT: _method = HTTPClient.METHOD_POST + _: + assert(false) func _merge_dict(dic_a : Dictionary, dic_b : Dictionary, nullify := false) -> Dictionary: From 763155803b9f72d89ceaa5c53499693c27df2caa Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Fri, 19 Jul 2024 07:47:58 -0400 Subject: [PATCH 45/49] Fix typo in docs --- addons/godot-firebase/firestore/firestore.gd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/godot-firebase/firestore/firestore.gd b/addons/godot-firebase/firestore/firestore.gd index 839990d..f7ea76e 100644 --- a/addons/godot-firebase/firestore/firestore.gd +++ b/addons/godot-firebase/firestore/firestore.gd @@ -90,7 +90,7 @@ func collection(path : String) -> FirestoreCollection: ## When awaited, this function returns the resulting array from the query. ## ## ex. -## [code]var query_results = wait Firebase.Firestore.query(FirestoreQuery.new())[/code] +## [code]var query_results = await Firebase.Firestore.query(FirestoreQuery.new())[/code] ## ## [b]Warning:[/b] It currently does not work offline! ## From 46dc6f74eb0226e25c19871935f435b325174934 Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Fri, 19 Jul 2024 07:49:17 -0400 Subject: [PATCH 46/49] Fix whitespace changes --- addons/godot-firebase/firestore/firestore.gd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/godot-firebase/firestore/firestore.gd b/addons/godot-firebase/firestore/firestore.gd index f7ea76e..1676d3a 100644 --- a/addons/godot-firebase/firestore/firestore.gd +++ b/addons/godot-firebase/firestore/firestore.gd @@ -105,7 +105,7 @@ func query(query : FirestoreQuery) -> Array: var task : FirestoreTask = FirestoreTask.new() task.action = FirestoreTask.Task.TASK_QUERY var body: Dictionary = { structuredQuery = query.query } - var url: String = _base_url + _extended_url + _query_suffix + var url: String = _base_url + _extended_url + _query_suffix task.data = query task._fields = JSON.stringify(body) From 49e6a6fd4aabf9690cfb70e7f469a483341bd691 Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Fri, 19 Jul 2024 10:59:54 -0400 Subject: [PATCH 47/49] Fix indentation issue causing null ref --- addons/godot-firebase/firestore/firestore_collection.gd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/godot-firebase/firestore/firestore_collection.gd b/addons/godot-firebase/firestore/firestore_collection.gd index 0dae95f..4883f87 100644 --- a/addons/godot-firebase/firestore/firestore_collection.gd +++ b/addons/godot-firebase/firestore/firestore_collection.gd @@ -108,8 +108,8 @@ func update(document : FirestoreDocument) -> FirestoreDocument: child.replace(result, true) break - if temp_transforms != null: - result._transforms = temp_transforms + if temp_transforms != null: + result._transforms = temp_transforms return result From c89725eab07492afe1cba222d62a26dccf6d7b60 Mon Sep 17 00:00:00 2001 From: Anish Mishra Date: Fri, 20 Sep 2024 19:13:20 +0530 Subject: [PATCH 48/49] fix Storage error reporting --- addons/godot-firebase/storage/storage.gd | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/addons/godot-firebase/storage/storage.gd b/addons/godot-firebase/storage/storage.gd index 28d016e..baabd75 100644 --- a/addons/godot-firebase/storage/storage.gd +++ b/addons/godot-firebase/storage/storage.gd @@ -156,6 +156,7 @@ func _check_emulating() -> void : func _upload(data : PackedByteArray, headers : PackedStringArray, ref : StorageReference, meta_only : bool) -> Variant: if _is_invalid_authentication(): + Firebase._printerr("Error uploading to storage: Invalid authentication") return 0 var task := StorageTask.new() @@ -169,6 +170,7 @@ func _upload(data : PackedByteArray, headers : PackedStringArray, ref : StorageR func _download(ref : StorageReference, meta_only : bool, url_only : bool) -> Variant: if _is_invalid_authentication(): + Firebase._printerr("Error downloading from storage: Invalid authentication") return 0 var info_task := StorageTask.new() @@ -204,6 +206,7 @@ func _download(ref : StorageReference, meta_only : bool, url_only : bool) -> Var func _list(ref : StorageReference, list_all : bool) -> Array: if _is_invalid_authentication(): + Firebase._printerr("Error getting object list from storage: Invalid authentication") return [] var task := StorageTask.new() @@ -215,6 +218,7 @@ func _list(ref : StorageReference, list_all : bool) -> Array: func _delete(ref : StorageReference) -> bool: if _is_invalid_authentication(): + Firebase._printerr("Error deleting object from storage: Invalid authentication") return false var task := StorageTask.new() @@ -270,6 +274,8 @@ func _finish_request(result : int) -> void: StorageTask.Task.TASK_DOWNLOAD_URL: var json = Utilities.get_json_data(_response_data) + if json != null and json.has("error"): + Firebase._printerr("Error getting object download url: "+json["error"].message) if json != null and json.has("downloadTokens"): task.data = _base_url + _get_file_url(task.ref) + "?alt=media&token=" + json.downloadTokens else: @@ -278,6 +284,8 @@ func _finish_request(result : int) -> void: StorageTask.Task.TASK_LIST, StorageTask.Task.TASK_LIST_ALL: var json = Utilities.get_json_data(_response_data) var items := [] + if json != null and json.has("error"): + Firebase._printerr("Error getting data from storage: "+json["error"].message) if json != null and json.has("items"): for item in json.items: var item_name : String = item.name From 138ddd8e38b5be9286f1602a055fd3a457c5be9a Mon Sep 17 00:00:00 2001 From: Anish Mishra Date: Thu, 26 Sep 2024 18:20:09 +0530 Subject: [PATCH 49/49] fix_oAuth_login_succeeded_emitting_twice --- addons/godot-firebase/auth/auth.gd | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/addons/godot-firebase/auth/auth.gd b/addons/godot-firebase/auth/auth.gd index ec6c24c..8eefcef 100644 --- a/addons/godot-firebase/auth/auth.gd +++ b/addons/godot-firebase/auth/auth.gd @@ -48,6 +48,7 @@ var auth : Dictionary = {} var _needs_refresh : bool = false var is_busy : bool = false var has_child : bool = false +var is_oauth_login: bool = false var tcp_server : TCPServer = TCPServer.new() @@ -290,7 +291,6 @@ func get_auth_with_redirect(provider: AuthProvider) -> void: else: set_local_provider(provider) OS.shell_open(url_endpoint) - print(url_endpoint) # Login with Google oAuth2. @@ -298,8 +298,8 @@ func get_auth_with_redirect(provider: AuthProvider) -> void: # @provider_id and @request_uri can be changed func login_with_oauth(_token: String, provider: AuthProvider) -> void: if _token: + is_oauth_login = true var token : String = _token.uri_decode() - print(token) var is_successful: bool = true if provider.should_exchange: exchange_token(token, _local_uri, provider.access_token_uri, provider.get_client_id(), provider.get_client_secret()) @@ -426,7 +426,6 @@ func _on_FirebaseAuth_request_completed(result : int, response_code : int, heade return res = json - if response_code == HTTPClient.RESPONSE_OK: if not res.has("kind"): auth = get_clean_keys(res) @@ -439,7 +438,7 @@ func _on_FirebaseAuth_request_completed(result : int, response_code : int, heade if _needs_refresh: _needs_refresh = false - login_succeeded.emit(auth) + if not is_oauth_login: login_succeeded.emit(auth) else: match res.kind: RESPONSE_SIGNUP: @@ -466,6 +465,7 @@ func _on_FirebaseAuth_request_completed(result : int, response_code : int, heade auth_request.emit(res.error.code, res.error.message) requesting = Requests.NONE auth_request_type = Auth_Type.NONE + is_oauth_login = false