├── icon.png ├── addons ├── gut │ ├── icon.png │ ├── source_code_pro.fnt │ ├── fonts │ │ ├── LobsterTwo-Bold.ttf │ │ ├── AnonymousPro-Bold.ttf │ │ ├── CourierPrime-Bold.ttf │ │ ├── LobsterTwo-Italic.ttf │ │ ├── LobsterTwo-Regular.ttf │ │ ├── AnonymousPro-Italic.ttf │ │ ├── AnonymousPro-Regular.ttf │ │ ├── CourierPrime-Italic.ttf │ │ ├── CourierPrime-Regular.ttf │ │ ├── AnonymousPro-BoldItalic.ttf │ │ ├── CourierPrime-BoldItalic.ttf │ │ ├── LobsterTwo-BoldItalic.ttf │ │ └── OFL.txt │ ├── plugin.cfg │ ├── double_templates │ │ ├── function_template.txt │ │ └── script_template.txt │ ├── gut_plugin.gd │ ├── icon.png.import │ ├── parameter_handler.gd │ ├── thing_counter.gd │ ├── compare_result.gd │ ├── LICENSE.md │ ├── stub_params.gd │ ├── hook_script.gd │ ├── one_to_many.gd │ ├── UserFileViewer.gd │ ├── diff_formatter.gd │ ├── autofree.gd │ ├── orphan_counter.gd │ ├── spy.gd │ ├── parameter_factory.gd │ ├── comparator.gd │ ├── diff_tool.gd │ ├── printers.gd │ ├── UserFileViewer.tscn │ ├── summary.gd │ ├── stubber.gd │ ├── strutils.gd │ ├── signal_watcher.gd │ ├── method_maker.gd │ ├── optparse.gd │ ├── test_collector.gd │ ├── logger.gd │ ├── plugin_control.gd │ └── GutScene.gd └── synced │ ├── SyncPeer.tscn │ ├── SyncInputFacade.gd │ ├── Aligned.gd │ ├── SyncSequence.gd │ └── SyncManager.gd ├── assets └── pong320at10.gif ├── playground └── pong │ ├── ball.png │ ├── icon.png │ ├── paddle.png │ ├── separator.png │ ├── icon.png.import │ ├── ball.png.import │ ├── paddle.png.import │ ├── separator.png.import │ ├── paddle.tscn │ ├── logic │ ├── paddle.gd │ ├── ball.gd │ ├── pong.gd │ └── lobby.gd │ ├── ball.tscn │ ├── pong.tscn │ └── lobby.tscn ├── .gitignore ├── .gutconfig.json ├── default_env.tres ├── icon.png.import ├── LICENSE.txt ├── test └── unit │ ├── test_Aligned.gd │ ├── test_Synced.gd │ ├── test_SyncPeer.gd │ └── test_SyncedProperty.gd ├── project.godot └── README.md /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leonix/godot-synced/HEAD/icon.png -------------------------------------------------------------------------------- /addons/gut/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leonix/godot-synced/HEAD/addons/gut/icon.png -------------------------------------------------------------------------------- /assets/pong320at10.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leonix/godot-synced/HEAD/assets/pong320at10.gif -------------------------------------------------------------------------------- /playground/pong/ball.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leonix/godot-synced/HEAD/playground/pong/ball.png -------------------------------------------------------------------------------- /playground/pong/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leonix/godot-synced/HEAD/playground/pong/icon.png -------------------------------------------------------------------------------- /playground/pong/paddle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leonix/godot-synced/HEAD/playground/pong/paddle.png -------------------------------------------------------------------------------- /playground/pong/separator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leonix/godot-synced/HEAD/playground/pong/separator.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Godot ### 2 | .import/ 3 | .godot/ 4 | #export.cfg 5 | export_presets.cfg 6 | *.translation 7 | -------------------------------------------------------------------------------- /addons/gut/source_code_pro.fnt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leonix/godot-synced/HEAD/addons/gut/source_code_pro.fnt -------------------------------------------------------------------------------- /addons/gut/fonts/LobsterTwo-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leonix/godot-synced/HEAD/addons/gut/fonts/LobsterTwo-Bold.ttf -------------------------------------------------------------------------------- /addons/gut/fonts/AnonymousPro-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leonix/godot-synced/HEAD/addons/gut/fonts/AnonymousPro-Bold.ttf -------------------------------------------------------------------------------- /addons/gut/fonts/CourierPrime-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leonix/godot-synced/HEAD/addons/gut/fonts/CourierPrime-Bold.ttf -------------------------------------------------------------------------------- /addons/gut/fonts/LobsterTwo-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leonix/godot-synced/HEAD/addons/gut/fonts/LobsterTwo-Italic.ttf -------------------------------------------------------------------------------- /addons/gut/fonts/LobsterTwo-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leonix/godot-synced/HEAD/addons/gut/fonts/LobsterTwo-Regular.ttf -------------------------------------------------------------------------------- /addons/gut/fonts/AnonymousPro-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leonix/godot-synced/HEAD/addons/gut/fonts/AnonymousPro-Italic.ttf -------------------------------------------------------------------------------- /addons/gut/fonts/AnonymousPro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leonix/godot-synced/HEAD/addons/gut/fonts/AnonymousPro-Regular.ttf -------------------------------------------------------------------------------- /addons/gut/fonts/CourierPrime-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leonix/godot-synced/HEAD/addons/gut/fonts/CourierPrime-Italic.ttf -------------------------------------------------------------------------------- /addons/gut/fonts/CourierPrime-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leonix/godot-synced/HEAD/addons/gut/fonts/CourierPrime-Regular.ttf -------------------------------------------------------------------------------- /addons/gut/fonts/AnonymousPro-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leonix/godot-synced/HEAD/addons/gut/fonts/AnonymousPro-BoldItalic.ttf -------------------------------------------------------------------------------- /addons/gut/fonts/CourierPrime-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leonix/godot-synced/HEAD/addons/gut/fonts/CourierPrime-BoldItalic.ttf -------------------------------------------------------------------------------- /addons/gut/fonts/LobsterTwo-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leonix/godot-synced/HEAD/addons/gut/fonts/LobsterTwo-BoldItalic.ttf -------------------------------------------------------------------------------- /.gutconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "dirs":["res://test/unit/","res://test/integration/"], 3 | "include_subdirs":true, 4 | "log_level":1, 5 | "should_exit":true 6 | } -------------------------------------------------------------------------------- /addons/gut/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="Gut" 4 | description="Unit Testing tool for Godot." 5 | author="Butch Wesley" 6 | version="7.1.0" 7 | script="gut_plugin.gd" 8 | -------------------------------------------------------------------------------- /default_env.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Environment" load_steps=2 format=2] 2 | 3 | [sub_resource type="ProceduralSky" id=1] 4 | 5 | [resource] 6 | background_mode = 2 7 | background_sky = SubResource( 1 ) 8 | -------------------------------------------------------------------------------- /addons/gut/double_templates/function_template.txt: -------------------------------------------------------------------------------- 1 | {func_decleration} 2 | __gut_spy('{method_name}', {param_array}) 3 | if(__gut_should_call_super('{method_name}', {param_array})): 4 | return {super_call} 5 | else: 6 | return __gut_get_stubbed_return('{method_name}', {param_array}) 7 | -------------------------------------------------------------------------------- /addons/synced/SyncPeer.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=2] 2 | 3 | [ext_resource path="res://addons/synced/SyncPeer.gd" type="Script" id=1] 4 | [ext_resource path="res://addons/synced/SyncInputFacade.gd" type="Script" id=2] 5 | 6 | [node name="SyncPeer" type="Node"] 7 | script = ExtResource( 1 ) 8 | 9 | [node name="SyncInputFacade" type="Node" parent="."] 10 | script = ExtResource( 2 ) 11 | -------------------------------------------------------------------------------- /addons/gut/gut_plugin.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends EditorPlugin 3 | 4 | func _enter_tree(): 5 | # Initialization of the plugin goes here 6 | # Add the new type with a name, a parent type, a script and an icon 7 | add_custom_type("Gut", "Control", preload("plugin_control.gd"), preload("icon.png")) 8 | 9 | func _exit_tree(): 10 | # Clean-up of the plugin goes here 11 | # Always remember to remove it from the engine when deactivated 12 | remove_custom_type("Gut") 13 | -------------------------------------------------------------------------------- /icon.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://icon.png" 13 | dest_files=[ "res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" ] 14 | 15 | [params] 16 | 17 | compress/mode=0 18 | compress/lossy_quality=0.7 19 | compress/hdr_mode=0 20 | compress/bptc_ldr=0 21 | compress/normal_map=0 22 | flags/repeat=0 23 | flags/filter=true 24 | flags/mipmaps=false 25 | flags/anisotropic=false 26 | flags/srgb=2 27 | process/fix_alpha_border=true 28 | process/premult_alpha=false 29 | process/HDR_as_SRGB=false 30 | process/invert_color=false 31 | stream=false 32 | size_limit=0 33 | detect_3d=true 34 | svg/scale=1.0 35 | -------------------------------------------------------------------------------- /addons/gut/icon.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/icon.png-91b084043b8aaf2f1c906e7b9fa92969.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://addons/gut/icon.png" 13 | dest_files=[ "res://.import/icon.png-91b084043b8aaf2f1c906e7b9fa92969.stex" ] 14 | 15 | [params] 16 | 17 | compress/mode=0 18 | compress/lossy_quality=0.7 19 | compress/hdr_mode=0 20 | compress/bptc_ldr=0 21 | compress/normal_map=0 22 | flags/repeat=0 23 | flags/filter=true 24 | flags/mipmaps=false 25 | flags/anisotropic=false 26 | flags/srgb=2 27 | process/fix_alpha_border=true 28 | process/premult_alpha=false 29 | process/HDR_as_SRGB=false 30 | process/invert_color=false 31 | stream=false 32 | size_limit=0 33 | detect_3d=true 34 | svg/scale=1.0 35 | -------------------------------------------------------------------------------- /playground/pong/icon.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/icon.png-80de03efd93655186c7ecb6e8512a5e9.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://playground/pong/icon.png" 13 | dest_files=[ "res://.import/icon.png-80de03efd93655186c7ecb6e8512a5e9.stex" ] 14 | 15 | [params] 16 | 17 | compress/mode=0 18 | compress/lossy_quality=0.7 19 | compress/hdr_mode=0 20 | compress/bptc_ldr=0 21 | compress/normal_map=0 22 | flags/repeat=0 23 | flags/filter=true 24 | flags/mipmaps=false 25 | flags/anisotropic=false 26 | flags/srgb=2 27 | process/fix_alpha_border=true 28 | process/premult_alpha=false 29 | process/HDR_as_SRGB=false 30 | process/invert_color=false 31 | stream=false 32 | size_limit=0 33 | detect_3d=true 34 | svg/scale=1.0 35 | -------------------------------------------------------------------------------- /playground/pong/ball.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/ball.png-f1e7dc1e434530fc037bdf090e17a56d.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://playground/pong/ball.png" 13 | dest_files=[ "res://.import/ball.png-f1e7dc1e434530fc037bdf090e17a56d.stex" ] 14 | 15 | [params] 16 | 17 | compress/mode=0 18 | compress/lossy_quality=0.7 19 | compress/hdr_mode=0 20 | compress/bptc_ldr=0 21 | compress/normal_map=0 22 | flags/repeat=0 23 | flags/filter=false 24 | flags/mipmaps=false 25 | flags/anisotropic=false 26 | flags/srgb=2 27 | process/fix_alpha_border=true 28 | process/premult_alpha=false 29 | process/HDR_as_SRGB=false 30 | process/invert_color=false 31 | stream=false 32 | size_limit=0 33 | detect_3d=true 34 | svg/scale=1.0 35 | -------------------------------------------------------------------------------- /playground/pong/paddle.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/paddle.png-520a93847c325a454de25df9fc1c8646.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://playground/pong/paddle.png" 13 | dest_files=[ "res://.import/paddle.png-520a93847c325a454de25df9fc1c8646.stex" ] 14 | 15 | [params] 16 | 17 | compress/mode=0 18 | compress/lossy_quality=0.7 19 | compress/hdr_mode=0 20 | compress/bptc_ldr=0 21 | compress/normal_map=0 22 | flags/repeat=0 23 | flags/filter=false 24 | flags/mipmaps=false 25 | flags/anisotropic=false 26 | flags/srgb=2 27 | process/fix_alpha_border=true 28 | process/premult_alpha=false 29 | process/HDR_as_SRGB=false 30 | process/invert_color=false 31 | stream=false 32 | size_limit=0 33 | detect_3d=true 34 | svg/scale=1.0 35 | -------------------------------------------------------------------------------- /playground/pong/separator.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/separator.png-ed8b7d9d68a27d0da5d27795756c934e.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://playground/pong/separator.png" 13 | dest_files=[ "res://.import/separator.png-ed8b7d9d68a27d0da5d27795756c934e.stex" ] 14 | 15 | [params] 16 | 17 | compress/mode=0 18 | compress/lossy_quality=0.7 19 | compress/hdr_mode=0 20 | compress/bptc_ldr=0 21 | compress/normal_map=0 22 | flags/repeat=0 23 | flags/filter=false 24 | flags/mipmaps=false 25 | flags/anisotropic=false 26 | flags/srgb=2 27 | process/fix_alpha_border=true 28 | process/premult_alpha=false 29 | process/HDR_as_SRGB=false 30 | process/invert_color=false 31 | stream=false 32 | size_limit=0 33 | detect_3d=true 34 | svg/scale=1.0 35 | -------------------------------------------------------------------------------- /addons/gut/parameter_handler.gd: -------------------------------------------------------------------------------- 1 | var _utils = load('res://addons/gut/utils.gd').get_instance() 2 | var _params = null 3 | var _call_count = 0 4 | var _logger = null 5 | 6 | func _init(params=null): 7 | _params = params 8 | _logger = _utils.get_logger() 9 | if(typeof(_params) != TYPE_ARRAY): 10 | _logger.error('You must pass an array to parameter_handler constructor.') 11 | _params = null 12 | 13 | 14 | func next_parameters(): 15 | _call_count += 1 16 | return _params[_call_count -1] 17 | 18 | func get_current_parameters(): 19 | return _params[_call_count] 20 | 21 | func is_done(): 22 | var done = true 23 | if(_params != null): 24 | done = _call_count == _params.size() 25 | return done 26 | 27 | func get_logger(): 28 | return _logger 29 | 30 | func set_logger(logger): 31 | _logger = logger 32 | 33 | func get_call_count(): 34 | return _call_count 35 | 36 | func get_parameter_count(): 37 | return _params.size() -------------------------------------------------------------------------------- /addons/gut/thing_counter.gd: -------------------------------------------------------------------------------- 1 | var things = {} 2 | 3 | func get_unique_count(): 4 | return things.size() 5 | 6 | func add(thing): 7 | if(things.has(thing)): 8 | things[thing] += 1 9 | else: 10 | things[thing] = 1 11 | 12 | func has(thing): 13 | return things.has(thing) 14 | 15 | func get(thing): 16 | var to_return = 0 17 | if(things.has(thing)): 18 | to_return = things[thing] 19 | return to_return 20 | 21 | func sum(): 22 | var count = 0 23 | for key in things: 24 | count += things[key] 25 | return count 26 | 27 | func to_s(): 28 | var to_return = "" 29 | for key in things: 30 | to_return += str(key, ": ", things[key], "\n") 31 | to_return += str("sum: ", sum()) 32 | return to_return 33 | 34 | func get_max_count(): 35 | var max_val = null 36 | for key in things: 37 | if(max_val == null or things[key] > max_val): 38 | max_val = things[key] 39 | return max_val 40 | 41 | func add_array_items(array): 42 | for i in range(array.size()): 43 | add(array[i]) 44 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021-2022 Leonid Vakulenko 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /addons/gut/compare_result.gd: -------------------------------------------------------------------------------- 1 | var are_equal = null setget set_are_equal, get_are_equal 2 | var summary = null setget set_summary, get_summary 3 | var max_differences = 30 setget set_max_differences, get_max_differences 4 | var differences = {} setget set_differences, get_differences 5 | 6 | func _block_set(which, val): 7 | push_error(str('cannot set ', which, ', value [', val, '] ignored.')) 8 | 9 | func _to_string(): 10 | return str(get_summary()) # could be null, gotta str it. 11 | 12 | func get_are_equal(): 13 | return are_equal 14 | 15 | func set_are_equal(r_eq): 16 | are_equal = r_eq 17 | 18 | func get_summary(): 19 | return summary 20 | 21 | func set_summary(smry): 22 | summary = smry 23 | 24 | func get_total_count(): 25 | pass 26 | 27 | func get_different_count(): 28 | pass 29 | 30 | func get_short_summary(): 31 | return summary 32 | 33 | func get_max_differences(): 34 | return max_differences 35 | 36 | func set_max_differences(max_diff): 37 | max_differences = max_diff 38 | 39 | func get_differences(): 40 | return differences 41 | 42 | func set_differences(diffs): 43 | _block_set('differences', diffs) 44 | 45 | func get_brackets(): 46 | return null 47 | 48 | -------------------------------------------------------------------------------- /addons/gut/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright (c) 2018 Tom "Butch" Wesley 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /addons/gut/stub_params.gd: -------------------------------------------------------------------------------- 1 | var return_val = null 2 | var stub_target = null 3 | var target_subpath = null 4 | var parameters = null 5 | var stub_method = null 6 | var call_super = false 7 | 8 | const NOT_SET = '|_1_this_is_not_set_1_|' 9 | 10 | func _init(target=null, method=null, subpath=null): 11 | stub_target = target 12 | stub_method = method 13 | target_subpath = subpath 14 | 15 | func to_return(val): 16 | return_val = val 17 | call_super = false 18 | return self 19 | 20 | func to_do_nothing(): 21 | return to_return(null) 22 | 23 | func to_call_super(): 24 | call_super = true 25 | return self 26 | 27 | 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): 28 | parameters = [p1,p2,p3,p4,p5,p6,p7,p8,p9,p10] 29 | var idx = 0 30 | while(idx < parameters.size()): 31 | if(str(parameters[idx]) == NOT_SET): 32 | parameters.remove(idx) 33 | else: 34 | idx += 1 35 | return self 36 | 37 | func to_s(): 38 | var base_string = str(stub_target, '[', target_subpath, '].', stub_method) 39 | if(call_super): 40 | base_string += " to call SUPER" 41 | else: 42 | base_string += str(' with (', parameters, ') = ', return_val) 43 | return base_string 44 | -------------------------------------------------------------------------------- /addons/gut/hook_script.gd: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # This script is the base for custom scripts to be used in pre and post 3 | # run hooks. 4 | # ------------------------------------------------------------------------------ 5 | 6 | # This is the instance of GUT that is running the tests. You can get 7 | # information about the run from this object. This is set by GUT when the 8 | # script is instantiated. 9 | var gut = null 10 | 11 | # the exit code to be used by gut_cmdln. See set method. 12 | var _exit_code = null 13 | 14 | var _should_abort = false 15 | # Virtual method that will be called by GUT after instantiating 16 | # this script. 17 | func run(): 18 | pass 19 | 20 | # Set the exit code when running from the command line. If not set then the 21 | # default exit code will be returned (0 when no tests fail, 1 when any tests 22 | # fail). 23 | func set_exit_code(code): 24 | _exit_code = code 25 | 26 | func get_exit_code(): 27 | return _exit_code 28 | 29 | # Usable by pre-run script to cause the run to end AFTER the run() method 30 | # finishes. post-run script will not be ran. 31 | func abort(): 32 | _should_abort = true 33 | 34 | func should_abort(): 35 | return _should_abort 36 | -------------------------------------------------------------------------------- /addons/gut/one_to_many.gd: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # This datastructure represents a simple one-to-many relationship. It manages 3 | # a dictionary of value/array pairs. It ignores duplicates of both the "one" 4 | # and the "many". 5 | # ------------------------------------------------------------------------------ 6 | var _items = {} 7 | 8 | # return the size of _items or the size of an element in _items if "one" was 9 | # specified. 10 | func size(one=null): 11 | var to_return = 0 12 | if(one == null): 13 | to_return = _items.size() 14 | elif(_items.has(one)): 15 | to_return = _items[one].size() 16 | return to_return 17 | 18 | # Add an element to "one" if it does not already exist 19 | func add(one, many_item): 20 | if(_items.has(one) and !_items[one].has(many_item)): 21 | _items[one].append(many_item) 22 | else: 23 | _items[one] = [many_item] 24 | 25 | func clear(): 26 | _items.clear() 27 | 28 | func has(one, many_item): 29 | var to_return = false 30 | if(_items.has(one)): 31 | to_return = _items[one].has(many_item) 32 | return to_return 33 | 34 | func to_s(): 35 | var to_return = '' 36 | for key in _items: 37 | to_return += str(key, ": ", _items[key], "\n") 38 | return to_return 39 | -------------------------------------------------------------------------------- /playground/pong/paddle.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=6 format=2] 2 | 3 | [ext_resource path="res://playground/pong/logic/paddle.gd" type="Script" id=1] 4 | [ext_resource path="res://playground/pong/paddle.png" type="Texture" id=2] 5 | [ext_resource path="res://addons/synced/Aligned.gd" type="Script" id=3] 6 | [ext_resource path="res://addons/synced/Synced.gd" type="Script" id=4] 7 | 8 | [sub_resource type="CapsuleShape2D" id=1] 9 | radius = 4.78568 10 | height = 23.6064 11 | 12 | [node name="Paddle" type="Area2D"] 13 | script = ExtResource( 1 ) 14 | 15 | [node name="Sprite" type="Sprite" parent="."] 16 | texture = ExtResource( 2 ) 17 | 18 | [node name="Shape" type="CollisionShape2D" parent="."] 19 | shape = SubResource( 1 ) 20 | 21 | [node name="You" type="Label" parent="."] 22 | margin_left = -26.0 23 | margin_top = -33.0 24 | margin_right = 27.0 25 | margin_bottom = -19.0 26 | size_flags_horizontal = 2 27 | size_flags_vertical = 0 28 | text = "You" 29 | align = 1 30 | __meta__ = { 31 | "_edit_use_anchors_": false 32 | } 33 | 34 | [node name="synced" type="Node" parent="."] 35 | script = ExtResource( 4 ) 36 | 37 | [node name="aligned" type="Node2D" parent="."] 38 | script = ExtResource( 3 ) 39 | [connection signal="area_entered" from="." to="." method="_on_paddle_area_enter"] 40 | -------------------------------------------------------------------------------- /playground/pong/logic/paddle.gd: -------------------------------------------------------------------------------- 1 | extends Area2D 2 | 3 | const MOTION_SPEED = 150.0 4 | 5 | export var left = false 6 | 7 | var _motion = 0.0 8 | var _you_hidden = false 9 | 10 | onready var _screen_size_y = get_viewport_rect().size.y 11 | onready var synced:Synced = $synced 12 | onready var aligned:Aligned = $aligned 13 | 14 | func _process(_delta): 15 | # Hide label instantly for another person's paddle, or after a move for your paddle. 16 | if not _you_hidden: 17 | if _motion != 0 or not synced.is_local_peer(): 18 | _hide_you_label() 19 | 20 | func _physics_process(delta): 21 | # if name == 'Player2': 22 | # synced.synced_property('rotation').debug_log = true 23 | _motion = synced.input.get_action_strength("move_down") - synced.input.get_action_strength("move_up") 24 | _motion *= MOTION_SPEED 25 | if _motion != 0.0: 26 | position.y = clamp(position.y + _motion * delta, 16, _screen_size_y - 16) 27 | aligned.position = position 28 | look_at(get_viewport().get_mouse_position()) 29 | if not left: 30 | rotation += PI 31 | 32 | func _hide_you_label(): 33 | _you_hidden = true 34 | get_node("You").hide() 35 | 36 | func _on_paddle_area_enter(area): 37 | if not SyncManager.is_client() or synced.is_local_peer(): 38 | area.find_parent('Ball').bounce(left, rotation) 39 | -------------------------------------------------------------------------------- /test/unit/test_Aligned.gd: -------------------------------------------------------------------------------- 1 | extends "res://addons/gut/test.gd" 2 | 3 | var AlignedReference = preload("res://addons/synced/Aligned.gd") 4 | 5 | func test_basic_td_node2d(): 6 | var node = autofree(Node2D.new()) 7 | var synced = autofree(Synced.new()) 8 | var td = autofree(Node2D.new()) 9 | td.set_script(AlignedReference) 10 | node.add_child(synced) 11 | node.add_child(td) 12 | add_child_autofree(node) 13 | assert_eq(2, synced.get_child_count()) 14 | assert_not_null(synced.synced_property('position')) 15 | assert_not_null(synced.synced_property('rotation')) 16 | assert_null(synced.synced_property('translation')) 17 | assert_true(synced._should_auto_update_parent) 18 | assert_true(synced._should_auto_read_parent) 19 | 20 | func test_basic_td_spatial(): 21 | var node = autofree(Spatial.new()) 22 | var synced = autofree(Synced.new()) 23 | var td = autofree(Spatial.new()) 24 | td.set_script(AlignedReference) 25 | node.add_child(td) 26 | node.add_child(synced) 27 | add_child_autofree(node) 28 | assert_eq(2, synced.get_child_count()) 29 | assert_null(synced.synced_property('position')) 30 | assert_not_null(synced.synced_property('rotation')) 31 | assert_not_null(synced.synced_property('translation')) 32 | assert_true(synced._should_auto_update_parent) 33 | assert_true(synced._should_auto_read_parent) 34 | -------------------------------------------------------------------------------- /addons/gut/UserFileViewer.gd: -------------------------------------------------------------------------------- 1 | extends WindowDialog 2 | 3 | onready var rtl = $TextDisplay/RichTextLabel 4 | var _has_opened_file = false 5 | 6 | func _get_file_as_text(path): 7 | var to_return = null 8 | var f = File.new() 9 | var result = f.open(path, f.READ) 10 | if(result == OK): 11 | to_return = f.get_as_text() 12 | f.close() 13 | else: 14 | to_return = str('ERROR: Could not open file. Error code ', result) 15 | return to_return 16 | 17 | func _ready(): 18 | rtl.clear() 19 | 20 | func _on_OpenFile_pressed(): 21 | $FileDialog.popup_centered() 22 | 23 | func _on_FileDialog_file_selected(path): 24 | show_file(path) 25 | 26 | func _on_Close_pressed(): 27 | self.hide() 28 | 29 | func show_file(path): 30 | var text = _get_file_as_text(path) 31 | if(text == ''): 32 | text = '' 33 | rtl.set_text(text) 34 | self.window_title = path 35 | 36 | func show_open(): 37 | self.popup_centered() 38 | $FileDialog.popup_centered() 39 | 40 | func _on_FileDialog_popup_hide(): 41 | if(rtl.text.length() == 0): 42 | self.hide() 43 | 44 | func get_rich_text_label(): 45 | return $TextDisplay/RichTextLabel 46 | 47 | func _on_Home_pressed(): 48 | rtl.scroll_to_line(0) 49 | 50 | func _on_End_pressed(): 51 | rtl.scroll_to_line(rtl.get_line_count() -1) 52 | 53 | 54 | func _on_Copy_pressed(): 55 | OS.clipboard = rtl.text 56 | -------------------------------------------------------------------------------- /playground/pong/ball.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=7 format=2] 2 | 3 | [ext_resource path="res://playground/pong/logic/ball.gd" type="Script" id=1] 4 | [ext_resource path="res://playground/pong/ball.png" type="Texture" id=2] 5 | [ext_resource path="res://addons/synced/SyncedProperty.gd" type="Script" id=3] 6 | [ext_resource path="res://addons/synced/Synced.gd" type="Script" id=4] 7 | [ext_resource path="res://addons/synced/Aligned.gd" type="Script" id=5] 8 | 9 | [sub_resource type="CircleShape2D" id=1] 10 | radius = 5.11969 11 | 12 | [node name="Ball" type="Node2D"] 13 | script = ExtResource( 1 ) 14 | 15 | [node name="no_td_sprite" type="Sprite" parent="."] 16 | visible = false 17 | rotation = -0.785398 18 | scale = Vector2( 0.5, 0.5 ) 19 | texture = ExtResource( 2 ) 20 | 21 | [node name="synced" type="Node" parent="."] 22 | script = ExtResource( 4 ) 23 | 24 | [node name="direction" type="Node" parent="synced"] 25 | script = ExtResource( 3 ) 26 | 27 | [node name="speed" type="Node" parent="synced"] 28 | script = ExtResource( 3 ) 29 | 30 | [node name="aligned" type="Node2D" parent="."] 31 | script = ExtResource( 5 ) 32 | 33 | [node name="Sprite" type="Sprite" parent="aligned"] 34 | texture = ExtResource( 2 ) 35 | 36 | [node name="area" type="Area2D" parent="aligned"] 37 | 38 | [node name="Shape" type="CollisionShape2D" parent="aligned/area"] 39 | shape = SubResource( 1 ) 40 | -------------------------------------------------------------------------------- /addons/gut/double_templates/script_template.txt: -------------------------------------------------------------------------------- 1 | {extends} 2 | 3 | var __gut_metadata_ = { 4 | path = '{path}', 5 | subpath = '{subpath}', 6 | stubber = __gut_instance_from_id({stubber_id}), 7 | spy = __gut_instance_from_id({spy_id}), 8 | gut = __gut_instance_from_id({gut_id}), 9 | } 10 | 11 | func __gut_instance_from_id(inst_id): 12 | if(inst_id == -1): 13 | return null 14 | else: 15 | return instance_from_id(inst_id) 16 | 17 | func __gut_should_call_super(method_name, called_with): 18 | if(__gut_metadata_.stubber != null): 19 | return __gut_metadata_.stubber.should_call_super(self, method_name, called_with) 20 | else: 21 | return false 22 | 23 | var __gut_utils_ = load('res://addons/gut/utils.gd').get_instance() 24 | 25 | func __gut_spy(method_name, called_with): 26 | if(__gut_metadata_.spy != null): 27 | __gut_metadata_.spy.add_call(self, method_name, called_with) 28 | 29 | func __gut_get_stubbed_return(method_name, called_with): 30 | if(__gut_metadata_.stubber != null): 31 | return __gut_metadata_.stubber.get_return(self, method_name, called_with) 32 | else: 33 | return null 34 | 35 | func _init(): 36 | if(__gut_metadata_.gut != null): 37 | __gut_metadata_.gut.get_autofree().add_free(self) 38 | 39 | # ------------------------------------------------------------------------------ 40 | # Methods start here 41 | # ------------------------------------------------------------------------------ 42 | -------------------------------------------------------------------------------- /playground/pong/logic/ball.gd: -------------------------------------------------------------------------------- 1 | extends Node2D 2 | 3 | const DEFAULT_SPEED = 100 4 | 5 | var stopped = false 6 | 7 | onready var _screen_size = get_viewport_rect().size 8 | 9 | onready var synced = $synced 10 | onready var aligned = $aligned 11 | onready var area = $aligned/area 12 | 13 | func _ready(): 14 | synced.speed = DEFAULT_SPEED 15 | synced.direction = Vector2.RIGHT 16 | 17 | func _physics_process(delta): 18 | if not stopped: 19 | synced.speed += delta 20 | position += synced.speed * delta * synced.direction 21 | 22 | # Check screen bounds to make ball bounce. 23 | if (position.y < 0 and synced.direction.y < 0) or (position.y > _screen_size.y and synced.direction.y > 0): 24 | synced.direction.y = -synced.direction.y 25 | 26 | # Check if scored 27 | if not SyncManager.is_client(): 28 | if aligned.position.x < 0: 29 | get_parent().update_score(false) 30 | _reset_ball(false) 31 | elif aligned.position.x > _screen_size.x: 32 | get_parent().update_score(true) 33 | _reset_ball(true) 34 | 35 | # called by paddle.gd when ball hits the paddle 36 | func bounce(left, rot): 37 | var dir = (1 if left else -1) 38 | aligned.touch('position') 39 | aligned.direction = Vector2( 40 | abs(aligned.direction.x)*dir, 41 | 3*(fposmod(dir*rot+PI/2, PI) / PI - 0.5) 42 | ).normalized() 43 | aligned.speed *= 1.1 44 | 45 | # called by pong.gd when the game ends 46 | func stop(): 47 | stopped = true 48 | 49 | func _reset_ball(for_left): 50 | position = _screen_size / 2 51 | if for_left: 52 | synced.direction = Vector2.LEFT 53 | else: 54 | synced.direction = Vector2.RIGHT 55 | synced.speed = DEFAULT_SPEED 56 | -------------------------------------------------------------------------------- /test/unit/test_Synced.gd: -------------------------------------------------------------------------------- 1 | extends "res://addons/gut/test.gd" 2 | 3 | func test_basic_sb(): 4 | var synced = Synced.new() 5 | var prop = autofree(SyncedProperty.new()) 6 | prop.name = 'zzzz' 7 | synced.add_child(prop) 8 | add_child_autofree(synced) 9 | synced.zzzz = 1234 10 | assert_eq(1234, synced.zzzz) 11 | 12 | func test_frame_pack_parse_basic(): 13 | var frame = { 14 | zzzz = 1234, 15 | qqqq = 5678 16 | } 17 | var sendtable = ['qqqq', 'wwww', 'zzzz', 'tttt'] 18 | var packed = Synced.pack_data_frame(sendtable, frame) 19 | var unpacked_frame = Synced.parse_data_frame(sendtable, packed[0], packed[1]) 20 | assert_true(unpacked_frame is Dictionary) 21 | assert_eq(frame.size(), unpacked_frame.size()) 22 | for k in frame: 23 | assert_eq(frame[k], unpacked_frame[k], 'error in key %s' % k) 24 | 25 | func test_frame_pack_parse_empty_frame(): 26 | var frame = {} 27 | var sendtable = ['qqqq', 'wwww', 'zzzz', 'tttt'] 28 | var packed = Synced.pack_data_frame(sendtable, frame) 29 | assert_null(packed[0]) 30 | assert_null(packed[1]) 31 | var unpacked_frame = Synced.parse_data_frame(sendtable, packed[0], packed[1]) 32 | assert_true(unpacked_frame is Dictionary) 33 | assert_eq(frame.size(), unpacked_frame.size()) 34 | 35 | func test_frame_pack_parse_no_ids(): 36 | var frame = { 37 | wwww = 1234, 38 | qqqq = 5678 39 | } 40 | var sendtable = ['qqqq', 'wwww', 'zzzz', 'tttt'] 41 | var packed = Synced.pack_data_frame(sendtable, frame) 42 | assert_null(packed[0]) 43 | var unpacked_frame = Synced.parse_data_frame(sendtable, packed[0], packed[1]) 44 | assert_true(unpacked_frame is Dictionary) 45 | assert_eq(frame.size(), unpacked_frame.size()) 46 | for k in frame: 47 | assert_eq(frame[k], unpacked_frame[k], 'error in key %s' % k) 48 | -------------------------------------------------------------------------------- /addons/gut/diff_formatter.gd: -------------------------------------------------------------------------------- 1 | var _utils = load('res://addons/gut/utils.gd').get_instance() 2 | var _strutils = _utils.Strutils.new() 3 | const INDENT = ' ' 4 | var _max_to_display = 30 5 | const ABSOLUTE_MAX_DISPLAYED = 10000 6 | const UNLIMITED = -1 7 | 8 | 9 | func _single_diff(diff, depth=0): 10 | var to_return = "" 11 | var brackets = diff.get_brackets() 12 | 13 | if(brackets != null and !diff.are_equal): 14 | to_return = '' 15 | to_return += str(brackets.open, "\n", 16 | _strutils.indent_text(differences_to_s(diff.differences, depth), depth+1, INDENT), "\n", 17 | brackets.close) 18 | else: 19 | to_return = str(diff) 20 | 21 | return to_return 22 | 23 | 24 | func make_it(diff): 25 | var to_return = '' 26 | if(diff.are_equal): 27 | to_return = diff.summary 28 | else: 29 | if(_max_to_display == ABSOLUTE_MAX_DISPLAYED): 30 | to_return = str(diff.get_value_1(), ' != ', diff.get_value_2()) 31 | else: 32 | to_return = diff.get_short_summary() 33 | to_return += str("\n", _strutils.indent_text(_single_diff(diff, 0), 1, ' ')) 34 | return to_return 35 | 36 | 37 | func differences_to_s(differences, depth=0): 38 | var to_return = '' 39 | var keys = differences.keys() 40 | keys.sort() 41 | var limit = min(_max_to_display, differences.size()) 42 | 43 | for i in range(limit): 44 | var key = keys[i] 45 | to_return += str(key, ": ", _single_diff(differences[key], depth)) 46 | 47 | if(i != limit -1): 48 | to_return += "\n" 49 | 50 | if(differences.size() > _max_to_display): 51 | to_return += str("\n\n... ", differences.size() - _max_to_display, " more.") 52 | 53 | return to_return 54 | 55 | 56 | func get_max_to_display(): 57 | return _max_to_display 58 | 59 | 60 | func set_max_to_display(max_to_display): 61 | _max_to_display = max_to_display 62 | if(_max_to_display == UNLIMITED): 63 | _max_to_display = ABSOLUTE_MAX_DISPLAYED 64 | 65 | -------------------------------------------------------------------------------- /playground/pong/logic/pong.gd: -------------------------------------------------------------------------------- 1 | extends Node2D 2 | 3 | signal game_finished() 4 | 5 | const SCORE_TO_WIN = 10 6 | 7 | onready var synced = $synced 8 | 9 | onready var score_left_node = $ScoreLeft 10 | onready var score_right_node = $ScoreRight 11 | onready var winner_left = $WinnerLeft 12 | onready var winner_right = $WinnerRight 13 | 14 | func _ready(): 15 | synced.score_left = 0 16 | synced.score_right = 0 17 | 18 | # We're in an authoritative multiplayer game now, yay! 19 | # Keep Server as the master of all nodes. 20 | # Instead, tell paddles to listen to input from proper source. 21 | if SyncManager.is_server(): 22 | # For the server, give control of player2 to the other peer, player1 to self. 23 | $Player1/synced.belongs_to_peer_id = 0 24 | $Player2/synced.belongs_to_peer_id = get_tree().get_network_connected_peers()[0] 25 | print('SERVER: player2 belongs to another player %s' % get_tree().get_network_connected_peers()[0]) 26 | elif SyncManager.is_client(): 27 | # For the client, change control of player2 to self, leave player1 belonging to no one 28 | $Player2/synced.belongs_to_peer_id = 0 29 | print('CLIENT: player1 belongs to another player %s' % get_tree().get_network_connected_peers()[0]) 30 | else: 31 | # Clicked "Play Scene" in Editor?.. 32 | # Single player, no networkging enabled for some reason. 33 | # Let's control both players for the fun of it. 34 | $Player1/synced.belongs_to_peer_id = 0 35 | $Player2/synced.belongs_to_peer_id = 0 36 | print('OFFLINE: both players are controlled by local input') 37 | 38 | func _process(_delta): 39 | score_left_node.set_text(str(synced.score_left)) 40 | score_right_node.set_text(str(synced.score_right)) 41 | 42 | func update_score(add_to_left): 43 | if add_to_left: 44 | synced.score_left += 1 45 | score_left_node.set_text(str(synced.score_left)) 46 | else: 47 | synced.score_right += 1 48 | score_right_node.set_text(str(synced.score_right)) 49 | 50 | var game_ended = false 51 | if synced.score_left == SCORE_TO_WIN: 52 | winner_left.show() 53 | game_ended = true 54 | elif synced.score_right == SCORE_TO_WIN: 55 | winner_right.show() 56 | game_ended = true 57 | 58 | if game_ended: 59 | $ExitGame.show() 60 | $Ball.stop() 61 | 62 | func _on_exit_game_pressed(): 63 | emit_signal("game_finished") 64 | -------------------------------------------------------------------------------- /addons/gut/autofree.gd: -------------------------------------------------------------------------------- 1 | # ############################################################################## 2 | #(G)odot (U)nit (T)est class 3 | # 4 | # ############################################################################## 5 | # The MIT License (MIT) 6 | # ===================== 7 | # 8 | # Copyright (c) 2020 Tom "Butch" Wesley 9 | # 10 | # Permission is hereby granted, free of charge, to any person obtaining a copy 11 | # of this software and associated documentation files (the "Software"), to deal 12 | # in the Software without restriction, including without limitation the rights 13 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | # copies of the Software, and to permit persons to whom the Software is 15 | # furnished to do so, subject to the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be included in 18 | # all copies or substantial portions of the Software. 19 | # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | # THE SOFTWARE. 27 | # 28 | # ############################################################################## 29 | # Class used to keep track of objects to be freed and utilities to free them. 30 | # ############################################################################## 31 | var _to_free = [] 32 | var _to_queue_free = [] 33 | 34 | func add_free(thing): 35 | if(typeof(thing) == TYPE_OBJECT): 36 | if(!thing is Reference): 37 | _to_free.append(thing) 38 | 39 | func add_queue_free(thing): 40 | _to_queue_free.append(thing) 41 | 42 | func get_queue_free_count(): 43 | return _to_queue_free.size() 44 | 45 | func get_free_count(): 46 | return _to_free.size() 47 | 48 | func free_all(): 49 | for i in range(_to_free.size()): 50 | if(is_instance_valid(_to_free[i])): 51 | _to_free[i].free() 52 | _to_free.clear() 53 | 54 | for i in range(_to_queue_free.size()): 55 | if(is_instance_valid(_to_queue_free[i])): 56 | _to_queue_free[i].queue_free() 57 | _to_queue_free.clear() 58 | 59 | 60 | -------------------------------------------------------------------------------- /addons/gut/orphan_counter.gd: -------------------------------------------------------------------------------- 1 | # ############################################################################## 2 | #(G)odot (U)nit (T)est class 3 | # 4 | # ############################################################################## 5 | # The MIT License (MIT) 6 | # ===================== 7 | # 8 | # Copyright (c) 2020 Tom "Butch" Wesley 9 | # 10 | # Permission is hereby granted, free of charge, to any person obtaining a copy 11 | # of this software and associated documentation files (the "Software"), to deal 12 | # in the Software without restriction, including without limitation the rights 13 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | # copies of the Software, and to permit persons to whom the Software is 15 | # furnished to do so, subject to the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be included in 18 | # all copies or substantial portions of the Software. 19 | # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | # THE SOFTWARE. 27 | # 28 | # ############################################################################## 29 | # This is a utility for tracking changes in the orphan count. Each time 30 | # add_counter is called it adds/resets the value in the dictionary to the 31 | # current number of orphans. Each call to get_counter will return the change 32 | # in orphans since add_counter was last called. 33 | # ############################################################################## 34 | var _counters = {} 35 | 36 | func orphan_count(): 37 | return Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT) 38 | 39 | func add_counter(name): 40 | _counters[name] = orphan_count() 41 | 42 | # Returns the number of orphans created since add_counter was last called for 43 | # the name. Returns -1 to avoid blowing up with an invalid name but still 44 | # be somewhat visible that we've done something wrong. 45 | func get_counter(name): 46 | return orphan_count() - _counters[name] if _counters.has(name) else -1 47 | 48 | func print_orphans(name, lgr): 49 | var count = get_counter(name) 50 | 51 | if(count > 0): 52 | var o = 'orphan' 53 | if(count > 1): 54 | o = 'orphans' 55 | lgr.orphan(str(count, ' new ', o, '(', name, ').')) 56 | -------------------------------------------------------------------------------- /addons/synced/SyncInputFacade.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | class_name SyncInputFacade 3 | 4 | # Pretends to be like Godot's Input class, serving as a (limited) drop-in replacement. 5 | # When attached to local SyncPeer, uses last locally sampled input frame. 6 | # When attached to remote SyncPeer, uses data previously received via network. 7 | # (Remote SyncPeers only exist on Server.) 8 | 9 | #signal _input # !!! TODO not implemented 10 | 11 | func is_action_pressed(action: String)->bool: 12 | var result = _get_value(action) 13 | if not result: 14 | return false 15 | return result > 0 16 | 17 | func is_action_just_pressed(action: String)->bool: 18 | var value = _get_value(action) 19 | if not value or value == 0.0: 20 | return false 21 | return _get_value(action, -2) == 0.0 22 | 23 | func is_action_just_released(action: String)->bool: 24 | var value = _get_value(action) 25 | if not value or value != 0.0: 26 | return false 27 | return _get_value(action, -2) != 0.0 28 | 29 | func get_action_strength(action: String)->float: 30 | var result = _get_value(action) 31 | if not result: 32 | return 0.0 33 | return result 34 | 35 | func _get_value(action, input_id=-1): 36 | var peer = get_parent() 37 | if not peer: 38 | return null 39 | if input_id < 0: 40 | input_id = peer.input_id + 1 + input_id 41 | # If last input from this client was too long ago, return empty frame. 42 | # This stops extrapolating last known frame after certain number of steps. 43 | if input_id - peer.storage.last_input_id > SyncManager.input_prediction_max_frames: 44 | return null 45 | var frame = peer.storage.read(input_id) 46 | if not (action in frame): 47 | return null 48 | return frame[action] 49 | 50 | func get_peer_id(): 51 | var peer = get_parent() 52 | if not peer: 53 | return null 54 | return int(peer.name) 55 | 56 | func action_press(_action: String)->void: 57 | assert(false, 'SyncInputFacade->action_press() is not implemented') 58 | 59 | func action_release(_action: String)->void: 60 | assert(false, 'SyncInputFacade->action_release() is not implemented') 61 | 62 | # Fake SyncInputFacade to return when asked for unknown peer_unique_id 63 | class FakeInputFacade: 64 | signal _input 65 | func get_peer_id(): 66 | return null 67 | func is_action_pressed(_action: String)->bool: 68 | return false 69 | func is_action_just_pressed(_action: String)->bool: 70 | return false 71 | func is_action_just_released(_action: String)->bool: 72 | return false 73 | func get_action_strength(_action: String)->float: 74 | return 0.0 75 | func action_press(_action: String)->void: 76 | pass 77 | func action_release(_action: String)->void: 78 | pass 79 | func __z(): 80 | emit_signal("_input") 81 | -------------------------------------------------------------------------------- /addons/gut/spy.gd: -------------------------------------------------------------------------------- 1 | # { 2 | # instance_id_or_path1:{ 3 | # method1:[ [p1, p2], [p1, p2] ], 4 | # method2:[ [p1, p2], [p1, p2] ] 5 | # }, 6 | # instance_id_or_path1:{ 7 | # method1:[ [p1, p2], [p1, p2] ], 8 | # method2:[ [p1, p2], [p1, p2] ] 9 | # }, 10 | # } 11 | var _calls = {} 12 | var _utils = load('res://addons/gut/utils.gd').get_instance() 13 | var _lgr = _utils.get_logger() 14 | var _compare = _utils.Comparator.new() 15 | 16 | func _find_parameters(call_params, params_to_find): 17 | var found = false 18 | var idx = 0 19 | while(idx < call_params.size() and !found): 20 | var result = _compare.deep(call_params[idx], params_to_find) 21 | if(result.are_equal): 22 | found = true 23 | else: 24 | idx += 1 25 | return found 26 | 27 | func _get_params_as_string(params): 28 | var to_return = '' 29 | if(params == null): 30 | return '' 31 | 32 | for i in range(params.size()): 33 | if(params[i] == null): 34 | to_return += 'null' 35 | else: 36 | if(typeof(params[i]) == TYPE_STRING): 37 | to_return += str('"', params[i], '"') 38 | else: 39 | to_return += str(params[i]) 40 | if(i != params.size() -1): 41 | to_return += ', ' 42 | return to_return 43 | 44 | func add_call(variant, method_name, parameters=null): 45 | if(!_calls.has(variant)): 46 | _calls[variant] = {} 47 | 48 | if(!_calls[variant].has(method_name)): 49 | _calls[variant][method_name] = [] 50 | 51 | _calls[variant][method_name].append(parameters) 52 | 53 | func was_called(variant, method_name, parameters=null): 54 | var to_return = false 55 | if(_calls.has(variant) and _calls[variant].has(method_name)): 56 | if(parameters): 57 | to_return = _find_parameters(_calls[variant][method_name], parameters) 58 | else: 59 | to_return = true 60 | return to_return 61 | 62 | func get_call_parameters(variant, method_name, index=-1): 63 | var to_return = null 64 | var get_index = -1 65 | 66 | if(_calls.has(variant) and _calls[variant].has(method_name)): 67 | var call_size = _calls[variant][method_name].size() 68 | if(index == -1): 69 | # get the most recent call by default 70 | get_index = call_size -1 71 | else: 72 | get_index = index 73 | 74 | if(get_index < call_size): 75 | to_return = _calls[variant][method_name][get_index] 76 | else: 77 | _lgr.error(str('Specified index ', index, ' is outside range of the number of registered calls: ', call_size)) 78 | 79 | return to_return 80 | 81 | func call_count(instance, method_name, parameters=null): 82 | var to_return = 0 83 | 84 | if(was_called(instance, method_name)): 85 | if(parameters): 86 | for i in range(_calls[instance][method_name].size()): 87 | if(_calls[instance][method_name][i] == parameters): 88 | to_return += 1 89 | else: 90 | to_return = _calls[instance][method_name].size() 91 | return to_return 92 | 93 | func clear(): 94 | _calls = {} 95 | 96 | func get_call_list_as_string(instance): 97 | var to_return = '' 98 | if(_calls.has(instance)): 99 | for method in _calls[instance]: 100 | for i in range(_calls[instance][method].size()): 101 | to_return += str(method, '(', _get_params_as_string(_calls[instance][method][i]), ")\n") 102 | return to_return 103 | 104 | func get_logger(): 105 | return _lgr 106 | 107 | func set_logger(logger): 108 | _lgr = logger 109 | -------------------------------------------------------------------------------- /addons/gut/parameter_factory.gd: -------------------------------------------------------------------------------- 1 | # ############################################################################## 2 | #(G)odot (U)nit (T)est class 3 | # 4 | # ############################################################################## 5 | # The MIT License (MIT) 6 | # ===================== 7 | # 8 | # Copyright (c) 2020 Tom "Butch" Wesley 9 | # 10 | # Permission is hereby granted, free of charge, to any person obtaining a copy 11 | # of this software and associated documentation files (the "Software"), to deal 12 | # in the Software without restriction, including without limitation the rights 13 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | # copies of the Software, and to permit persons to whom the Software is 15 | # furnished to do so, subject to the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be included in 18 | # all copies or substantial portions of the Software. 19 | # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | # THE SOFTWARE. 27 | # 28 | # ############################################################################## 29 | # This is the home for all parameter creation helpers. These functions should 30 | # all return an array of values to be used as parameters for parameterized 31 | # tests. 32 | # ############################################################################## 33 | 34 | # ------------------------------------------------------------------------------ 35 | # Creates an array of dictionaries. It pairs up the names array with each set 36 | # of values in values. If more names than values are specified then the missing 37 | # values will be filled with nulls. If more values than names are specified 38 | # those values will be ignored. 39 | # 40 | # Example: 41 | # create_named_parameters(['a', 'b'], [[1, 2], ['one', 'two']]) returns 42 | # [{a:1, b:2}, {a:'one', b:'two'}] 43 | # 44 | # This allows you to increase readability of your parameterized tests: 45 | # var params = create_named_parameters(['a', 'b'], [[1, 2], ['one', 'two']]) 46 | # func test_foo(p = use_parameters(params)): 47 | # assert_eq(p.a, p.b) 48 | # 49 | # Parameters: 50 | # names: an array of names to be used as keys in the dictionaries 51 | # values: an array of arrays of values. 52 | # ------------------------------------------------------------------------------ 53 | static func named_parameters(names, values): 54 | var named = [] 55 | for i in range(values.size()): 56 | var entry = {} 57 | 58 | var parray = values[i] 59 | if(typeof(parray) != TYPE_ARRAY): 60 | parray = [values[i]] 61 | 62 | for j in range(names.size()): 63 | if(j >= parray.size()): 64 | entry[names[j]] = null 65 | else: 66 | entry[names[j]] = parray[j] 67 | named.append(entry) 68 | 69 | return named 70 | 71 | # Additional Helper Ideas 72 | # * File. IDK what it would look like. csv maybe. 73 | # * Random values within a range? 74 | # * All int values in a range or add an optioanal step. 75 | # * -------------------------------------------------------------------------------- /playground/pong/pong.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=7 format=2] 2 | 3 | [ext_resource path="res://playground/pong/logic/pong.gd" type="Script" id=1] 4 | [ext_resource path="res://playground/pong/separator.png" type="Texture" id=2] 5 | [ext_resource path="res://playground/pong/paddle.tscn" type="PackedScene" id=3] 6 | [ext_resource path="res://playground/pong/ball.tscn" type="PackedScene" id=4] 7 | [ext_resource path="res://addons/synced/SyncedProperty.gd" type="Script" id=5] 8 | [ext_resource path="res://addons/synced/Synced.gd" type="Script" id=6] 9 | 10 | [node name="Pong" type="Node2D"] 11 | script = ExtResource( 1 ) 12 | 13 | [node name="ColorRect" type="ColorRect" parent="."] 14 | margin_right = 640.0 15 | margin_bottom = 400.0 16 | color = Color( 0.141176, 0.152941, 0.164706, 1 ) 17 | __meta__ = { 18 | "_edit_use_anchors_": false 19 | } 20 | 21 | [node name="Separator" type="Sprite" parent="."] 22 | position = Vector2( 320, 200 ) 23 | texture = ExtResource( 2 ) 24 | 25 | [node name="Player1" parent="." instance=ExtResource( 3 )] 26 | modulate = Color( 0, 1, 1, 1 ) 27 | position = Vector2( 32.49, 188.622 ) 28 | left = true 29 | 30 | [node name="Player2" parent="." instance=ExtResource( 3 )] 31 | modulate = Color( 1, 0, 1, 1 ) 32 | position = Vector2( 608.88, 188.622 ) 33 | 34 | [node name="Ball" parent="." instance=ExtResource( 4 )] 35 | position = Vector2( 320.387, 189.525 ) 36 | 37 | [node name="ScoreLeft" type="Label" parent="."] 38 | margin_left = 240.0 39 | margin_top = 10.0 40 | margin_right = 280.0 41 | margin_bottom = 30.0 42 | size_flags_horizontal = 2 43 | size_flags_vertical = 0 44 | text = "0" 45 | align = 1 46 | __meta__ = { 47 | "_edit_use_anchors_": false 48 | } 49 | 50 | [node name="ScoreRight" type="Label" parent="."] 51 | margin_left = 360.0 52 | margin_top = 10.0 53 | margin_right = 400.0 54 | margin_bottom = 30.0 55 | size_flags_horizontal = 2 56 | size_flags_vertical = 0 57 | text = "0" 58 | align = 1 59 | 60 | [node name="WinnerLeft" type="Label" parent="."] 61 | visible = false 62 | margin_left = 190.0 63 | margin_top = 170.0 64 | margin_right = 267.0 65 | margin_bottom = 184.0 66 | size_flags_horizontal = 2 67 | size_flags_vertical = 0 68 | text = "The Winner!" 69 | 70 | [node name="WinnerRight" type="Label" parent="."] 71 | visible = false 72 | margin_left = 380.0 73 | margin_top = 170.0 74 | margin_right = 457.0 75 | margin_bottom = 184.0 76 | size_flags_horizontal = 2 77 | size_flags_vertical = 0 78 | text = "The Winner!" 79 | 80 | [node name="ExitGame" type="Button" parent="."] 81 | visible = false 82 | margin_left = 280.0 83 | margin_top = 340.0 84 | margin_right = 360.0 85 | margin_bottom = 360.0 86 | size_flags_horizontal = 2 87 | size_flags_vertical = 2 88 | text = "Exit Game" 89 | 90 | [node name="Camera2D" type="Camera2D" parent="."] 91 | offset = Vector2( 320, 200 ) 92 | current = true 93 | 94 | [node name="synced" type="Node" parent="."] 95 | script = ExtResource( 6 ) 96 | 97 | [node name="score_left" type="Node" parent="synced"] 98 | script = ExtResource( 5 ) 99 | sync_strategy = 1 100 | 101 | [node name="score_right" type="Node" parent="synced"] 102 | script = ExtResource( 5 ) 103 | sync_strategy = 1 104 | meta = { 105 | 106 | } 107 | [connection signal="pressed" from="ExitGame" to="." method="_on_exit_game_pressed"] 108 | 109 | [editable path="Player1"] 110 | 111 | [editable path="Player2"] 112 | -------------------------------------------------------------------------------- /playground/pong/lobby.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=2] 2 | 3 | [ext_resource path="res://playground/pong/logic/lobby.gd" type="Script" id=1] 4 | 5 | [node name="Lobby" type="Control"] 6 | anchor_left = 0.5 7 | anchor_top = 0.5 8 | anchor_right = 0.5 9 | anchor_bottom = 0.5 10 | margin_left = -320.0 11 | margin_top = -200.0 12 | margin_right = 320.0 13 | margin_bottom = 200.0 14 | size_flags_horizontal = 2 15 | size_flags_vertical = 2 16 | __meta__ = { 17 | "_edit_use_anchors_": false 18 | } 19 | 20 | [node name="Title" type="Label" parent="."] 21 | margin_left = 210.0 22 | margin_top = 40.0 23 | margin_right = 430.0 24 | margin_bottom = 80.0 25 | size_flags_horizontal = 2 26 | size_flags_vertical = 0 27 | text = "Multiplayer Pong" 28 | align = 1 29 | valign = 1 30 | 31 | [node name="LobbyPanel" type="Panel" parent="."] 32 | margin_left = 210.0 33 | margin_top = 160.0 34 | margin_right = 430.0 35 | margin_bottom = 270.0 36 | size_flags_horizontal = 2 37 | size_flags_vertical = 2 38 | script = ExtResource( 1 ) 39 | 40 | [node name="AddressLabel" type="Label" parent="LobbyPanel"] 41 | margin_left = 10.0 42 | margin_top = 10.0 43 | margin_right = 62.0 44 | margin_bottom = 24.0 45 | size_flags_horizontal = 2 46 | size_flags_vertical = 0 47 | text = "Address" 48 | 49 | [node name="Address" type="LineEdit" parent="LobbyPanel"] 50 | margin_left = 10.0 51 | margin_top = 30.0 52 | margin_right = 210.0 53 | margin_bottom = 54.0 54 | size_flags_horizontal = 2 55 | size_flags_vertical = 2 56 | text = "127.0.0.1" 57 | 58 | [node name="HostButton" type="Button" parent="LobbyPanel"] 59 | margin_left = 10.0 60 | margin_top = 60.0 61 | margin_right = 90.0 62 | margin_bottom = 80.0 63 | size_flags_horizontal = 2 64 | size_flags_vertical = 2 65 | text = "Host" 66 | 67 | [node name="JoinButton" type="Button" parent="LobbyPanel"] 68 | margin_left = 130.0 69 | margin_top = 60.0 70 | margin_right = 210.0 71 | margin_bottom = 80.0 72 | size_flags_horizontal = 2 73 | size_flags_vertical = 2 74 | text = "Join" 75 | 76 | [node name="StatusOk" type="Label" parent="LobbyPanel"] 77 | margin_left = 10.0 78 | margin_top = 90.0 79 | margin_right = 210.0 80 | margin_bottom = 104.0 81 | size_flags_horizontal = 2 82 | size_flags_vertical = 0 83 | custom_colors/font_color = Color( 0, 1, 0.015625, 1 ) 84 | align = 1 85 | 86 | [node name="StatusFail" type="Label" parent="LobbyPanel"] 87 | margin_left = 10.0 88 | margin_top = 90.0 89 | margin_right = 210.0 90 | margin_bottom = 104.0 91 | size_flags_horizontal = 2 92 | size_flags_vertical = 0 93 | custom_colors/font_color = Color( 1, 0, 0, 1 ) 94 | align = 1 95 | 96 | [node name="PortForward" type="Label" parent="LobbyPanel"] 97 | visible = false 98 | margin_left = -128.0 99 | margin_top = 136.0 100 | margin_right = 124.0 101 | margin_bottom = 184.0 102 | custom_constants/line_spacing = 6 103 | text = "If you want non-LAN clients to connect, 104 | make sure the port 8910 in UDP 105 | is forwarded on your router." 106 | align = 1 107 | __meta__ = { 108 | "_edit_use_anchors_": false 109 | } 110 | 111 | [node name="FindPublicIP" type="LinkButton" parent="LobbyPanel"] 112 | visible = false 113 | margin_left = 155.0 114 | margin_top = 152.0 115 | margin_right = 328.0 116 | margin_bottom = 166.0 117 | text = "Find your public IP address" 118 | __meta__ = { 119 | "_edit_use_anchors_": false 120 | } 121 | 122 | [connection signal="pressed" from="LobbyPanel/HostButton" to="LobbyPanel" method="_on_host_pressed"] 123 | [connection signal="pressed" from="LobbyPanel/JoinButton" to="LobbyPanel" method="_on_join_pressed"] 124 | [connection signal="pressed" from="LobbyPanel/FindPublicIP" to="LobbyPanel" method="_on_find_public_ip_pressed"] 125 | -------------------------------------------------------------------------------- /addons/gut/comparator.gd: -------------------------------------------------------------------------------- 1 | var _utils = load('res://addons/gut/utils.gd').get_instance() 2 | var _strutils = _utils.Strutils.new() 3 | var _max_length = 100 4 | var _should_compare_int_to_float = true 5 | 6 | const MISSING = '|__missing__gut__compare__value__|' 7 | const DICTIONARY_DISCLAIMER = 'Dictionaries are compared-by-ref. See assert_eq in wiki.' 8 | 9 | func _cannot_comapre_text(v1, v2): 10 | return str('Cannot compare ', _strutils.types[typeof(v1)], ' with ', 11 | _strutils.types[typeof(v2)], '.') 12 | 13 | func _make_missing_string(text): 14 | return '' 15 | 16 | func _create_missing_result(v1, v2, text): 17 | var to_return = null 18 | var v1_str = format_value(v1) 19 | var v2_str = format_value(v2) 20 | 21 | if(typeof(v1) == TYPE_STRING and v1 == MISSING): 22 | v1_str = _make_missing_string(text) 23 | to_return = _utils.CompareResult.new() 24 | elif(typeof(v2) == TYPE_STRING and v2 == MISSING): 25 | v2_str = _make_missing_string(text) 26 | to_return = _utils.CompareResult.new() 27 | 28 | if(to_return != null): 29 | to_return.summary = str(v1_str, ' != ', v2_str) 30 | to_return.are_equal = false 31 | 32 | return to_return 33 | 34 | 35 | func simple(v1, v2, missing_string=''): 36 | var missing_result = _create_missing_result(v1, v2, missing_string) 37 | if(missing_result != null): 38 | return missing_result 39 | 40 | var result = _utils.CompareResult.new() 41 | var cmp_str = null 42 | var extra = '' 43 | 44 | if(_should_compare_int_to_float and [2, 3].has(typeof(v1)) and [2, 3].has(typeof(v2))): 45 | result.are_equal = v1 == v2 46 | 47 | elif(_utils.are_datatypes_same(v1, v2)): 48 | result.are_equal = v1 == v2 49 | if(typeof(v1) == TYPE_DICTIONARY): 50 | if(result.are_equal): 51 | extra = '. Same dictionary ref. ' 52 | else: 53 | extra = '. Different dictionary refs. ' 54 | extra += DICTIONARY_DISCLAIMER 55 | 56 | if(typeof(v1) == TYPE_ARRAY): 57 | var array_result = _utils.DiffTool.new(v1, v2, _utils.DIFF.SHALLOW) 58 | result.summary = array_result.get_short_summary() 59 | if(!array_result.are_equal()): 60 | extra = ".\n" + array_result.get_short_summary() 61 | 62 | else: 63 | cmp_str = '!=' 64 | result.are_equal = false 65 | extra = str('. ', _cannot_comapre_text(v1, v2)) 66 | 67 | cmp_str = get_compare_symbol(result.are_equal) 68 | if(typeof(v1) != TYPE_ARRAY): 69 | result.summary = str(format_value(v1), ' ', cmp_str, ' ', format_value(v2), extra) 70 | 71 | return result 72 | 73 | 74 | func shallow(v1, v2): 75 | var result = null 76 | 77 | if(_utils.are_datatypes_same(v1, v2)): 78 | if(typeof(v1) in [TYPE_ARRAY, TYPE_DICTIONARY]): 79 | result = _utils.DiffTool.new(v1, v2, _utils.DIFF.SHALLOW) 80 | else: 81 | result = simple(v1, v2) 82 | else: 83 | result = simple(v1, v2) 84 | 85 | return result 86 | 87 | 88 | func deep(v1, v2): 89 | var result = null 90 | 91 | if(_utils.are_datatypes_same(v1, v2)): 92 | if(typeof(v1) in [TYPE_ARRAY, TYPE_DICTIONARY]): 93 | result = _utils.DiffTool.new(v1, v2, _utils.DIFF.DEEP) 94 | else: 95 | result = simple(v1, v2) 96 | else: 97 | result = simple(v1, v2) 98 | 99 | return result 100 | 101 | 102 | func format_value(val, max_val_length=_max_length): 103 | return _strutils.truncate_string(_strutils.type2str(val), max_val_length) 104 | 105 | 106 | func compare(v1, v2, diff_type=_utils.DIFF.SIMPLE): 107 | var result = null 108 | if(diff_type == _utils.DIFF.SIMPLE): 109 | result = simple(v1, v2) 110 | elif(diff_type == _utils.DIFF.SHALLOW): 111 | result = shallow(v1, v2) 112 | elif(diff_type == _utils.DIFF.DEEP): 113 | result = deep(v1, v2) 114 | 115 | return result 116 | 117 | 118 | func get_should_compare_int_to_float(): 119 | return _should_compare_int_to_float 120 | 121 | 122 | func set_should_compare_int_to_float(should_compare_int_float): 123 | _should_compare_int_to_float = should_compare_int_float 124 | 125 | 126 | func get_compare_symbol(is_equal): 127 | if(is_equal): 128 | return '==' 129 | else: 130 | return '!=' 131 | -------------------------------------------------------------------------------- /playground/pong/logic/lobby.gd: -------------------------------------------------------------------------------- 1 | extends Control 2 | 3 | # Default game server port. Can be any number between 1024 and 49151. 4 | # Not on the list of registered or common ports as of November 2020: 5 | # https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers 6 | const DEFAULT_PORT = 8910 7 | 8 | onready var address = $Address 9 | onready var host_button = $HostButton 10 | onready var join_button = $JoinButton 11 | onready var status_ok = $StatusOk 12 | onready var status_fail = $StatusFail 13 | onready var port_forward_label = $PortForward 14 | onready var find_public_ip_button = $FindPublicIP 15 | 16 | var peer = null 17 | 18 | func _ready(): 19 | # Connect all the callbacks related to networking. 20 | get_tree().connect("network_peer_connected", self, "_player_connected") 21 | get_tree().connect("network_peer_disconnected", self, "_player_disconnected") 22 | get_tree().connect("connected_to_server", self, "_connected_ok") 23 | get_tree().connect("connection_failed", self, "_connected_fail") 24 | get_tree().connect("server_disconnected", self, "_server_disconnected") 25 | 26 | #### Network callbacks from SceneTree #### 27 | 28 | # Callback from SceneTree. 29 | func _player_connected(_id): 30 | # Someone connected, start the game! 31 | var pong = load("res://playground/pong/pong.tscn").instance() 32 | # Connect deferred so we can safely erase it from the callback. 33 | pong.connect("game_finished", self, "_end_game", [], CONNECT_DEFERRED) 34 | 35 | get_tree().get_root().add_child(pong) 36 | hide() 37 | 38 | 39 | func _player_disconnected(_id): 40 | if get_tree().is_network_server(): 41 | _end_game("Client disconnected") 42 | else: 43 | _end_game("Server disconnected") 44 | 45 | 46 | # Callback from SceneTree, only for clients (not server). 47 | func _connected_ok(): 48 | pass # This function is not needed for this project. 49 | 50 | 51 | # Callback from SceneTree, only for clients (not server). 52 | func _connected_fail(): 53 | _set_status("Couldn't connect", false) 54 | 55 | get_tree().set_network_peer(null) # Remove peer. 56 | host_button.set_disabled(false) 57 | join_button.set_disabled(false) 58 | 59 | 60 | func _server_disconnected(): 61 | _end_game("Server disconnected") 62 | 63 | ##### Game creation functions ###### 64 | 65 | func _end_game(with_error = ""): 66 | if has_node("/root/Pong"): 67 | # Erase immediately, otherwise network might show 68 | # errors (this is why we connected deferred above). 69 | get_node("/root/Pong").free() 70 | show() 71 | 72 | get_tree().set_network_peer(null) # Remove peer. 73 | host_button.set_disabled(false) 74 | join_button.set_disabled(false) 75 | 76 | _set_status(with_error, false) 77 | 78 | 79 | func _set_status(text, isok): 80 | # Simple way to show status. 81 | if isok: 82 | status_ok.set_text(text) 83 | status_fail.set_text("") 84 | else: 85 | status_ok.set_text("") 86 | status_fail.set_text(text) 87 | 88 | 89 | func _on_host_pressed(): 90 | peer = NetworkedMultiplayerENet.new() 91 | peer.set_compression_mode(NetworkedMultiplayerENet.COMPRESS_RANGE_CODER) 92 | var err = peer.create_server(DEFAULT_PORT, 1) # Maximum of 1 peer, since it's a 2-player game. 93 | if err != OK: 94 | # Is another server running? 95 | _set_status("Can't host, address in use.",false) 96 | return 97 | 98 | get_tree().set_network_peer(peer) 99 | host_button.set_disabled(true) 100 | join_button.set_disabled(true) 101 | _set_status("Waiting for player...", true) 102 | 103 | # Only show hosting instructions when relevant. 104 | port_forward_label.visible = true 105 | find_public_ip_button.visible = true 106 | 107 | 108 | func _on_join_pressed(): 109 | var ip = address.get_text() 110 | if not ip.is_valid_ip_address(): 111 | _set_status("IP address is invalid", false) 112 | return 113 | 114 | peer = NetworkedMultiplayerENet.new() 115 | peer.set_compression_mode(NetworkedMultiplayerENet.COMPRESS_RANGE_CODER) 116 | peer.create_client(ip, DEFAULT_PORT) 117 | get_tree().set_network_peer(peer) 118 | 119 | _set_status("Connecting...", true) 120 | 121 | 122 | func _on_find_public_ip_pressed(): 123 | OS.shell_open("https://icanhazip.com/") 124 | -------------------------------------------------------------------------------- /project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=4 10 | 11 | _global_script_classes=[ { 12 | "base": "Node", 13 | "class": "Aligned", 14 | "language": "GDScript", 15 | "path": "res://addons/synced/Aligned.gd" 16 | }, { 17 | "base": "Node", 18 | "class": "SyncInputFacade", 19 | "language": "GDScript", 20 | "path": "res://addons/synced/SyncInputFacade.gd" 21 | }, { 22 | "base": "Node", 23 | "class": "SyncPeer", 24 | "language": "GDScript", 25 | "path": "res://addons/synced/SyncPeer.gd" 26 | }, { 27 | "base": "Reference", 28 | "class": "SyncSequence", 29 | "language": "GDScript", 30 | "path": "res://addons/synced/SyncSequence.gd" 31 | }, { 32 | "base": "Node", 33 | "class": "Synced", 34 | "language": "GDScript", 35 | "path": "res://addons/synced/Synced.gd" 36 | }, { 37 | "base": "Node", 38 | "class": "SyncedProperty", 39 | "language": "GDScript", 40 | "path": "res://addons/synced/SyncedProperty.gd" 41 | } ] 42 | _global_script_class_icons={ 43 | "Aligned": "", 44 | "SyncInputFacade": "", 45 | "SyncPeer": "", 46 | "SyncSequence": "", 47 | "Synced": "", 48 | "SyncedProperty": "" 49 | } 50 | 51 | [application] 52 | 53 | config/name="Godot Synced Networking Framework Demo" 54 | run/main_scene="res://playground/pong/lobby.tscn" 55 | config/icon="res://icon.png" 56 | 57 | [autoload] 58 | 59 | SyncManager="*res://addons/synced/SyncManager.gd" 60 | 61 | [debug] 62 | 63 | gdscript/warnings/return_value_discarded=false 64 | 65 | [display] 66 | 67 | window/size/width=640 68 | window/size/height=400 69 | window/dpi/allow_hidpi=true 70 | window/stretch/mode="2d" 71 | window/stretch/aspect="expand" 72 | stretch_2d=true 73 | 74 | [editor_plugins] 75 | 76 | enabled=PoolStringArray( "gut" ) 77 | 78 | [input] 79 | 80 | move_down={ 81 | "deadzone": 0.5, 82 | "events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777234,"physical_scancode":0,"unicode":0,"echo":false,"script":null) 83 | , Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":13,"pressure":0.0,"pressed":false,"script":null) 84 | , Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":1,"axis_value":1.0,"script":null) 85 | , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":90,"physical_scancode":0,"unicode":0,"echo":false,"script":null) 86 | , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":83,"physical_scancode":0,"unicode":0,"echo":false,"script":null) 87 | ] 88 | } 89 | move_up={ 90 | "deadzone": 0.5, 91 | "events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777232,"physical_scancode":0,"unicode":0,"echo":false,"script":null) 92 | , Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":12,"pressure":0.0,"pressed":false,"script":null) 93 | , Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":1,"axis_value":-1.0,"script":null) 94 | , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":65,"physical_scancode":0,"unicode":0,"echo":false,"script":null) 95 | , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":87,"physical_scancode":0,"unicode":0,"echo":false,"script":null) 96 | ] 97 | } 98 | 99 | [network] 100 | 101 | limits/debugger_stdout/max_chars_per_second=40960 102 | limits/debugger_stdout/max_messages_per_frame=40 103 | 104 | [rendering] 105 | 106 | environment/default_environment="res://default_env.tres" 107 | -------------------------------------------------------------------------------- /addons/gut/diff_tool.gd: -------------------------------------------------------------------------------- 1 | extends 'res://addons/gut/compare_result.gd' 2 | const INDENT = ' ' 3 | enum { 4 | DEEP, 5 | SHALLOW, 6 | SIMPLE 7 | } 8 | 9 | var _utils = load('res://addons/gut/utils.gd').get_instance() 10 | var _strutils = _utils.Strutils.new() 11 | var _compare = _utils.Comparator.new() 12 | var DiffTool = load('res://addons/gut/diff_tool.gd') 13 | 14 | var _value_1 = null 15 | var _value_2 = null 16 | var _total_count = 0 17 | var _diff_type = null 18 | var _brackets = null 19 | var _valid = true 20 | var _desc_things = 'somethings' 21 | 22 | # -------- comapre_result.gd "interface" --------------------- 23 | func set_are_equal(val): 24 | _block_set('are_equal', val) 25 | 26 | func get_are_equal(): 27 | return are_equal() 28 | 29 | func set_summary(val): 30 | _block_set('summary', val) 31 | 32 | func get_summary(): 33 | return summarize() 34 | 35 | func get_different_count(): 36 | return differences.size() 37 | 38 | func get_total_count(): 39 | return _total_count 40 | 41 | func get_short_summary(): 42 | var text = str(_strutils.truncate_string(str(_value_1), 50), 43 | ' ', _compare.get_compare_symbol(are_equal()), ' ', 44 | _strutils.truncate_string(str(_value_2), 50)) 45 | if(!are_equal()): 46 | text += str(' ', get_different_count(), ' of ', get_total_count(), 47 | ' ', _desc_things, ' do not match.') 48 | return text 49 | 50 | func get_brackets(): 51 | return _brackets 52 | # -------- comapre_result.gd "interface" --------------------- 53 | 54 | 55 | func _invalidate(): 56 | _valid = false 57 | differences = null 58 | 59 | 60 | func _init(v1, v2, diff_type=DEEP): 61 | _value_1 = v1 62 | _value_2 = v2 63 | _diff_type = diff_type 64 | _compare.set_should_compare_int_to_float(false) 65 | _find_differences(_value_1, _value_2) 66 | 67 | 68 | func _find_differences(v1, v2): 69 | if(_utils.are_datatypes_same(v1, v2)): 70 | if(typeof(v1) == TYPE_ARRAY): 71 | _brackets = {'open':'[', 'close':']'} 72 | _desc_things = 'indexes' 73 | _diff_array(v1, v2) 74 | elif(typeof(v2) == TYPE_DICTIONARY): 75 | _brackets = {'open':'{', 'close':'}'} 76 | _desc_things = 'keys' 77 | _diff_dictionary(v1, v2) 78 | else: 79 | _invalidate() 80 | _utils.get_logger().error('Only Arrays and Dictionaries are supported.') 81 | else: 82 | _invalidate() 83 | _utils.get_logger().error('Only Arrays and Dictionaries are supported.') 84 | 85 | 86 | func _diff_array(a1, a2): 87 | _total_count = max(a1.size(), a2.size()) 88 | for i in range(a1.size()): 89 | var result = null 90 | if(i < a2.size()): 91 | if(_diff_type == DEEP): 92 | result = _compare.deep(a1[i], a2[i]) 93 | else: 94 | result = _compare.simple(a1[i], a2[i]) 95 | else: 96 | result = _compare.simple(a1[i], _compare.MISSING, 'index') 97 | 98 | if(!result.are_equal): 99 | differences[i] = result 100 | 101 | if(a1.size() < a2.size()): 102 | for i in range(a1.size(), a2.size()): 103 | differences[i] = _compare.simple(_compare.MISSING, a2[i], 'index') 104 | 105 | 106 | func _diff_dictionary(d1, d2): 107 | var d1_keys = d1.keys() 108 | var d2_keys = d2.keys() 109 | 110 | # Process all the keys in d1 111 | _total_count += d1_keys.size() 112 | for key in d1_keys: 113 | if(!d2.has(key)): 114 | differences[key] = _compare.simple(d1[key], _compare.MISSING, 'key') 115 | else: 116 | d2_keys.remove(d2_keys.find(key)) 117 | 118 | var result = null 119 | if(_diff_type == DEEP): 120 | result = _compare.deep(d1[key], d2[key]) 121 | else: 122 | result = _compare.simple(d1[key], d2[key]) 123 | 124 | if(!result.are_equal): 125 | differences[key] = result 126 | 127 | # Process all the keys in d2 that didn't exist in d1 128 | _total_count += d2_keys.size() 129 | for i in range(d2_keys.size()): 130 | differences[d2_keys[i]] = _compare.simple(_compare.MISSING, d2[d2_keys[i]], 'key') 131 | 132 | 133 | func summarize(): 134 | var summary = '' 135 | 136 | if(are_equal()): 137 | summary = get_short_summary() 138 | else: 139 | var formatter = load('res://addons/gut/diff_formatter.gd').new() 140 | formatter.set_max_to_display(max_differences) 141 | summary = formatter.make_it(self) 142 | 143 | return summary 144 | 145 | 146 | func are_equal(): 147 | if(!_valid): 148 | return null 149 | else: 150 | return differences.size() == 0 151 | 152 | 153 | func get_diff_type(): 154 | return _diff_type 155 | 156 | 157 | func get_value_1(): 158 | return _value_1 159 | 160 | 161 | func get_value_2(): 162 | return _value_2 -------------------------------------------------------------------------------- /addons/gut/printers.gd: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # Interface and some basic functionality for all printers. 3 | # ------------------------------------------------------------------------------ 4 | class Printer: 5 | var _format_enabled = true 6 | var _disabled = false 7 | var _printer_name = 'NOT SET' 8 | var _show_name = false # used for debugging, set manually 9 | 10 | func get_format_enabled(): 11 | return _format_enabled 12 | 13 | func set_format_enabled(format_enabled): 14 | _format_enabled = format_enabled 15 | 16 | func send(text, fmt=null): 17 | if(_disabled): 18 | return 19 | 20 | var formatted = text 21 | if(fmt != null and _format_enabled): 22 | formatted = format_text(text, fmt) 23 | 24 | if(_show_name): 25 | formatted = str('(', _printer_name, ')') + formatted 26 | 27 | _output(formatted) 28 | 29 | func get_disabled(): 30 | return _disabled 31 | 32 | func set_disabled(disabled): 33 | _disabled = disabled 34 | 35 | # -------------------- 36 | # Virtual Methods (some have some default behavior) 37 | # -------------------- 38 | func _output(text): 39 | pass 40 | 41 | func format_text(text, fmt): 42 | return text 43 | 44 | # ------------------------------------------------------------------------------ 45 | # Responsible for sending text to a GUT gui. 46 | # ------------------------------------------------------------------------------ 47 | class GutGuiPrinter: 48 | extends Printer 49 | var _gut = null 50 | 51 | var _colors = { 52 | red = Color.red, 53 | yellow = Color.yellow, 54 | green = Color.green 55 | } 56 | 57 | func _init(): 58 | _printer_name = 'gui' 59 | 60 | func _wrap_with_tag(text, tag): 61 | return str('[', tag, ']', text, '[/', tag, ']') 62 | 63 | func _color_text(text, c_word): 64 | return '[color=' + c_word + ']' + text + '[/color]' 65 | 66 | func format_text(text, fmt): 67 | var box = _gut.get_gui().get_text_box() 68 | 69 | if(fmt == 'bold'): 70 | box.push_bold() 71 | elif(fmt == 'underline'): 72 | box.push_underline() 73 | elif(_colors.has(fmt)): 74 | box.push_color(_colors[fmt]) 75 | else: 76 | # just pushing something to pop. 77 | box.push_normal() 78 | 79 | box.add_text(text) 80 | box.pop() 81 | 82 | return '' 83 | 84 | func _output(text): 85 | _gut.get_gui().get_text_box().add_text(text) 86 | 87 | func get_gut(): 88 | return _gut 89 | 90 | func set_gut(gut): 91 | _gut = gut 92 | 93 | # This can be very very slow when the box has a lot of text. 94 | func clear_line(): 95 | var box = _gut.get_gui().get_text_box() 96 | box.remove_line(box.get_line_count() - 1) 97 | box.update() 98 | 99 | # ------------------------------------------------------------------------------ 100 | # This AND TerminalPrinter should not be enabled at the same time since it will 101 | # result in duplicate output. printraw does not print to the console so i had 102 | # to make another one. 103 | # ------------------------------------------------------------------------------ 104 | class ConsolePrinter: 105 | extends Printer 106 | var _buffer = '' 107 | 108 | func _init(): 109 | _printer_name = 'console' 110 | 111 | # suppresses output until it encounters a newline to keep things 112 | # inline as much as possible. 113 | func _output(text): 114 | if(text.ends_with("\n")): 115 | print(_buffer + text.left(text.length() -1)) 116 | _buffer = '' 117 | else: 118 | _buffer += text 119 | 120 | # ------------------------------------------------------------------------------ 121 | # Prints text to terminal, formats some words. 122 | # ------------------------------------------------------------------------------ 123 | class TerminalPrinter: 124 | extends Printer 125 | 126 | var escape = PoolByteArray([0x1b]).get_string_from_ascii() 127 | var cmd_colors = { 128 | red = escape + '[31m', 129 | yellow = escape + '[33m', 130 | green = escape + '[32m', 131 | 132 | underline = escape + '[4m', 133 | bold = escape + '[1m', 134 | 135 | default = escape + '[0m', 136 | 137 | clear_line = escape + '[2K' 138 | } 139 | 140 | func _init(): 141 | _printer_name = 'terminal' 142 | 143 | func _output(text): 144 | # Note, printraw does not print to the console. 145 | printraw(text) 146 | 147 | func format_text(text, fmt): 148 | return cmd_colors[fmt] + text + cmd_colors.default 149 | 150 | func clear_line(): 151 | send(cmd_colors.clear_line) 152 | 153 | func back(n): 154 | send(escape + str('[', n, 'D')) 155 | 156 | func forward(n): 157 | send(escape + str('[', n, 'C')) 158 | -------------------------------------------------------------------------------- /addons/gut/UserFileViewer.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=2] 2 | 3 | [ext_resource path="res://addons/gut/UserFileViewer.gd" type="Script" id=1] 4 | 5 | [node name="UserFileViewer" type="WindowDialog"] 6 | margin_top = 20.0 7 | margin_right = 800.0 8 | margin_bottom = 450.0 9 | rect_min_size = Vector2( 800, 180 ) 10 | popup_exclusive = true 11 | window_title = "View File" 12 | resizable = true 13 | script = ExtResource( 1 ) 14 | __meta__ = { 15 | "_edit_use_anchors_": false 16 | } 17 | 18 | [node name="FileDialog" type="FileDialog" parent="."] 19 | margin_right = 416.0 20 | margin_bottom = 184.0 21 | rect_min_size = Vector2( 400, 140 ) 22 | rect_scale = Vector2( 2, 2 ) 23 | popup_exclusive = true 24 | window_title = "Open a File" 25 | resizable = true 26 | mode = 0 27 | access = 1 28 | show_hidden_files = true 29 | current_dir = "user://" 30 | current_path = "user://" 31 | __meta__ = { 32 | "_edit_use_anchors_": false 33 | } 34 | 35 | [node name="TextDisplay" type="ColorRect" parent="."] 36 | anchor_right = 1.0 37 | anchor_bottom = 1.0 38 | margin_left = 8.0 39 | margin_right = -10.0 40 | margin_bottom = -65.0 41 | color = Color( 0.2, 0.188235, 0.188235, 1 ) 42 | __meta__ = { 43 | "_edit_use_anchors_": false 44 | } 45 | 46 | [node name="RichTextLabel" type="RichTextLabel" parent="TextDisplay"] 47 | anchor_right = 1.0 48 | anchor_bottom = 1.0 49 | focus_mode = 2 50 | 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. 51 | 52 | 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. 53 | 54 | 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." 55 | selection_enabled = true 56 | __meta__ = { 57 | "_edit_use_anchors_": false 58 | } 59 | 60 | [node name="OpenFile" type="Button" parent="."] 61 | anchor_left = 1.0 62 | anchor_top = 1.0 63 | anchor_right = 1.0 64 | anchor_bottom = 1.0 65 | margin_left = -158.0 66 | margin_top = -50.0 67 | margin_right = -84.0 68 | margin_bottom = -30.0 69 | rect_scale = Vector2( 2, 2 ) 70 | text = "Open File" 71 | 72 | [node name="Home" type="Button" parent="."] 73 | anchor_left = 1.0 74 | anchor_top = 1.0 75 | anchor_right = 1.0 76 | anchor_bottom = 1.0 77 | margin_left = -478.0 78 | margin_top = -50.0 79 | margin_right = -404.0 80 | margin_bottom = -30.0 81 | rect_scale = Vector2( 2, 2 ) 82 | text = "Home" 83 | __meta__ = { 84 | "_edit_use_anchors_": false 85 | } 86 | 87 | [node name="Copy" type="Button" parent="."] 88 | anchor_top = 1.0 89 | anchor_bottom = 1.0 90 | margin_left = 160.0 91 | margin_top = -50.0 92 | margin_right = 234.0 93 | margin_bottom = -30.0 94 | rect_scale = Vector2( 2, 2 ) 95 | text = "Copy" 96 | __meta__ = { 97 | "_edit_use_anchors_": false 98 | } 99 | 100 | [node name="End" type="Button" parent="."] 101 | anchor_left = 1.0 102 | anchor_top = 1.0 103 | anchor_right = 1.0 104 | anchor_bottom = 1.0 105 | margin_left = -318.0 106 | margin_top = -50.0 107 | margin_right = -244.0 108 | margin_bottom = -30.0 109 | rect_scale = Vector2( 2, 2 ) 110 | text = "End" 111 | 112 | [node name="Close" type="Button" parent="."] 113 | anchor_top = 1.0 114 | anchor_bottom = 1.0 115 | margin_left = 10.0 116 | margin_top = -50.0 117 | margin_right = 80.0 118 | margin_bottom = -30.0 119 | rect_scale = Vector2( 2, 2 ) 120 | text = "Close" 121 | [connection signal="file_selected" from="FileDialog" to="." method="_on_FileDialog_file_selected"] 122 | [connection signal="popup_hide" from="FileDialog" to="." method="_on_FileDialog_popup_hide"] 123 | [connection signal="pressed" from="OpenFile" to="." method="_on_OpenFile_pressed"] 124 | [connection signal="pressed" from="Home" to="." method="_on_Home_pressed"] 125 | [connection signal="pressed" from="Copy" to="." method="_on_Copy_pressed"] 126 | [connection signal="pressed" from="End" to="." method="_on_End_pressed"] 127 | [connection signal="pressed" from="Close" to="." method="_on_Close_pressed"] 128 | -------------------------------------------------------------------------------- /addons/gut/fonts/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009, Mark Simonson (http://www.ms-studio.com, mark@marksimonson.com), 2 | with Reserved Font Name Anonymous Pro. 3 | 4 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 5 | This license is copied below, and is also available with a FAQ at: 6 | http://scripts.sil.org/OFL 7 | 8 | 9 | ----------------------------------------------------------- 10 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 11 | ----------------------------------------------------------- 12 | 13 | PREAMBLE 14 | The goals of the Open Font License (OFL) are to stimulate worldwide 15 | development of collaborative font projects, to support the font creation 16 | efforts of academic and linguistic communities, and to provide a free and 17 | open framework in which fonts may be shared and improved in partnership 18 | with others. 19 | 20 | The OFL allows the licensed fonts to be used, studied, modified and 21 | redistributed freely as long as they are not sold by themselves. The 22 | fonts, including any derivative works, can be bundled, embedded, 23 | redistributed and/or sold with any software provided that any reserved 24 | names are not used by derivative works. The fonts and derivatives, 25 | however, cannot be released under any other type of license. The 26 | requirement for fonts to remain under this license does not apply 27 | to any document created using the fonts or their derivatives. 28 | 29 | DEFINITIONS 30 | "Font Software" refers to the set of files released by the Copyright 31 | Holder(s) under this license and clearly marked as such. This may 32 | include source files, build scripts and documentation. 33 | 34 | "Reserved Font Name" refers to any names specified as such after the 35 | copyright statement(s). 36 | 37 | "Original Version" refers to the collection of Font Software components as 38 | distributed by the Copyright Holder(s). 39 | 40 | "Modified Version" refers to any derivative made by adding to, deleting, 41 | or substituting -- in part or in whole -- any of the components of the 42 | Original Version, by changing formats or by porting the Font Software to a 43 | new environment. 44 | 45 | "Author" refers to any designer, engineer, programmer, technical 46 | writer or other person who contributed to the Font Software. 47 | 48 | PERMISSION & CONDITIONS 49 | Permission is hereby granted, free of charge, to any person obtaining 50 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 51 | redistribute, and sell modified and unmodified copies of the Font 52 | Software, subject to the following conditions: 53 | 54 | 1) Neither the Font Software nor any of its individual components, 55 | in Original or Modified Versions, may be sold by itself. 56 | 57 | 2) Original or Modified Versions of the Font Software may be bundled, 58 | redistributed and/or sold with any software, provided that each copy 59 | contains the above copyright notice and this license. These can be 60 | included either as stand-alone text files, human-readable headers or 61 | in the appropriate machine-readable metadata fields within text or 62 | binary files as long as those fields can be easily viewed by the user. 63 | 64 | 3) No Modified Version of the Font Software may use the Reserved Font 65 | Name(s) unless explicit written permission is granted by the corresponding 66 | Copyright Holder. This restriction only applies to the primary font name as 67 | presented to the users. 68 | 69 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 70 | Software shall not be used to promote, endorse or advertise any 71 | Modified Version, except to acknowledge the contribution(s) of the 72 | Copyright Holder(s) and the Author(s) or with their explicit written 73 | permission. 74 | 75 | 5) The Font Software, modified or unmodified, in part or in whole, 76 | must be distributed entirely under this license, and must not be 77 | distributed under any other license. The requirement for fonts to 78 | remain under this license does not apply to any document created 79 | using the Font Software. 80 | 81 | TERMINATION 82 | This license becomes null and void if any of the above conditions are 83 | not met. 84 | 85 | DISCLAIMER 86 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 87 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 88 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 89 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 90 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 91 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 92 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 93 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 94 | OTHER DEALINGS IN THE FONT SOFTWARE. 95 | -------------------------------------------------------------------------------- /addons/synced/Aligned.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | class_name Aligned 3 | 4 | # 5 | # Aligned node allows to set up lag compensation technique for efficient continuous 6 | # collision detection or (local, non-ranged) hit-scans every frame. 7 | # Instead of applying and reverting lag compensation on demand every frame, 8 | # this node keeps part of scene tree sort-of lag-compensated at all times. 9 | # 10 | # Attach this script to a Node2D or a Spatial. `Aligned` keeps changing its Transform 11 | # to make an illusion that children of Aligned are positioned and rotated as if 12 | # being in the past, according to global Time Depth value of parent's position 13 | # relative to all players. 14 | # 15 | # Transform and rotation from this node are only applied on Server. 16 | # `Aligned` does nothing to compensate how clients see the world. 17 | # For client-side counterpart, see SyncedProperty.time_depth 18 | # 19 | 20 | onready var synced: Synced 21 | 22 | # `Aligned` works by adding two SyncedProperties to sibling Synced object. 23 | # These properties are set up to track rotation and position (translation for Spatial) 24 | # and are never actually sent over the network to clients. 25 | 26 | func _ready(): 27 | # Make sure we're attached to either Spatial or Node2d 28 | assert(get_parent() is Spatial or get_parent() is Node2D, "Aligned node's parent must be either a Node2D or a Spatial.") 29 | if get_parent() is Spatial: 30 | assert('rotation' in self and 'translation' in self, "Aligned node must be set as script for a Spatial.") 31 | else: 32 | assert('rotation' in self and 'position' in self, "Aligned node must be set as script for a Node2D.") 33 | synced = _get_synced_sibling() 34 | assert(synced is Synced) 35 | synced.is_csp_enabled = true 36 | 37 | func _process(_d): 38 | if not SyncManager.is_server() and not SyncManager.is_client(): 39 | return 40 | 41 | var rotation_property = synced.get_rotation_property() 42 | var position_property = synced.get_position_property() 43 | 44 | _set_parent = true 45 | var old_state_id = get_time_depth_state_id() 46 | for p in [position_property, rotation_property]: 47 | if not p.ready_to_read(): 48 | continue 49 | var real_value 50 | var old_value 51 | if SyncManager.is_server(): 52 | # On server, we show visuals as if back in time according to Time Depth. 53 | real_value = p._get(-1) 54 | old_value = p._get(old_state_id) 55 | elif synced.is_client_side_predicted(p) and p.last_rollback_from_state_id > 0: 56 | # On client, we show predicted coordinates slightly back in time 57 | var target_state_id = SyncManager.seq.interpolation_state_id_frac 58 | real_value = p._get(int(target_state_id)) 59 | old_state_id = target_state_id - synced.get_csp_lag(p) 60 | old_value = p._get(old_state_id) 61 | if false and get_parent().name == 'Ball' and p.name == 'position': # !!! 62 | print("%s@%s(%s|%s)=%s(%s)" % [ 63 | p.name, 64 | int(target_state_id) % 1000, 65 | int(old_state_id) % 1000, 66 | int(target_state_id) - int(old_state_id), 67 | int(real_value.x), 68 | int(old_value.x) 69 | ]) 70 | if false: print([int(p.last_state_id) % 1000, # !!! 71 | int(p._get(-1).x), 72 | int(p._get(-2).x), 73 | int(p._get(-3).x), 74 | int(p._get(-4).x), 75 | int(p._get(-5).x), 76 | int(p._get(-6).x), 77 | int(p._get(-7).x), 78 | int(p._get(-8).x), 79 | int(p._get(-9).x), 80 | int(p._get(-10).x), 81 | ]) 82 | else: 83 | if false and get_parent().name == 'Ball' and p.name == 'position': # !!! 84 | print("Aligned reset") 85 | real_value = p._get(-1) 86 | old_value = real_value 87 | 88 | set(p.auto_sync_property, old_value - real_value) 89 | _set_parent = false 90 | 91 | var _set_parent = false 92 | 93 | func get_time_depth_state_id(): 94 | if not SyncManager.is_server(): 95 | return 0 # not applicable, not used on clients 96 | var real_coord = synced.get('position') 97 | if real_coord == null: 98 | return SyncManager.seq.state_id 99 | return SyncManager.seq.state_id - SyncManager.seq.get_time_depth(real_coord) 100 | 101 | func _get_synced_sibling(): 102 | for sibling in get_parent().get_children(): 103 | if sibling is Synced: 104 | return sibling 105 | 106 | func touch(prop): 107 | _set(prop, _get(prop)) 108 | 109 | func _get(prop): 110 | var p = synced.synced_properties.get(prop) if synced and synced.synced_properties else null 111 | if not p: 112 | return null 113 | if not SyncManager.is_server() or synced.is_client_owned(p) or not p.ready_to_read(): 114 | return synced._get(prop) 115 | return p.read(get_time_depth_state_id()) 116 | 117 | func _set(prop, value): 118 | if _set_parent: 119 | return 120 | var p = synced.synced_properties.get(prop) if synced and synced.synced_properties else null 121 | if not p: 122 | return null 123 | 124 | if not synced.is_client_owned(p): 125 | synced.rollback(prop) 126 | synced._set(prop, value) 127 | return true 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Godot Synced 2 | ============ 3 | 4 | Synced is a high-level networking framework for Godot game engine. It provides tools and design patterns 5 | to facilitate making fast-paced multiplayer games with authoritative server architecture. 6 | 7 | Synced is designed to be easy to grasp. It hides from game logic code most complexities inherent to networking. 8 | In most cases you should code your game objects as if you were making a single player game. 9 | 10 | TODO: apologize for a very early dev stage and all the bugs and TODOs ;) 11 | 12 | Getting started: working example 13 | -------------------------------- 14 | 15 | If you clone this repo, it contains a project with a game of Pong, re-implemented with Synced. 16 | 17 | ![pong-gif](assets/pong320at10.gif) 18 | 19 | Unremarkable unless you know what it tries to do. 20 | 21 | * One game instance is an **authoritative Server** in full control of game state. 22 | Server simulates game world at 60 (actually, `Engine.iterations_per_second`) ticks per second. 23 | * Another instance is a Client. Client **sends keyboard and mouse input** to Server 30 times per second 24 | and receives object positions and the rest of game state from Server 20 times per second. 25 | * Client smoothly **interpolates** game object positions between network frames, rendering game at 60+ FPS. 26 | Interpolation makes slow network sendrate imperceptible. Limiting network sendrate **optimizes traffic usage**. 27 | * Client implements **client-side prediction** mechanisms. This hides network round-trip to server, 28 | making player-controlled paddle react immediately to keypress and mouse movement. 29 | * Client implements **lag-compensation** mechanisms. Client's interaction with the ball happens exactly where Client 30 | sees the ball, hiding the fact that Server might see the ball elsewhere when Client's input finally comes to Server. 31 | * All this in barely more code than Godot's original demo of Pong. In particular, same code works for server-side 32 | simulation and for client-side prediction. **No code duplication.** 33 | 34 | Getting started: add to existing game 35 | ------------------------------------- 36 | 37 | 1. Copy addons/synced to the same dir in your project. 38 | 39 | 2. Add `res://addons/synced/SyncManager.gd` as a singleton. 40 | 41 | 3. Attach a [Synced](godot-synced/addons/synced/Synced.gd) node as a child to all active game objects 42 | (Node2D or Spatial) that need to be synced over the network between peers. This will make the Server send 43 | their positions and rotations to all Clients 20 times per second, with smooth interpolation between frames. 44 | See Pong example game. 45 | 46 | 4. Set up client-server connection as you would normally do via built-in Godot networking. 47 | See [lobby script](playground/pong/logic/lobby.gd) as an example. 48 | 49 | 5. Assign different `Synced` nodes you added in (3) to belong to different players. This has to be done for all players 50 | connected. Each player usually has one game object that represents player's avatar. Set `Synced.belongs_to_peer_id` 51 | to 0 for local player on both Clients and Server; and to peer_id from Godot's `NetworkedMultiplayerPeer` on Server. 52 | See [pong script](playground/pong/logic/pong.gd) as an example. 53 | 54 | 6. Instead of Godot's built-in `Input` class, you have to use `$synced.input` to read player input. 55 | `$synced` here means `Synced` object from (3) above that is a child of game object in question, 56 | and `$synced.input` is a (limited) drop-in replacement for Godot's Input class. 57 | See [paddle script](playground/pong/logic/paddle.gd) as an example. 58 | 59 | This should launch and sync as long as Clients and Server have the same NodePaths for all objects. All synced 60 | game objects will have their positions synced via network and interpolated between network frames. 61 | 62 | In-depth topics 63 | --------------- 64 | 65 | To sync any property on your game objects, not just their positions, add `SyncedProperty` children to `Synced` object. 66 | This also allows to customize how sync and interpolation should be done. See how Ball is set up to sync 67 | direction of movement and speed, and Pong scene to sync score. 68 | 69 | TODO: describe how to set up more nice things, how they work and why use them: authoritative server archetecture; 70 | interpolation; client-side prediction; lag compensation. 71 | 72 | TODO: add more complex examples: how to spawn and sync dynamic game objects; animation sync; tweak traffic footprint. 73 | 74 | Class docs 75 | ---------- 76 | 77 | Nothing to show here yet, WIP. Consider reading code comments inside `addons/synced`. TODO. 78 | 79 | Contacts 80 | -------- 81 | 82 | I will probably launch a Discord server eventually. Until then, reach me via `WhiteVirus#2531` on Discord. 83 | 84 | 85 | License 86 | ------- 87 | 88 | Copyright 2021-2022 Leonid Vakulenko. 89 | 90 | Licensed under the [MIT License](LICENSE.txt). 91 | -------------------------------------------------------------------------------- /test/unit/test_SyncPeer.gd: -------------------------------------------------------------------------------- 1 | extends "res://addons/gut/test.gd" 2 | 3 | var obj = null 4 | 5 | var sendtable1 = { 6 | 'bool': ['bool_zzzz', 'bool_qqqq'], 7 | 'float': ['float_z', 'float_q'] 8 | } 9 | var sendtable2 = { 10 | 'bool': [], 11 | 'float': ['float_z'] 12 | } 13 | var sendtable3 = { 14 | 'bool': ['bool_zzzz'], 15 | 'float': [] 16 | } 17 | 18 | var __input_frames_min_batch 19 | 20 | func before_all(): 21 | __input_frames_min_batch = SyncManager.input_frames_min_batch 22 | 23 | func before_each(): 24 | obj = autofree(SyncPeer.new()) 25 | 26 | func after_each(): 27 | SyncManager.input_frames_min_batch = __input_frames_min_batch 28 | 29 | func test_frame_batcher_parser1(): 30 | var frames = [{ 31 | 'bool_qqqq': 0, 32 | 'bool_zzzz': 1, 33 | 'float_q': 100.5, 34 | 'float_z': 0.0, 35 | }, { 36 | 'bool_qqqq': 0, 37 | 'bool_zzzz': 0, 38 | 'float_q': 0.0, 39 | 'float_z': 0.0, 40 | }] 41 | var packed_batch = obj.pack_input_batch(sendtable1, frames) 42 | var frames_out = obj.parse_input_batch(sendtable1, packed_batch[0], packed_batch[1], packed_batch[2]) 43 | assert_eq_deep(frames, frames_out) 44 | 45 | func test_frame_batcher_parser2(): 46 | var frames = [{ 47 | 'bool_qqqq': 0, 48 | 'bool_zzzz': 0, 49 | 'float_q': 0.0, 50 | 'float_z': 0.0, 51 | }, { 52 | 'bool_qqqq': 0, 53 | 'bool_zzzz': 0, 54 | 'float_q': 0.0, 55 | 'float_z': 0.0, 56 | }] 57 | SyncManager.input_frames_min_batch = frames.size() 58 | var packed_batch = obj.pack_input_batch(sendtable1, frames) 59 | assert_eq([], packed_batch[2]) 60 | var frames_out = obj.parse_input_batch(sendtable1, packed_batch[0], packed_batch[1], packed_batch[2]) 61 | assert_eq_deep(frames, frames_out) 62 | 63 | func test_frame_batcher_parser3(): 64 | var frames = [{ 65 | 'float_z': 1.0, 66 | }, { 67 | 'float_z': 2.0, 68 | }, { 69 | 'float_z': 0.0, 70 | }] 71 | var packed_batch = obj.pack_input_batch(sendtable2, frames) 72 | var frames_out = obj.parse_input_batch(sendtable2, packed_batch[0], packed_batch[1], packed_batch[2]) 73 | assert_eq_deep(frames, frames_out) 74 | 75 | func test_frame_batcher_parser4(): 76 | var frames = [{ 77 | 'bool_zzzz': 1, 78 | }, { 79 | 'bool_zzzz': 1, 80 | }, { 81 | 'bool_zzzz': 0, 82 | }] 83 | var packed_batch = obj.pack_input_batch(sendtable3, frames) 84 | var frames_out = obj.parse_input_batch(sendtable3, packed_batch[0], packed_batch[1], packed_batch[2]) 85 | assert_eq_deep(frames, frames_out) 86 | 87 | func test_frame_batcher_parser5(): 88 | var frames = [{ 89 | 'float_z': 0.0, 90 | }, { 91 | 'float_z': 0.0, 92 | }, { 93 | 'float_z': 0.0, 94 | }] 95 | SyncManager.input_frames_min_batch = frames.size() 96 | var packed_batch = obj.pack_input_batch(sendtable2, frames) 97 | var frames_out = obj.parse_input_batch(sendtable2, packed_batch[0], packed_batch[1], packed_batch[2]) 98 | assert_eq_deep(frames, frames_out) 99 | 100 | func test_frame_batcher_parser6(): 101 | var frames = [{ 102 | 'bool_zzzz': 0, 103 | }, { 104 | 'bool_zzzz': 0, 105 | }, { 106 | 'bool_zzzz': 0, 107 | }] 108 | SyncManager.input_frames_min_batch = frames.size() 109 | var packed_batch = obj.pack_input_batch(sendtable3, frames) 110 | var frames_out = obj.parse_input_batch(sendtable3, packed_batch[0], packed_batch[1], packed_batch[2]) 111 | assert_eq_deep(frames, frames_out) 112 | 113 | func test_frame_batcher_parser7(): 114 | var frames = [{ 115 | 'bool_qqqq': 0, 116 | 'bool_zzzz': 1, 117 | 'float_q': 100.5, 118 | 'float_z': 0.0, 119 | 'cop__Player1': [1, 2.0, 3], 120 | }, { 121 | 'bool_qqqq': 0, 122 | 'bool_zzzz': 0, 123 | 'float_q': 0.0, 124 | 'float_z': 0.0, 125 | 'cop__Player2': [3, 5.0, 9], 126 | }] 127 | var packed_batch = obj.pack_input_batch(sendtable1, frames) 128 | assert_true(packed_batch[1].size() > 0) 129 | var frames_out = obj.parse_input_batch(sendtable1, packed_batch[0], packed_batch[1], packed_batch[2]) 130 | assert_eq_deep(frames, frames_out) 131 | 132 | func test_cb_get_index(): 133 | var prop = SyncPeer.CircularBuffer.new(4, 0) 134 | assert_true(prop is Reference) 135 | prop.write(11, 111.0) 136 | prop.write(12, 112.0) 137 | prop.write(13, 113.0) 138 | prop.write(14, 114.0) 139 | prop.write(15, 115.0) 140 | assert_eq(15, prop.last_input_id) 141 | assert_eq(112.0, prop.container[prop._get_index(11)]) 142 | assert_eq(115.0, prop.container[prop._get_index(16)]) 143 | for i in range(12, 16): 144 | assert_eq(100.0+i, prop.container[prop._get_index(i)]) 145 | 146 | func test_cb_fetch(): 147 | var prop = SyncPeer.CircularBuffer.new(10, 0.0) 148 | assert_true(prop is Reference) 149 | assert_eq(10, prop.container.count(0.0)) 150 | prop.write(12, 100.0) 151 | assert_eq(100.0, prop.read(-1)) 152 | assert_eq(10, prop.container.size()) 153 | assert_eq(12, prop.last_input_id) 154 | assert_eq(1, prop.container.count(100.0)) 155 | prop.write(15, 200.0) 156 | assert_eq(100.0, prop.read(-2)) 157 | assert_eq(200.0, prop.read(-1)) 158 | assert_eq(15, prop.last_input_id) 159 | assert_eq(1, prop.container.count(200.0)) 160 | assert_eq(3, prop.container.count(100.0)) 161 | assert_eq(6, prop.container.count(0.0)) 162 | assert_eq(200.0, prop.container[prop.last_index]) 163 | assert_eq(200.0, prop.read(15)) 164 | assert_eq(200.0, prop.read(16)) 165 | assert_eq(0.0, prop.read(1)) 166 | 167 | func test_cb_contains(): 168 | var prop = SyncPeer.CircularBuffer.new(10, 0.0) 169 | prop.write(12, 100.0) 170 | assert_false(prop.contains(13)) 171 | assert_false(prop.contains(2)) 172 | for i in range(3, 13): 173 | assert_true(prop.contains(i)) 174 | -------------------------------------------------------------------------------- /addons/gut/summary.gd: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # Contains all the results of a single test. Allows for multiple asserts results 3 | # and pending calls. 4 | # ------------------------------------------------------------------------------ 5 | class Test: 6 | var pass_texts = [] 7 | var fail_texts = [] 8 | var pending_texts = [] 9 | 10 | # NOTE: The "failed" and "pending" text must match what is outputted by 11 | # the logger in order for text highlighting to occur in summary. 12 | func to_s(): 13 | var pad = ' ' 14 | var to_return = '' 15 | for i in range(fail_texts.size()): 16 | to_return += str(pad, '[Failed]: ', fail_texts[i], "\n") 17 | for i in range(pending_texts.size()): 18 | to_return += str(pad, '[Pending]: ', pending_texts[i], "\n") 19 | return to_return 20 | 21 | # ------------------------------------------------------------------------------ 22 | # Contains all the results for a single test-script/inner class. Persists the 23 | # names of the tests and results and the order in which the tests were run. 24 | # ------------------------------------------------------------------------------ 25 | class TestScript: 26 | var name = 'NOT_SET' 27 | var _tests = {} 28 | var _test_order = [] 29 | 30 | func _init(script_name): 31 | name = script_name 32 | 33 | func get_pass_count(): 34 | var count = 0 35 | for key in _tests: 36 | count += _tests[key].pass_texts.size() 37 | return count 38 | 39 | func get_fail_count(): 40 | var count = 0 41 | for key in _tests: 42 | count += _tests[key].fail_texts.size() 43 | return count 44 | 45 | func get_pending_count(): 46 | var count = 0 47 | for key in _tests: 48 | count += _tests[key].pending_texts.size() 49 | return count 50 | 51 | func get_test_obj(obj_name): 52 | if(!_tests.has(obj_name)): 53 | _tests[obj_name] = Test.new() 54 | _test_order.append(obj_name) 55 | return _tests[obj_name] 56 | 57 | func add_pass(test_name, reason): 58 | var t = get_test_obj(test_name) 59 | t.pass_texts.append(reason) 60 | 61 | func add_fail(test_name, reason): 62 | var t = get_test_obj(test_name) 63 | t.fail_texts.append(reason) 64 | 65 | func add_pending(test_name, reason): 66 | var t = get_test_obj(test_name) 67 | t.pending_texts.append(reason) 68 | 69 | # ------------------------------------------------------------------------------ 70 | # Summary Class 71 | # 72 | # This class holds the results of all the test scripts and Inner Classes that 73 | # were run. 74 | # -------------------------------------------d----------------------------------- 75 | var _scripts = [] 76 | 77 | func add_script(name): 78 | _scripts.append(TestScript.new(name)) 79 | 80 | func get_scripts(): 81 | return _scripts 82 | 83 | func get_current_script(): 84 | return _scripts[_scripts.size() - 1] 85 | 86 | func add_test(test_name): 87 | get_current_script().get_test_obj(test_name) 88 | 89 | func add_pass(test_name, reason = ''): 90 | get_current_script().add_pass(test_name, reason) 91 | 92 | func add_fail(test_name, reason = ''): 93 | get_current_script().add_fail(test_name, reason) 94 | 95 | func add_pending(test_name, reason = ''): 96 | get_current_script().add_pending(test_name, reason) 97 | 98 | func get_test_text(test_name): 99 | return test_name + "\n" + get_current_script().get_test_obj(test_name).to_s() 100 | 101 | # Gets the count of unique script names minus the . at the 102 | # end. Used for displaying the number of scripts without including all the 103 | # Inner Classes. 104 | func get_non_inner_class_script_count(): 105 | var unique_scripts = {} 106 | for i in range(_scripts.size()): 107 | var ext_loc = _scripts[i].name.find_last('.gd.') 108 | if(ext_loc == -1): 109 | unique_scripts[_scripts[i].name] = 1 110 | else: 111 | unique_scripts[_scripts[i].name.substr(0, ext_loc + 3)] = 1 112 | return unique_scripts.keys().size() 113 | 114 | func get_totals(): 115 | var totals = { 116 | passing = 0, 117 | pending = 0, 118 | failing = 0, 119 | tests = 0, 120 | scripts = 0 121 | } 122 | 123 | for s in range(_scripts.size()): 124 | totals.passing += _scripts[s].get_pass_count() 125 | totals.pending += _scripts[s].get_pending_count() 126 | totals.failing += _scripts[s].get_fail_count() 127 | totals.tests += _scripts[s]._test_order.size() 128 | 129 | totals.scripts = get_non_inner_class_script_count() 130 | 131 | return totals 132 | 133 | func log_summary_text(lgr): 134 | var orig_indent = lgr.get_indent_level() 135 | var found_failing_or_pending = false 136 | 137 | for s in range(_scripts.size()): 138 | lgr.set_indent_level(0) 139 | if(_scripts[s].get_fail_count() > 0 or _scripts[s].get_pending_count() > 0): 140 | lgr.log(_scripts[s].name, lgr.fmts.underline) 141 | 142 | 143 | for t in range(_scripts[s]._test_order.size()): 144 | var tname = _scripts[s]._test_order[t] 145 | var test = _scripts[s].get_test_obj(tname) 146 | if(test.fail_texts.size() > 0 or test.pending_texts.size() > 0): 147 | found_failing_or_pending = true 148 | lgr.log(str('- ', tname)) 149 | lgr.inc_indent() 150 | 151 | for i in range(test.fail_texts.size()): 152 | lgr.failed(test.fail_texts[i]) 153 | for i in range(test.pending_texts.size()): 154 | lgr.pending(test.pending_texts[i]) 155 | lgr.dec_indent() 156 | 157 | lgr.set_indent_level(0) 158 | if(!found_failing_or_pending): 159 | lgr.log('All tests passed', lgr.fmts.green) 160 | 161 | lgr.log() 162 | var _totals = get_totals() 163 | lgr.log("Totals", lgr.fmts.yellow) 164 | lgr.log(str('Scripts: ', get_non_inner_class_script_count())) 165 | lgr.log(str('Tests: ', _totals.tests)) 166 | lgr.log(str('Passing asserts: ', _totals.passing)) 167 | lgr.log(str('Failing asserts: ',_totals.failing)) 168 | lgr.log(str('Pending: ', _totals.pending)) 169 | 170 | lgr.set_indent_level(orig_indent) 171 | 172 | -------------------------------------------------------------------------------- /addons/gut/stubber.gd: -------------------------------------------------------------------------------- 1 | # { 2 | # inst_id_or_path1:{ 3 | # method_name1: [StubParams, StubParams], 4 | # method_name2: [StubParams, StubParams] 5 | # }, 6 | # inst_id_or_path2:{ 7 | # method_name1: [StubParams, StubParams], 8 | # method_name2: [StubParams, StubParams] 9 | # } 10 | # } 11 | var returns = {} 12 | var _utils = load('res://addons/gut/utils.gd').get_instance() 13 | var _lgr = _utils.get_logger() 14 | var _strutils = _utils.Strutils.new() 15 | 16 | func _make_key_from_metadata(doubled): 17 | var to_return = doubled.__gut_metadata_.path 18 | if(doubled.__gut_metadata_.subpath != ''): 19 | to_return += str('-', doubled.__gut_metadata_.subpath) 20 | return to_return 21 | 22 | # Creates they key for the returns hash based on the type of object passed in 23 | # obj could be a string of a path to a script with an optional subpath or 24 | # it could be an instance of a doubled object. 25 | func _make_key_from_variant(obj, subpath=null): 26 | var to_return = null 27 | 28 | match typeof(obj): 29 | TYPE_STRING: 30 | # this has to match what is done in _make_key_from_metadata 31 | to_return = obj 32 | if(subpath != null and subpath != ''): 33 | to_return += str('-', subpath) 34 | TYPE_OBJECT: 35 | if(_utils.is_instance(obj)): 36 | to_return = _make_key_from_metadata(obj) 37 | elif(_utils.is_native_class(obj)): 38 | to_return = _utils.get_native_class_name(obj) 39 | else: 40 | to_return = obj.resource_path 41 | return to_return 42 | 43 | func _add_obj_method(obj, method, subpath=null): 44 | var key = _make_key_from_variant(obj, subpath) 45 | if(_utils.is_instance(obj)): 46 | key = obj 47 | 48 | if(!returns.has(key)): 49 | returns[key] = {} 50 | if(!returns[key].has(method)): 51 | returns[key][method] = [] 52 | 53 | return key 54 | 55 | # ############## 56 | # Public 57 | # ############## 58 | 59 | # TODO: This method is only used in tests and should be refactored out. It 60 | # does not support inner classes and isn't helpful. 61 | func set_return(obj, method, value, parameters=null): 62 | var key = _add_obj_method(obj, method) 63 | var sp = _utils.StubParams.new(key, method) 64 | sp.parameters = parameters 65 | sp.return_val = value 66 | returns[key][method].append(sp) 67 | 68 | func add_stub(stub_params): 69 | if(stub_params.stub_method == '_init'): 70 | _lgr.error("You cannot stub _init. Super's _init is ALWAYS called.") 71 | else: 72 | var key = _add_obj_method(stub_params.stub_target, stub_params.stub_method, stub_params.target_subpath) 73 | returns[key][stub_params.stub_method].append(stub_params) 74 | 75 | # Searches returns for an entry that matches the instance or the class that 76 | # passed in obj is. 77 | # 78 | # obj can be an instance, class, or a path. 79 | func _find_stub(obj, method, parameters=null): 80 | var key = _make_key_from_variant(obj) 81 | var to_return = null 82 | 83 | if(_utils.is_instance(obj)): 84 | if(returns.has(obj) and returns[obj].has(method)): 85 | key = obj 86 | elif(obj.get('__gut_metadata_')): 87 | key = _make_key_from_metadata(obj) 88 | 89 | if(returns.has(key) and returns[key].has(method)): 90 | var param_idx = -1 91 | var null_idx = -1 92 | 93 | for i in range(returns[key][method].size()): 94 | if(returns[key][method][i].parameters == parameters): 95 | param_idx = i 96 | if(returns[key][method][i].parameters == null): 97 | null_idx = i 98 | 99 | # We have matching parameter values so return the stub value for that 100 | if(param_idx != -1): 101 | to_return = returns[key][method][param_idx] 102 | # We found a case where the parameters were not specified so return 103 | # parameters for that 104 | elif(null_idx != -1): 105 | to_return = returns[key][method][null_idx] 106 | else: 107 | _lgr.warn(str('Call to [', method, '] was not stubbed for the supplied parameters ', parameters, '. Null was returned.')) 108 | 109 | return to_return 110 | 111 | # Gets a stubbed return value for the object and method passed in. If the 112 | # instance was stubbed it will use that, otherwise it will use the path and 113 | # subpath of the object to try to find a value. 114 | # 115 | # It will also use the optional list of parameter values to find a value. If 116 | # the object was stubbed with no parameters than any parameters will match. 117 | # If it was stubbed with specific parameter values then it will try to match. 118 | # If the parameters do not match BUT there was also an empty parameter list stub 119 | # then it will return those. 120 | # If it cannot find anything that matches then null is returned.for 121 | # 122 | # Parameters 123 | # obj: this should be an instance of a doubled object. 124 | # method: the method called 125 | # parameters: optional array of parameter vales to find a return value for. 126 | func get_return(obj, method, parameters=null): 127 | var stub_info = _find_stub(obj, method, parameters) 128 | 129 | if(stub_info != null): 130 | return stub_info.return_val 131 | else: 132 | return null 133 | 134 | func should_call_super(obj, method, parameters=null): 135 | var stub_info = _find_stub(obj, method, parameters) 136 | if(stub_info != null): 137 | return stub_info.call_super 138 | else: 139 | # this log message is here because of how the generated doubled scripts 140 | # are structured. With this log msg here, you will only see one 141 | # "unstubbed" info instead of multiple. 142 | _lgr.info('Unstubbed call to ' + method + '::' + _strutils.type2str(obj)) 143 | return false 144 | 145 | 146 | func clear(): 147 | returns.clear() 148 | 149 | func get_logger(): 150 | return _lgr 151 | 152 | func set_logger(logger): 153 | _lgr = logger 154 | 155 | func to_s(): 156 | var text = '' 157 | for thing in returns: 158 | text += str(thing) + "\n" 159 | for method in returns[thing]: 160 | text += str("\t", method, "\n") 161 | for i in range(returns[thing][method].size()): 162 | text += "\t\t" + returns[thing][method][i].to_s() + "\n" 163 | return text 164 | -------------------------------------------------------------------------------- /addons/gut/strutils.gd: -------------------------------------------------------------------------------- 1 | 2 | var _utils = load('res://addons/gut/utils.gd').get_instance() 3 | # Hash containing all the built in types in Godot. This provides an English 4 | # name for the types that corosponds with the type constants defined in the 5 | # engine. 6 | var types = {} 7 | 8 | func _init_types_dictionary(): 9 | types[TYPE_NIL] = 'TYPE_NIL' 10 | types[TYPE_BOOL] = 'Bool' 11 | types[TYPE_INT] = 'Int' 12 | types[TYPE_REAL] = 'Float/Real' 13 | types[TYPE_STRING] = 'String' 14 | types[TYPE_VECTOR2] = 'Vector2' 15 | types[TYPE_RECT2] = 'Rect2' 16 | types[TYPE_VECTOR3] = 'Vector3' 17 | #types[8] = 'Matrix32' 18 | types[TYPE_PLANE] = 'Plane' 19 | types[TYPE_QUAT] = 'QUAT' 20 | types[TYPE_AABB] = 'AABB' 21 | #types[12] = 'Matrix3' 22 | types[TYPE_TRANSFORM] = 'Transform' 23 | types[TYPE_COLOR] = 'Color' 24 | #types[15] = 'Image' 25 | types[TYPE_NODE_PATH] = 'Node Path' 26 | types[TYPE_RID] = 'RID' 27 | types[TYPE_OBJECT] = 'TYPE_OBJECT' 28 | #types[19] = 'TYPE_INPUT_EVENT' 29 | types[TYPE_DICTIONARY] = 'Dictionary' 30 | types[TYPE_ARRAY] = 'Array' 31 | types[TYPE_RAW_ARRAY] = 'TYPE_RAW_ARRAY' 32 | types[TYPE_INT_ARRAY] = 'TYPE_INT_ARRAY' 33 | types[TYPE_REAL_ARRAY] = 'TYPE_REAL_ARRAY' 34 | types[TYPE_STRING_ARRAY] = 'TYPE_STRING_ARRAY' 35 | types[TYPE_VECTOR2_ARRAY] = 'TYPE_VECTOR2_ARRAY' 36 | types[TYPE_VECTOR3_ARRAY] = 'TYPE_VECTOR3_ARRAY' 37 | types[TYPE_COLOR_ARRAY] = 'TYPE_COLOR_ARRAY' 38 | types[TYPE_MAX] = 'TYPE_MAX' 39 | 40 | # Types to not be formatted when using _str 41 | var _str_ignore_types = [ 42 | TYPE_INT, TYPE_REAL, TYPE_STRING, 43 | TYPE_NIL, TYPE_BOOL 44 | ] 45 | 46 | func _init(): 47 | _init_types_dictionary() 48 | 49 | # ------------------------------------------------------------------------------ 50 | # ------------------------------------------------------------------------------ 51 | func _get_filename(path): 52 | return path.split('/')[-1] 53 | 54 | # ------------------------------------------------------------------------------ 55 | # Gets the filename of an object passed in. This does not return the 56 | # full path to the object, just the filename. 57 | # ------------------------------------------------------------------------------ 58 | func _get_obj_filename(thing): 59 | var filename = null 60 | 61 | if(thing == null or 62 | !is_instance_valid(thing) or 63 | str(thing) == '[Object:null]' or 64 | typeof(thing) != TYPE_OBJECT or 65 | thing.has_method('__gut_instance_from_id')): 66 | return 67 | 68 | if(thing.get_script() == null): 69 | if(thing is PackedScene): 70 | filename = _get_filename(thing.resource_path) 71 | else: 72 | # If it isn't a packed scene and it doesn't have a script then 73 | # we do nothing. This just read better. 74 | pass 75 | elif(thing.get_script() is NativeScript): 76 | # Work with GDNative scripts: 77 | # inst2dict fails with "Not a script with an instance" on GDNative script instances 78 | filename = _get_filename(thing.get_script().resource_path) 79 | elif(!_utils.is_native_class(thing)): 80 | var dict = inst2dict(thing) 81 | filename = _get_filename(dict['@path']) 82 | if(dict['@subpath'] != ''): 83 | filename += str('/', dict['@subpath']) 84 | 85 | return filename 86 | 87 | # ------------------------------------------------------------------------------ 88 | # Better object/thing to string conversion. Includes extra details about 89 | # whatever is passed in when it can/should. 90 | # ------------------------------------------------------------------------------ 91 | func type2str(thing): 92 | var oc = _utils.OrphanCounter.new() 93 | var filename = _get_obj_filename(thing) 94 | var str_thing = str(thing) 95 | 96 | if(thing == null): 97 | # According to str there is a difference between null and an Object 98 | # that is somehow null. To avoid getting '[Object:null]' as output 99 | # always set it to str(null) instead of str(thing). A null object 100 | # will pass typeof(thing) == TYPE_OBJECT check so this has to be 101 | # before that. 102 | str_thing = str(null) 103 | elif(typeof(thing) == TYPE_REAL): 104 | if(!'.' in str_thing): 105 | str_thing += '.0' 106 | elif(typeof(thing) == TYPE_STRING): 107 | str_thing = str('"', thing, '"') 108 | elif(typeof(thing) in _str_ignore_types): 109 | # do nothing b/c we already have str(thing) in 110 | # to_return. I think this just reads a little 111 | # better this way. 112 | pass 113 | elif(typeof(thing) == TYPE_OBJECT): 114 | if(_utils.is_native_class(thing)): 115 | str_thing = _utils.get_native_class_name(thing) 116 | elif(_utils.is_double(thing)): 117 | var double_path = _get_filename(thing.__gut_metadata_.path) 118 | if(thing.__gut_metadata_.subpath != ''): 119 | double_path += str('/', thing.__gut_metadata_.subpath) 120 | 121 | str_thing += '(double of ' + double_path + ')' 122 | filename = null 123 | elif(types.has(typeof(thing))): 124 | if(!str_thing.begins_with('(')): 125 | str_thing = '(' + str_thing + ')' 126 | str_thing = str(types[typeof(thing)], str_thing) 127 | 128 | if(filename != null): 129 | str_thing += str('(', filename, ')') 130 | return str_thing 131 | 132 | # ------------------------------------------------------------------------------ 133 | # Returns the string truncated with an '...' in it. Shows the start and last 134 | # 10 chars. If the string is smaller than max_size the entire string is 135 | # returned. If max_size is -1 then truncation is skipped. 136 | # ------------------------------------------------------------------------------ 137 | func truncate_string(src, max_size): 138 | var to_return = src 139 | if(src.length() > max_size - 10 and max_size != -1): 140 | to_return = str(src.substr(0, max_size - 10), '...', src.substr(src.length() - 10, src.length())) 141 | return to_return 142 | 143 | 144 | func _get_indent_text(times, pad): 145 | var to_return = '' 146 | for i in range(times): 147 | to_return += pad 148 | 149 | return to_return 150 | 151 | func indent_text(text, times, pad): 152 | if(times == 0): 153 | return text 154 | 155 | var to_return = text 156 | var ending_newline = '' 157 | 158 | if(text.ends_with("\n")): 159 | ending_newline = "\n" 160 | to_return = to_return.left(to_return.length() -1) 161 | 162 | var padding = _get_indent_text(times, pad) 163 | to_return = to_return.replace("\n", "\n" + padding) 164 | to_return += ending_newline 165 | 166 | return padding + to_return 167 | -------------------------------------------------------------------------------- /addons/gut/signal_watcher.gd: -------------------------------------------------------------------------------- 1 | # ############################################################################## 2 | # The MIT License (MIT) 3 | # ===================== 4 | # 5 | # Copyright (c) 2020 Tom "Butch" Wesley 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | # THE SOFTWARE. 24 | # 25 | # ############################################################################## 26 | 27 | # Some arbitrary string that should never show up by accident. If it does, then 28 | # shame on you. 29 | const ARG_NOT_SET = '_*_argument_*_is_*_not_set_*_' 30 | 31 | # This hash holds the objects that are being watched, the signals that are being 32 | # watched, and an array of arrays that contains arguments that were passed 33 | # each time the signal was emitted. 34 | # 35 | # For example: 36 | # _watched_signals => { 37 | # ref1 => { 38 | # 'signal1' => [[], [], []], 39 | # 'signal2' => [[p1, p2]], 40 | # 'signal3' => [[p1]] 41 | # }, 42 | # ref2 => { 43 | # 'some_signal' => [], 44 | # 'other_signal' => [[p1, p2, p3], [p1, p2, p3], [p1, p2, p3]] 45 | # } 46 | # } 47 | # 48 | # In this sample: 49 | # - signal1 on the ref1 object was emitted 3 times and each time, zero 50 | # parameters were passed. 51 | # - signal3 on ref1 was emitted once and passed a single parameter 52 | # - some_signal on ref2 was never emitted. 53 | # - other_signal on ref2 was emitted 3 times, each time with 3 parameters. 54 | var _watched_signals = {} 55 | var _utils = load('res://addons/gut/utils.gd').get_instance() 56 | 57 | func _add_watched_signal(obj, name): 58 | # SHORTCIRCUIT - ignore dupes 59 | if(_watched_signals.has(obj) and _watched_signals[obj].has(name)): 60 | return 61 | 62 | if(!_watched_signals.has(obj)): 63 | _watched_signals[obj] = {name:[]} 64 | else: 65 | _watched_signals[obj][name] = [] 66 | obj.connect(name, self, '_on_watched_signal', [obj, name]) 67 | 68 | # This handles all the signals that are watched. It supports up to 9 parameters 69 | # which could be emitted by the signal and the two parameters used when it is 70 | # connected via watch_signal. I chose 9 since you can only specify up to 9 71 | # parameters when dynamically calling a method via call (per the Godot 72 | # documentation, i.e. some_object.call('some_method', 1, 2, 3...)). 73 | # 74 | # Based on the documentation of emit_signal, it appears you can only pass up 75 | # to 4 parameters when firing a signal. I haven't verified this, but this should 76 | # future proof this some if the value ever grows. 77 | func _on_watched_signal(arg1=ARG_NOT_SET, arg2=ARG_NOT_SET, arg3=ARG_NOT_SET, \ 78 | arg4=ARG_NOT_SET, arg5=ARG_NOT_SET, arg6=ARG_NOT_SET, \ 79 | arg7=ARG_NOT_SET, arg8=ARG_NOT_SET, arg9=ARG_NOT_SET, \ 80 | arg10=ARG_NOT_SET, arg11=ARG_NOT_SET): 81 | var args = [arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11] 82 | 83 | # strip off any unused vars. 84 | var idx = args.size() -1 85 | while(str(args[idx]) == ARG_NOT_SET): 86 | args.remove(idx) 87 | idx -= 1 88 | 89 | # retrieve object and signal name from the array and remove them. These 90 | # will always be at the end since they are added when the connect happens. 91 | var signal_name = args[args.size() -1] 92 | args.pop_back() 93 | var object = args[args.size() -1] 94 | args.pop_back() 95 | 96 | _watched_signals[object][signal_name].append(args) 97 | 98 | func does_object_have_signal(object, signal_name): 99 | var signals = object.get_signal_list() 100 | for i in range(signals.size()): 101 | if(signals[i]['name'] == signal_name): 102 | return true 103 | return false 104 | 105 | func watch_signals(object): 106 | var signals = object.get_signal_list() 107 | for i in range(signals.size()): 108 | _add_watched_signal(object, signals[i]['name']) 109 | 110 | func watch_signal(object, signal_name): 111 | var did = false 112 | if(does_object_have_signal(object, signal_name)): 113 | _add_watched_signal(object, signal_name) 114 | did = true 115 | return did 116 | 117 | func get_emit_count(object, signal_name): 118 | var to_return = -1 119 | if(is_watching(object, signal_name)): 120 | to_return = _watched_signals[object][signal_name].size() 121 | return to_return 122 | 123 | func did_emit(object, signal_name): 124 | var did = false 125 | if(is_watching(object, signal_name)): 126 | did = get_emit_count(object, signal_name) != 0 127 | return did 128 | 129 | func print_object_signals(object): 130 | var list = object.get_signal_list() 131 | for i in range(list.size()): 132 | print(list[i].name, "\n ", list[i]) 133 | 134 | func get_signal_parameters(object, signal_name, index=-1): 135 | var params = null 136 | if(is_watching(object, signal_name)): 137 | var all_params = _watched_signals[object][signal_name] 138 | if(all_params.size() > 0): 139 | if(index == -1): 140 | index = all_params.size() -1 141 | params = all_params[index] 142 | return params 143 | 144 | func is_watching_object(object): 145 | return _watched_signals.has(object) 146 | 147 | func is_watching(object, signal_name): 148 | return _watched_signals.has(object) and _watched_signals[object].has(signal_name) 149 | 150 | func clear(): 151 | for obj in _watched_signals: 152 | if(_utils.is_not_freed(obj)): 153 | for signal_name in _watched_signals[obj]: 154 | obj.disconnect(signal_name, self, '_on_watched_signal') 155 | _watched_signals.clear() 156 | 157 | # Returns a list of all the signal names that were emitted by the object. 158 | # If the object is not being watched then an empty list is returned. 159 | func get_signals_emitted(obj): 160 | var emitted = [] 161 | if(is_watching_object(obj)): 162 | for signal_name in _watched_signals[obj]: 163 | if(_watched_signals[obj][signal_name].size() > 0): 164 | emitted.append(signal_name) 165 | 166 | return emitted 167 | -------------------------------------------------------------------------------- /addons/gut/method_maker.gd: -------------------------------------------------------------------------------- 1 | # This class will generate method declaration lines based on method meta 2 | # data. It will create defaults that match the method data. 3 | # 4 | # -------------------- 5 | # function meta data 6 | # -------------------- 7 | # name: 8 | # flags: 9 | # args: [{ 10 | # (class_name:), 11 | # (hint:0), 12 | # (hint_string:), 13 | # (name:), 14 | # (type:4), 15 | # (usage:7) 16 | # }] 17 | # default_args [] 18 | 19 | var _utils = load('res://addons/gut/utils.gd').get_instance() 20 | var _lgr = _utils.get_logger() 21 | const PARAM_PREFIX = 'p_' 22 | 23 | # ------------------------------------------------------ 24 | # _supported_defaults 25 | # 26 | # This array contains all the data types that are supported for default values. 27 | # If a value is supported it will contain either an empty string or a prefix 28 | # that should be used when setting the parameter default value. 29 | # For example int, real, bool do not need anything func(p1=1, p2=2.2, p3=false) 30 | # but things like Vectors and Colors do since only the parameters to create a 31 | # new Vector or Color are included in the metadata. 32 | # ------------------------------------------------------ 33 | # TYPE_NIL = 0 — Variable is of type nil (only applied for null). 34 | # TYPE_BOOL = 1 — Variable is of type bool. 35 | # TYPE_INT = 2 — Variable is of type int. 36 | # TYPE_REAL = 3 — Variable is of type float/real. 37 | # TYPE_STRING = 4 — Variable is of type String. 38 | # TYPE_VECTOR2 = 5 — Variable is of type Vector2. 39 | # TYPE_RECT2 = 6 — Variable is of type Rect2. 40 | # TYPE_VECTOR3 = 7 — Variable is of type Vector3. 41 | # TYPE_COLOR = 14 — Variable is of type Color. 42 | # TYPE_OBJECT = 17 — Variable is of type Object. 43 | # TYPE_DICTIONARY = 18 — Variable is of type Dictionary. 44 | # TYPE_ARRAY = 19 — Variable is of type Array. 45 | # TYPE_VECTOR2_ARRAY = 24 — Variable is of type PoolVector2Array. 46 | 47 | 48 | 49 | # TYPE_TRANSFORM2D = 8 — Variable is of type Transform2D. 50 | # TYPE_PLANE = 9 — Variable is of type Plane. 51 | # TYPE_QUAT = 10 — Variable is of type Quat. 52 | # TYPE_AABB = 11 — Variable is of type AABB. 53 | # TYPE_BASIS = 12 — Variable is of type Basis. 54 | # TYPE_TRANSFORM = 13 — Variable is of type Transform. 55 | # TYPE_NODE_PATH = 15 — Variable is of type NodePath. 56 | # TYPE_RID = 16 — Variable is of type RID. 57 | # TYPE_RAW_ARRAY = 20 — Variable is of type PoolByteArray. 58 | # TYPE_INT_ARRAY = 21 — Variable is of type PoolIntArray. 59 | # TYPE_REAL_ARRAY = 22 — Variable is of type PoolRealArray. 60 | # TYPE_STRING_ARRAY = 23 — Variable is of type PoolStringArray. 61 | # TYPE_VECTOR3_ARRAY = 25 — Variable is of type PoolVector3Array. 62 | # TYPE_COLOR_ARRAY = 26 — Variable is of type PoolColorArray. 63 | # TYPE_MAX = 27 — Marker for end of type constants. 64 | # ------------------------------------------------------ 65 | var _supported_defaults = [] 66 | 67 | func _init(): 68 | for _i in range(TYPE_MAX): 69 | _supported_defaults.append(null) 70 | 71 | # These types do not require a prefix for defaults 72 | _supported_defaults[TYPE_NIL] = '' 73 | _supported_defaults[TYPE_BOOL] = '' 74 | _supported_defaults[TYPE_INT] = '' 75 | _supported_defaults[TYPE_REAL] = '' 76 | _supported_defaults[TYPE_OBJECT] = '' 77 | _supported_defaults[TYPE_ARRAY] = '' 78 | _supported_defaults[TYPE_STRING] = '' 79 | _supported_defaults[TYPE_DICTIONARY] = '' 80 | _supported_defaults[TYPE_VECTOR2_ARRAY] = '' 81 | 82 | # These require a prefix for whatever default is provided 83 | _supported_defaults[TYPE_VECTOR2] = 'Vector2' 84 | _supported_defaults[TYPE_RECT2] = 'Rect2' 85 | _supported_defaults[TYPE_VECTOR3] = 'Vector3' 86 | _supported_defaults[TYPE_COLOR] = 'Color' 87 | 88 | # ############### 89 | # Private 90 | # ############### 91 | var _func_text = _utils.get_file_as_text('res://addons/gut/double_templates/function_template.txt') 92 | 93 | func _is_supported_default(type_flag): 94 | return type_flag >= 0 and type_flag < _supported_defaults.size() and [type_flag] != null 95 | 96 | # Creates a list of parameters with defaults of null unless a default value is 97 | # found in the metadata. If a default is found in the meta then it is used if 98 | # it is one we know how support. 99 | # 100 | # If a default is found that we don't know how to handle then this method will 101 | # return null. 102 | func _get_arg_text(method_meta): 103 | var text = '' 104 | var args = method_meta.args 105 | var defaults = [] 106 | var has_unsupported_defaults = false 107 | 108 | # fill up the defaults with null defaults for everything that doesn't have 109 | # a default in the meta data. default_args is an array of default values 110 | # for the last n parameters where n is the size of default_args so we only 111 | # add nulls for everything up to the first parameter with a default. 112 | for _i in range(args.size() - method_meta.default_args.size()): 113 | defaults.append('null') 114 | 115 | # Add meta-data defaults. 116 | for i in range(method_meta.default_args.size()): 117 | var t = args[defaults.size()]['type'] 118 | var value = '' 119 | if(_is_supported_default(t)): 120 | # strings are special, they need quotes around the value 121 | if(t == TYPE_STRING): 122 | value = str("'", str(method_meta.default_args[i]), "'") 123 | # Colors need the parens but things like Vector2 and Rect2 don't 124 | elif(t == TYPE_COLOR): 125 | value = str(_supported_defaults[t], '(', str(method_meta.default_args[i]), ')') 126 | elif(t == TYPE_OBJECT): 127 | if(str(method_meta.default_args[i]) == "[Object:null]"): 128 | value = str(_supported_defaults[t], 'null') 129 | else: 130 | value = str(_supported_defaults[t], str(method_meta.default_args[i]).to_lower()) 131 | 132 | # Everything else puts the prefix (if one is there) form _supported_defaults 133 | # in front. The to_lower is used b/c for some reason the defaults for 134 | # null, true, false are all "Null", "True", "False". 135 | else: 136 | value = str(_supported_defaults[t], str(method_meta.default_args[i]).to_lower()) 137 | else: 138 | _lgr.warn(str( 139 | 'Unsupported default param type: ',method_meta.name, '-', args[defaults.size()].name, ' ', t, ' = ', method_meta.default_args[i])) 140 | value = str('unsupported=',t) 141 | has_unsupported_defaults = true 142 | 143 | defaults.append(value) 144 | 145 | # construct the string of parameters 146 | for i in range(args.size()): 147 | text += str(PARAM_PREFIX, args[i].name, '=', defaults[i]) 148 | if(i != args.size() -1): 149 | text += ', ' 150 | 151 | # if we don't know how to make a default then we have to return null b/c 152 | # it will cause a runtime error and it's one thing we could return to let 153 | # callers know it didn't work. 154 | if(has_unsupported_defaults): 155 | text = null 156 | 157 | return text 158 | 159 | # ############### 160 | # Public 161 | # ############### 162 | 163 | # Creates a delceration for a function based off of function metadata. All 164 | # types whose defaults are supported will have their values. If a datatype 165 | # is not supported and the parameter has a default, a warning message will be 166 | # printed and the declaration will return null. 167 | func get_function_text(meta): 168 | var method_params = _get_arg_text(meta) 169 | var text = null 170 | 171 | var param_array = get_spy_call_parameters_text(meta) 172 | if(param_array == 'null'): 173 | param_array = '[]' 174 | 175 | if(method_params != null): 176 | var decleration = str('func ', meta.name, '(', method_params, '):') 177 | text = _func_text.format({ 178 | "func_decleration":decleration, 179 | "method_name":meta.name, 180 | "param_array":param_array, 181 | "super_call":get_super_call_text(meta) 182 | }) 183 | return text 184 | 185 | # creates a call to the function in meta in the super's class. 186 | func get_super_call_text(meta): 187 | var params = '' 188 | 189 | for i in range(meta.args.size()): 190 | params += PARAM_PREFIX + meta.args[i].name 191 | if(meta.args.size() > 1 and i != meta.args.size() -1): 192 | params += ', ' 193 | if(meta.name == '_init'): 194 | return 'null' 195 | else: 196 | return str('.', meta.name, '(', params, ')') 197 | 198 | func get_spy_call_parameters_text(meta): 199 | var called_with = 'null' 200 | if(meta.args.size() > 0): 201 | called_with = '[' 202 | for i in range(meta.args.size()): 203 | called_with += str(PARAM_PREFIX, meta.args[i].name) 204 | if(i < meta.args.size() - 1): 205 | called_with += ', ' 206 | called_with += ']' 207 | return called_with 208 | 209 | func get_logger(): 210 | return _lgr 211 | 212 | func set_logger(logger): 213 | _lgr = logger 214 | -------------------------------------------------------------------------------- /addons/synced/SyncSequence.gd: -------------------------------------------------------------------------------- 1 | extends Reference 2 | class_name SyncSequence 3 | 4 | # Server: last World State that's been processed (or being processed) 5 | # Client: an artifical value designed to match state_id of a netframe when 6 | # the new netframe comes from the server. It increments every physics tick but 7 | # can sometimes skip states or hold for one value for several ticks 8 | # in order to achieve time sync. 9 | var state_id: int = 1 10 | 11 | # Same as state_id, smoothed out into float during _process() calls 12 | var state_id_frac: float setget , get_state_id_frac 13 | 14 | # Client: same as state_id but takes interpolation lag into account 15 | var interpolation_state_id: int setget ,get_interpolation_state_id 16 | 17 | # Client: same as state_id_frac but takes interpolation lag into account 18 | var interpolation_state_id_frac: float setget ,get_interpolation_state_id_frac 19 | 20 | # Last input_id generated by local player, processed by server. 21 | var last_consumed_input_id = 0 22 | 23 | var current_latency_in_state_ids = 0 24 | 25 | var _mgr # :SyncManager 26 | var _input_id_to_state_id: SyncPeer.CircularBuffer 27 | 28 | func _init(manager): 29 | _mgr = manager 30 | _input_id_to_state_id = SyncPeer.CircularBuffer.new( 31 | int(max( 32 | _mgr.client_interpolation_history_size, 33 | _mgr.input_frames_history_size 34 | )), 35 | 0 36 | ) 37 | 38 | # Called by SyncManager at _process() time 39 | func _process(_delta): 40 | _fix_state_id_frac() 41 | 42 | # Called by SyncManager at _physics_process() time 43 | func _physics_process(_delta): 44 | _first_process_since_physics_process = true 45 | 46 | # Increment global state time 47 | state_id += 1 48 | # Fix clock desync 49 | if _mgr.is_client(): 50 | state_id = _fix_current_state_id(state_id) 51 | _input_id_to_state_id.write(_mgr.get_local_peer().input_id, get_interpolation_state_id()) 52 | 53 | if _mgr.is_server(): 54 | _input_id_to_state_id.write(_mgr.get_local_peer().input_id, state_id) 55 | last_consumed_input_id = _mgr.get_local_peer().input_id 56 | 57 | func get_time_depth(target_coord): 58 | return calculate_time_depth(target_coord)[0] 59 | 60 | func calculate_time_depth(target_coord): 61 | if not _mgr.is_server(): 62 | return [0, null] 63 | # For each peer_id, find the closest object to target_coord 64 | var candidates = {} 65 | for wr in _synced_belong_to_players: 66 | var synced = wr.get_ref() 67 | if not synced: 68 | continue 69 | assert(synced is Synced and synced.belongs_to_peer_id != null) 70 | var coord = _mgr.get_coord(synced.get_parent()) 71 | if (coord is Vector3) != (target_coord is Vector3): 72 | continue 73 | var distance_squared = target_coord.distance_squared_to(coord) 74 | if not (synced.belongs_to_peer_id in candidates) or candidates[synced.belongs_to_peer_id][0] > distance_squared: 75 | candidates[synced.belongs_to_peer_id] = [ 76 | distance_squared, 77 | synced.belongs_to_peer_id, 78 | ] 79 | var result = _td_calc(candidates.values()) 80 | return result 81 | 82 | # Keep track of all Synced objects belonging to players. 83 | # We'll need their positions in order to calcullate Time Depth. 84 | var _synced_belong_to_players = [] 85 | func update_synced_belong_to_players(before, after, synced: Synced): 86 | if synced.ignore_peer_time_depth: 87 | return 88 | if before != null and after == null: 89 | var new_arr = [] 90 | for wr in _synced_belong_to_players: 91 | var synced2 = wr.get_ref() 92 | if synced2 and synced2 != synced: 93 | new_arr.append(synced2) 94 | _synced_belong_to_players = new_arr 95 | elif before == null and after != null: 96 | _synced_belong_to_players.append(weakref(synced)) 97 | if not synced.synced_property('_td_position'): 98 | # Need to track positions of stuff that belogs to players 99 | # in order to calculate Time depth 100 | if synced.get_parent() is Node2D or synced.get_parent() is Spatial: 101 | synced.add_synced_property('_td_position', SyncedProperty.new({ 102 | missing_state_interpolation = SyncedProperty.NO_INTERPOLATION, 103 | interpolation = SyncedProperty.NO_INTERPOLATION, 104 | sync_strategy = SyncedProperty.DO_NOT_SYNC, 105 | auto_sync_property = 'translation' if synced.get_parent() is Spatial else 'position' 106 | })) 107 | 108 | func _td_calc(players: Array): 109 | # No reason to bend spacetime when there's only one player 110 | if players.size() <= 1: 111 | return [0, null] 112 | 113 | # we are only interested in two closest players 114 | players.sort_custom(self, '_td_calc_sorter') 115 | players = players.slice(0, 1) 116 | assert(2 == players.size() and players[0][0] <= players[1][0]) 117 | var distance_to_closest_sq = players[0][0] 118 | var distance_to_second_closest_sq = players[1][0] 119 | if distance_to_second_closest_sq <= 0: 120 | return [0, null] # paranoid mode 121 | 122 | # We want time depth at coordinate of each player to be equal to plat player's delay 123 | # When distance is equal to both players, time depth is 0 124 | # Far away from either player, time depth is 0 125 | var closest_peer_id = players[0][1] 126 | var half_delay = state_id - _mgr.get_peer(closest_peer_id).state_id 127 | return [ 128 | int(half_delay * (1 - clamp(distance_to_closest_sq / distance_to_second_closest_sq, 0, 1))), 129 | closest_peer_id 130 | ] 131 | func _td_calc_sorter(a, b): 132 | return a[0] < b[0] 133 | 134 | # Used to sync server clock and client clock. 135 | # Only maintained on Client 136 | var _old_received_state_id = 0 137 | var _last_received_state_id = 0 138 | var _old_received_state_mtime = 0 139 | var _last_received_state_mtime = 0 140 | 141 | # Maintain stats to calculate a running average 142 | # for server tickrate over (up to) last 1000 frames 143 | func update_received_state_id_and_mtime(new_server_state_id, consumed_input_id): 144 | if _last_received_state_id >= new_server_state_id: 145 | return 146 | _last_received_state_id = new_server_state_id 147 | _last_received_state_mtime = OS.get_system_time_msecs() 148 | if _old_received_state_id == 0: 149 | _old_received_state_id = _last_received_state_id 150 | _old_received_state_mtime = _last_received_state_mtime 151 | return 152 | var new_state_diff = _last_received_state_id - _old_received_state_id 153 | var new_time_diff = _last_received_state_mtime - _old_received_state_mtime 154 | if new_state_diff > 1000: 155 | _old_received_state_id = _last_received_state_id - 1000 156 | _old_received_state_mtime = _last_received_state_mtime - (new_time_diff * 1000.0 / new_state_diff) 157 | 158 | if consumed_input_id: 159 | var old_client_state_id = input_id_to_state_id(consumed_input_id) 160 | current_latency_in_state_ids = int(move_toward(current_latency_in_state_ids, new_server_state_id - old_client_state_id, 1)) 161 | 162 | func reset_client_connection_stats(): 163 | _last_received_state_mtime = 0 164 | _old_received_state_mtime = 0 165 | _last_received_state_id = 0 166 | _old_received_state_id = 0 167 | state_id = 1 168 | 169 | # On Client, CSP correction must remember when we recorded each input frame 170 | func input_id_to_state_id(input_id:int): 171 | assert(_mgr.is_client()) 172 | if not _input_id_to_state_id.contains(input_id): 173 | return null 174 | return _input_id_to_state_id.read(input_id) 175 | 176 | # Shenanigans needed to calculate state_id_frac 177 | var _state_id_frac_fix = 0.0 178 | var _first_process_since_physics_process = true 179 | 180 | # idea behind this is to make first call to _process() after _physics_process() 181 | # always give integer state_id_frac 182 | func _fix_state_id_frac(): 183 | if _first_process_since_physics_process and _mgr.state_id_frac_to_integer_reduction > 0: 184 | _first_process_since_physics_process = false 185 | _state_id_frac_fix = move_toward( 186 | _state_id_frac_fix, 187 | Engine.get_physics_interpolation_fraction(), 188 | _mgr.state_id_frac_to_integer_reduction 189 | ) 190 | 191 | # This scary logic figures out if client should skip some state_ids 192 | # or wait and render state_id two times in a row. This is needed in case 193 | # of clock desync between client and server, or short network failure. 194 | func _fix_current_state_id(st_id:int)->int: 195 | assert(_mgr.is_client()) 196 | var should_be_current_state_id = int(clamp(st_id, _last_received_state_id, _last_received_state_id + _mgr.client_interpolation_lag)) 197 | if abs(should_be_current_state_id - st_id) > _mgr.client_interpolation_lag + _mgr.max_offline_extrapolation: 198 | # Instantly jump if difference is too large 199 | st_id = should_be_current_state_id 200 | else: 201 | # Gradually move towards proper value 202 | st_id = int(move_toward(st_id, should_be_current_state_id, 1)) 203 | # Refuse to extrapolate more than allowed by global settings 204 | if st_id > _last_received_state_id + _mgr.max_offline_extrapolation: 205 | st_id = _last_received_state_id + _mgr.max_offline_extrapolation 206 | return st_id 207 | 208 | func get_interpolation_state_id()->int: 209 | assert(_mgr.is_client()) 210 | var result = state_id - _mgr.client_interpolation_lag 211 | return result if result > 1 else 1 212 | 213 | func get_interpolation_state_id_frac()->float: 214 | assert(_mgr.is_client()) 215 | return max(1, get_state_id_frac() - _mgr.client_interpolation_lag) 216 | 217 | func get_state_id_frac(): 218 | if Engine.is_in_physics_frame(): 219 | return float(state_id) 220 | var result = Engine.get_physics_interpolation_fraction() - _state_id_frac_fix 221 | result = clamp(result, 0.0, 0.99) 222 | return result + state_id 223 | -------------------------------------------------------------------------------- /addons/gut/optparse.gd: -------------------------------------------------------------------------------- 1 | # ############################################################################## 2 | #(G)odot (U)nit (T)est class 3 | # 4 | # ############################################################################## 5 | # The MIT License (MIT) 6 | # ===================== 7 | # 8 | # Copyright (c) 2020 Tom "Butch" Wesley 9 | # 10 | # Permission is hereby granted, free of charge, to any person obtaining a copy 11 | # of this software and associated documentation files (the "Software"), to deal 12 | # in the Software without restriction, including without limitation the rights 13 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | # copies of the Software, and to permit persons to whom the Software is 15 | # furnished to do so, subject to the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be included in 18 | # all copies or substantial portions of the Software. 19 | # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | # THE SOFTWARE. 27 | # 28 | # ############################################################################## 29 | # Description 30 | # ----------- 31 | # Command line interface for the GUT unit testing tool. Allows you to run tests 32 | # from the command line instead of running a scene. Place this script along with 33 | # gut.gd into your scripts directory at the root of your project. Once there you 34 | # can run this script (from the root of your project) using the following command: 35 | # godot -s -d test/gut/gut_cmdln.gd 36 | # 37 | # See the readme for a list of options and examples. You can also use the -gh 38 | # option to get more information about how to use the command line interface. 39 | # ############################################################################## 40 | 41 | #------------------------------------------------------------------------------- 42 | # Parses the command line arguments supplied into an array that can then be 43 | # examined and parsed based on how the gut options work. 44 | #------------------------------------------------------------------------------- 45 | class CmdLineParser: 46 | var _used_options = [] 47 | # an array of arrays. Each element in this array will contain an option 48 | # name and if that option contains a value then it will have a sedond 49 | # element. For example: 50 | # [[-gselect, test.gd], [-gexit]] 51 | var _opts = [] 52 | 53 | func _init(): 54 | for i in range(OS.get_cmdline_args().size()): 55 | var opt_val = OS.get_cmdline_args()[i].split('=') 56 | _opts.append(opt_val) 57 | 58 | # Parse out multiple comma delimited values from a command line 59 | # option. Values are separated from option name with "=" and 60 | # additional values are comma separated. 61 | func _parse_array_value(full_option): 62 | var value = _parse_option_value(full_option) 63 | var split = value.split(',') 64 | return split 65 | 66 | # Parse out the value of an option. Values are separated from 67 | # the option name with "=" 68 | func _parse_option_value(full_option): 69 | if(full_option.size() > 1): 70 | return full_option[1] 71 | else: 72 | return null 73 | 74 | # Search _opts for an element that starts with the option name 75 | # specified. 76 | func find_option(name): 77 | var found = false 78 | var idx = 0 79 | 80 | while(idx < _opts.size() and !found): 81 | if(_opts[idx][0] == name): 82 | found = true 83 | else: 84 | idx += 1 85 | 86 | if(found): 87 | return idx 88 | else: 89 | return -1 90 | 91 | func get_array_value(option): 92 | _used_options.append(option) 93 | var to_return = [] 94 | var opt_loc = find_option(option) 95 | if(opt_loc != -1): 96 | to_return = _parse_array_value(_opts[opt_loc]) 97 | _opts.remove(opt_loc) 98 | 99 | return to_return 100 | 101 | # returns the value of an option if it was specified, null otherwise. This 102 | # used to return the default but that became problemnatic when trying to 103 | # punch through the different places where values could be specified. 104 | func get_value(option): 105 | _used_options.append(option) 106 | var to_return = null 107 | var opt_loc = find_option(option) 108 | if(opt_loc != -1): 109 | to_return = _parse_option_value(_opts[opt_loc]) 110 | _opts.remove(opt_loc) 111 | 112 | return to_return 113 | 114 | # returns true if it finds the option, false if not. 115 | func was_specified(option): 116 | _used_options.append(option) 117 | return find_option(option) != -1 118 | 119 | # Returns any unused command line options. I found that only the -s and 120 | # script name come through from godot, all other options that godot uses 121 | # are not sent through OS.get_cmdline_args(). 122 | # 123 | # This is a onetime thing b/c i kill all items in _used_options 124 | func get_unused_options(): 125 | var to_return = [] 126 | for i in range(_opts.size()): 127 | to_return.append(_opts[i][0]) 128 | 129 | var script_option = to_return.find('-s') 130 | if script_option != -1: 131 | to_return.remove(script_option + 1) 132 | to_return.remove(script_option) 133 | 134 | while(_used_options.size() > 0): 135 | var index = to_return.find(_used_options[0].split("=")[0]) 136 | if(index != -1): 137 | to_return.remove(index) 138 | _used_options.remove(0) 139 | 140 | return to_return 141 | 142 | #------------------------------------------------------------------------------- 143 | # Simple class to hold a command line option 144 | #------------------------------------------------------------------------------- 145 | class Option: 146 | var value = null 147 | var option_name = '' 148 | var default = null 149 | var description = '' 150 | 151 | func _init(name, default_value, desc=''): 152 | option_name = name 153 | default = default_value 154 | description = desc 155 | value = null#default_value 156 | 157 | func pad(to_pad, size, pad_with=' '): 158 | var to_return = to_pad 159 | for _i in range(to_pad.length(), size): 160 | to_return += pad_with 161 | 162 | return to_return 163 | 164 | func to_s(min_space=0): 165 | var subbed_desc = description 166 | if(subbed_desc.find('[default]') != -1): 167 | subbed_desc = subbed_desc.replace('[default]', str(default)) 168 | return pad(option_name, min_space) + subbed_desc 169 | 170 | #------------------------------------------------------------------------------- 171 | # The high level interface between this script and the command line options 172 | # supplied. Uses Option class and CmdLineParser to extract information from 173 | # the command line and make it easily accessible. 174 | #------------------------------------------------------------------------------- 175 | var options = [] 176 | var _opts = [] 177 | var _banner = '' 178 | 179 | func add(name, default, desc): 180 | options.append(Option.new(name, default, desc)) 181 | 182 | func get_value(name): 183 | var found = false 184 | var idx = 0 185 | 186 | while(idx < options.size() and !found): 187 | if(options[idx].option_name == name): 188 | found = true 189 | else: 190 | idx += 1 191 | 192 | if(found): 193 | return options[idx].value 194 | else: 195 | print("COULD NOT FIND OPTION " + name) 196 | return null 197 | 198 | func set_banner(banner): 199 | _banner = banner 200 | 201 | func print_help(): 202 | var longest = 0 203 | for i in range(options.size()): 204 | if(options[i].option_name.length() > longest): 205 | longest = options[i].option_name.length() 206 | 207 | print('---------------------------------------------------------') 208 | print(_banner) 209 | 210 | print("\nOptions\n-------") 211 | for i in range(options.size()): 212 | print(' ' + options[i].to_s(longest + 2)) 213 | print('---------------------------------------------------------') 214 | 215 | func print_options(): 216 | for i in range(options.size()): 217 | print(options[i].option_name + '=' + str(options[i].value)) 218 | 219 | func parse(): 220 | var parser = CmdLineParser.new() 221 | 222 | for i in range(options.size()): 223 | var t = typeof(options[i].default) 224 | # only set values that were specified at the command line so that 225 | # we can punch through default and config values correctly later. 226 | # Without this check, you can't tell the difference between the 227 | # defaults and what was specified, so you can't punch through 228 | # higher level options. 229 | if(parser.was_specified(options[i].option_name)): 230 | if(t == TYPE_INT): 231 | options[i].value = int(parser.get_value(options[i].option_name)) 232 | elif(t == TYPE_STRING): 233 | options[i].value = parser.get_value(options[i].option_name) 234 | elif(t == TYPE_ARRAY): 235 | options[i].value = parser.get_array_value(options[i].option_name) 236 | elif(t == TYPE_BOOL): 237 | options[i].value = parser.was_specified(options[i].option_name) 238 | elif(t == TYPE_NIL): 239 | print(options[i].option_name + ' cannot be processed, it has a nil datatype') 240 | else: 241 | print(options[i].option_name + ' cannot be processed, it has unknown datatype:' + str(t)) 242 | 243 | var unused = parser.get_unused_options() 244 | if(unused.size() > 0): 245 | print("Unrecognized options: ", unused) 246 | return false 247 | 248 | return true 249 | -------------------------------------------------------------------------------- /addons/gut/test_collector.gd: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # Used to keep track of info about each test ran. 3 | # ------------------------------------------------------------------------------ 4 | class Test: 5 | # indicator if it passed or not. defaults to true since it takes only 6 | # one failure to make it not pass. _fail in gut will set this. 7 | var passed = true 8 | # the name of the function 9 | var name = "" 10 | # flag to know if the name has been printed yet. 11 | var has_printed_name = false 12 | # the number of arguments the method has 13 | var arg_count = 0 14 | # The number of asserts in the test 15 | var assert_count = 0 16 | # if the test has been marked pending at anypont during 17 | # execution. 18 | var pending = false 19 | 20 | 21 | # ------------------------------------------------------------------------------ 22 | # This holds all the meta information for a test script. It contains the 23 | # name of the inner class and an array of Test "structs". 24 | # 25 | # This class also facilitates all the exporting and importing of tests. 26 | # ------------------------------------------------------------------------------ 27 | class TestScript: 28 | var inner_class_name = null 29 | var tests = [] 30 | var path = null 31 | var _utils = null 32 | var _lgr = null 33 | 34 | func _init(utils=null, logger=null): 35 | _utils = utils 36 | _lgr = logger 37 | 38 | func to_s(): 39 | var to_return = path 40 | if(inner_class_name != null): 41 | to_return += str('.', inner_class_name) 42 | to_return += "\n" 43 | for i in range(tests.size()): 44 | to_return += str(' ', tests[i].name, "\n") 45 | return to_return 46 | 47 | func get_new(): 48 | return load_script().new() 49 | 50 | func load_script(): 51 | #print('loading: ', get_full_name()) 52 | var to_return = load(path) 53 | if(inner_class_name != null): 54 | # If we wanted to do inner classes in inner classses 55 | # then this would have to become some kind of loop or recursive 56 | # call to go all the way down the chain or this class would 57 | # have to change to hold onto the loaded class instead of 58 | # just path information. 59 | to_return = to_return.get(inner_class_name) 60 | return to_return 61 | 62 | func get_filename_and_inner(): 63 | var to_return = get_filename() 64 | if(inner_class_name != null): 65 | to_return += '.' + inner_class_name 66 | return to_return 67 | 68 | func get_full_name(): 69 | var to_return = path 70 | if(inner_class_name != null): 71 | to_return += '.' + inner_class_name 72 | return to_return 73 | 74 | func get_filename(): 75 | return path.get_file() 76 | 77 | func has_inner_class(): 78 | return inner_class_name != null 79 | 80 | # Note: although this no longer needs to export the inner_class names since 81 | # they are pulled from metadata now, it is easier to leave that in 82 | # so we don't have to cut the export down to unique script names. 83 | func export_to(config_file, section): 84 | config_file.set_value(section, 'path', path) 85 | config_file.set_value(section, 'inner_class', inner_class_name) 86 | var names = [] 87 | for i in range(tests.size()): 88 | names.append(tests[i].name) 89 | config_file.set_value(section, 'tests', names) 90 | 91 | func _remap_path(source_path): 92 | var to_return = source_path 93 | if(!_utils.file_exists(source_path)): 94 | _lgr.debug('Checking for remap for: ' + source_path) 95 | var remap_path = source_path.get_basename() + '.gd.remap' 96 | if(_utils.file_exists(remap_path)): 97 | var cf = ConfigFile.new() 98 | cf.load(remap_path) 99 | to_return = cf.get_value('remap', 'path') 100 | else: 101 | _lgr.warn('Could not find remap file ' + remap_path) 102 | return to_return 103 | 104 | func import_from(config_file, section): 105 | path = config_file.get_value(section, 'path') 106 | path = _remap_path(path) 107 | # Null is an acceptable value, but you can't pass null as a default to 108 | # get_value since it thinks you didn't send a default...then it spits 109 | # out red text. This works around that. 110 | var inner_name = config_file.get_value(section, 'inner_class', 'Placeholder') 111 | if(inner_name != 'Placeholder'): 112 | inner_class_name = inner_name 113 | else: # just being explicit 114 | inner_class_name = null 115 | 116 | func get_test_named(name): 117 | return _utils.search_array(tests, 'name', name) 118 | 119 | # ------------------------------------------------------------------------------ 120 | # start test_collector, I don't think I like the name. 121 | # ------------------------------------------------------------------------------ 122 | var scripts = [] 123 | var _test_prefix = 'test_' 124 | var _test_class_prefix = 'Test' 125 | 126 | var _utils = load('res://addons/gut/utils.gd').get_instance() 127 | var _lgr = _utils.get_logger() 128 | 129 | func _does_inherit_from_test(thing): 130 | var base_script = thing.get_base_script() 131 | var to_return = false 132 | if(base_script != null): 133 | var base_path = base_script.get_path() 134 | if(base_path == 'res://addons/gut/test.gd'): 135 | to_return = true 136 | else: 137 | to_return = _does_inherit_from_test(base_script) 138 | return to_return 139 | 140 | func _populate_tests(test_script): 141 | var methods = test_script.load_script().get_script_method_list() 142 | for i in range(methods.size()): 143 | var name = methods[i]['name'] 144 | if(name.begins_with(_test_prefix)): 145 | var t = Test.new() 146 | t.name = name 147 | t.arg_count = methods[i]['args'].size() 148 | test_script.tests.append(t) 149 | 150 | func _get_inner_test_class_names(loaded): 151 | var inner_classes = [] 152 | var const_map = loaded.get_script_constant_map() 153 | for key in const_map: 154 | var thing = const_map[key] 155 | if(typeof(thing) == TYPE_OBJECT): 156 | if(key.begins_with(_test_class_prefix)): 157 | if(_does_inherit_from_test(thing)): 158 | inner_classes.append(key) 159 | else: 160 | _lgr.warn(str('Ignoring Inner Class ', key, 161 | ' because it does not extend res://addons/gut/test.gd')) 162 | 163 | # This could go deeper and find inner classes within inner classes 164 | # but requires more experimentation. Right now I'm keeping it at 165 | # one level since that is what the previous version did and there 166 | # has been no demand for deeper nesting. 167 | # _populate_inner_test_classes(thing) 168 | return inner_classes 169 | 170 | func _parse_script(test_script): 171 | var inner_classes = [] 172 | var scripts_found = [] 173 | 174 | var loaded = load(test_script.path) 175 | if(_does_inherit_from_test(loaded)): 176 | _populate_tests(test_script) 177 | scripts_found.append(test_script.path) 178 | inner_classes = _get_inner_test_class_names(loaded) 179 | 180 | for i in range(inner_classes.size()): 181 | var loaded_inner = loaded.get(inner_classes[i]) 182 | if(_does_inherit_from_test(loaded_inner)): 183 | var ts = TestScript.new(_utils, _lgr) 184 | ts.path = test_script.path 185 | ts.inner_class_name = inner_classes[i] 186 | _populate_tests(ts) 187 | scripts.append(ts) 188 | scripts_found.append(test_script.path + '[' + inner_classes[i] +']') 189 | 190 | return scripts_found 191 | 192 | # ----------------- 193 | # Public 194 | # ----------------- 195 | func add_script(path): 196 | # SHORTCIRCUIT 197 | if(has_script(path)): 198 | return [] 199 | 200 | var f = File.new() 201 | # SHORTCIRCUIT 202 | if(!f.file_exists(path)): 203 | _lgr.error('Could not find script: ' + path) 204 | return 205 | 206 | var ts = TestScript.new(_utils, _lgr) 207 | ts.path = path 208 | scripts.append(ts) 209 | return _parse_script(ts) 210 | 211 | func clear(): 212 | scripts.clear() 213 | 214 | func has_script(path): 215 | var found = false 216 | var idx = 0 217 | while(idx < scripts.size() and !found): 218 | if(scripts[idx].get_full_name() == path): 219 | found = true 220 | else: 221 | idx += 1 222 | return found 223 | 224 | func export_tests(path): 225 | var success = true 226 | var f = ConfigFile.new() 227 | for i in range(scripts.size()): 228 | scripts[i].export_to(f, str('TestScript-', i)) 229 | var result = f.save(path) 230 | if(result != OK): 231 | _lgr.error(str('Could not save exported tests to [', path, ']. Error code: ', result)) 232 | success = false 233 | return success 234 | 235 | func import_tests(path): 236 | var success = false 237 | var f = ConfigFile.new() 238 | var result = f.load(path) 239 | if(result != OK): 240 | _lgr.error(str('Could not load exported tests from [', path, ']. Error code: ', result)) 241 | else: 242 | var sections = f.get_sections() 243 | for key in sections: 244 | var ts = TestScript.new(_utils, _lgr) 245 | ts.import_from(f, key) 246 | _populate_tests(ts) 247 | scripts.append(ts) 248 | success = true 249 | return success 250 | 251 | func get_script_named(name): 252 | return _utils.search_array(scripts, 'get_filename_and_inner', name) 253 | 254 | func get_test_named(script_name, test_name): 255 | var s = get_script_named(script_name) 256 | if(s != null): 257 | return s.get_test_named(test_name) 258 | else: 259 | return null 260 | 261 | func to_s(): 262 | var to_return = '' 263 | for i in range(scripts.size()): 264 | to_return += scripts[i].to_s() + "\n" 265 | return to_return 266 | 267 | # --------------------- 268 | # Accessors 269 | # --------------------- 270 | func get_logger(): 271 | return _lgr 272 | 273 | func set_logger(logger): 274 | _lgr = logger 275 | 276 | func get_test_prefix(): 277 | return _test_prefix 278 | 279 | func set_test_prefix(test_prefix): 280 | _test_prefix = test_prefix 281 | 282 | func get_test_class_prefix(): 283 | return _test_class_prefix 284 | 285 | func set_test_class_prefix(test_class_prefix): 286 | _test_class_prefix = test_class_prefix 287 | -------------------------------------------------------------------------------- /addons/gut/logger.gd: -------------------------------------------------------------------------------- 1 | # ############################################################################## 2 | #(G)odot (U)nit (T)est class 3 | # 4 | # ############################################################################## 5 | # The MIT License (MIT) 6 | # ===================== 7 | # 8 | # Copyright (c) 2020 Tom "Butch" Wesley 9 | # 10 | # Permission is hereby granted, free of charge, to any person obtaining a copy 11 | # of this software and associated documentation files (the "Software"), to deal 12 | # in the Software without restriction, including without limitation the rights 13 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | # copies of the Software, and to permit persons to whom the Software is 15 | # furnished to do so, subject to the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be included in 18 | # all copies or substantial portions of the Software. 19 | # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | # THE SOFTWARE. 27 | # 28 | # ############################################################################## 29 | # This class wraps around the various printers and supplies formatting for the 30 | # various message types (error, warning, etc). 31 | # ############################################################################## 32 | var types = { 33 | debug = 'debug', 34 | deprecated = 'deprecated', 35 | error = 'error', 36 | failed = 'failed', 37 | info = 'info', 38 | normal = 'normal', 39 | orphan = 'orphan', 40 | passed = 'passed', 41 | pending = 'pending', 42 | warn ='warn', 43 | } 44 | 45 | var fmts = { 46 | red = 'red', 47 | yellow = 'yellow', 48 | green = 'green', 49 | 50 | bold = 'bold', 51 | underline = 'underline', 52 | 53 | none = null 54 | } 55 | 56 | var _type_data = { 57 | types.debug: {disp='DEBUG', enabled=true, fmt=fmts.none}, 58 | types.deprecated: {disp='DEPRECATED', enabled=true, fmt=fmts.none}, 59 | types.error: {disp='ERROR', enabled=true, fmt=fmts.red}, 60 | types.failed: {disp='Failed', enabled=true, fmt=fmts.red}, 61 | types.info: {disp='INFO', enabled=true, fmt=fmts.bold}, 62 | types.normal: {disp='NORMAL', enabled=true, fmt=fmts.none}, 63 | types.orphan: {disp='Orphans', enabled=true, fmt=fmts.yellow}, 64 | types.passed: {disp='Passed', enabled=true, fmt=fmts.green}, 65 | types.pending: {disp='Pending', enabled=true, fmt=fmts.yellow}, 66 | types.warn: {disp='WARNING', enabled=true, fmt=fmts.yellow}, 67 | } 68 | 69 | var _logs = { 70 | types.warn: [], 71 | types.error: [], 72 | types.info: [], 73 | types.debug: [], 74 | types.deprecated: [], 75 | } 76 | 77 | var _printers = { 78 | terminal = null, 79 | gui = null, 80 | console = null 81 | } 82 | 83 | var _gut = null 84 | var _utils = null 85 | var _indent_level = 0 86 | var _indent_string = ' ' 87 | var _skip_test_name_for_testing = false 88 | var _less_test_names = false 89 | var _yield_calls = 0 90 | var _last_yield_text = '' 91 | 92 | 93 | func _init(): 94 | _utils = load('res://addons/gut/utils.gd').get_instance() 95 | _printers.terminal = _utils.Printers.TerminalPrinter.new() 96 | _printers.console = _utils.Printers.ConsolePrinter.new() 97 | # There were some problems in the timing of disabling this at the right 98 | # time in gut_cmdln so it is disabled by default. This is enabled 99 | # by plugin_control.gd based on settings. 100 | _printers.console.set_disabled(true) 101 | 102 | func get_indent_text(): 103 | var pad = '' 104 | for i in range(_indent_level): 105 | pad += _indent_string 106 | 107 | return pad 108 | 109 | func _indent_text(text): 110 | var to_return = text 111 | var ending_newline = '' 112 | 113 | if(text.ends_with("\n")): 114 | ending_newline = "\n" 115 | to_return = to_return.left(to_return.length() -1) 116 | 117 | var pad = get_indent_text() 118 | to_return = to_return.replace("\n", "\n" + pad) 119 | to_return += ending_newline 120 | 121 | return pad + to_return 122 | 123 | func _should_print_to_printer(key_name): 124 | return _printers[key_name] != null and !_printers[key_name].get_disabled() 125 | 126 | func _print_test_name(): 127 | if(_gut == null): 128 | return 129 | var cur_test = _gut.get_current_test_object() 130 | if(cur_test == null): 131 | return false 132 | 133 | if(!cur_test.has_printed_name): 134 | _output('* ' + cur_test.name + "\n") 135 | cur_test.has_printed_name = true 136 | 137 | func _output(text, fmt=null): 138 | for key in _printers: 139 | if(_should_print_to_printer(key)): 140 | var info = ''#str(self, ':', key, ':', _printers[key], '| ') 141 | _printers[key].send(info + text, fmt) 142 | 143 | func _log(text, fmt=fmts.none): 144 | _print_test_name() 145 | var indented = _indent_text(text) 146 | _output(indented, fmt) 147 | 148 | # --------------- 149 | # Get Methods 150 | # --------------- 151 | func get_warnings(): 152 | return get_log_entries(types.warn) 153 | 154 | func get_errors(): 155 | return get_log_entries(types.error) 156 | 157 | func get_infos(): 158 | return get_log_entries(types.info) 159 | 160 | func get_debugs(): 161 | return get_log_entries(types.debug) 162 | 163 | func get_deprecated(): 164 | return get_log_entries(types.deprecated) 165 | 166 | func get_count(log_type=null): 167 | var count = 0 168 | if(log_type == null): 169 | for key in _logs: 170 | count += _logs[key].size() 171 | else: 172 | count = _logs[log_type].size() 173 | return count 174 | 175 | func get_log_entries(log_type): 176 | return _logs[log_type] 177 | 178 | # --------------- 179 | # Log methods 180 | # --------------- 181 | func _output_type(type, text): 182 | var td = _type_data[type] 183 | if(!td.enabled): 184 | return 185 | 186 | _print_test_name() 187 | if(type != types.normal): 188 | if(_logs.has(type)): 189 | _logs[type].append(text) 190 | 191 | var start = str('[', td.disp, ']') 192 | if(text != null and text != ''): 193 | start += ': ' 194 | else: 195 | start += ' ' 196 | var indented_start = _indent_text(start) 197 | var indented_end = _indent_text(text) 198 | indented_end = indented_end.lstrip(_indent_string) 199 | _output(indented_start, td.fmt) 200 | _output(indented_end + "\n") 201 | 202 | func debug(text): 203 | _output_type(types.debug, text) 204 | 205 | # supply some text or the name of the deprecated method and the replacement. 206 | func deprecated(text, alt_method=null): 207 | var msg = text 208 | if(alt_method): 209 | msg = str('The method ', text, ' is deprecated, use ', alt_method , ' instead.') 210 | return _output_type(types.deprecated, msg) 211 | 212 | func error(text): 213 | _output_type(types.error, text) 214 | 215 | func failed(text): 216 | _output_type(types.failed, text) 217 | 218 | func info(text): 219 | _output_type(types.info, text) 220 | 221 | func orphan(text): 222 | _output_type(types.orphan, text) 223 | 224 | func passed(text): 225 | _output_type(types.passed, text) 226 | 227 | func pending(text): 228 | _output_type(types.pending, text) 229 | 230 | func warn(text): 231 | _output_type(types.warn, text) 232 | 233 | func log(text='', fmt=fmts.none): 234 | end_yield() 235 | if(text == ''): 236 | _output("\n") 237 | else: 238 | _log(text + "\n", fmt) 239 | return null 240 | 241 | func lograw(text, fmt=fmts.none): 242 | return _output(text, fmt) 243 | 244 | # Print the test name if we aren't skipping names of tests that pass (basically 245 | # what _less_test_names means)) 246 | func log_test_name(): 247 | # suppress output if we haven't printed the test name yet and 248 | # what to print is the test name. 249 | if(!_less_test_names): 250 | _print_test_name() 251 | 252 | # --------------- 253 | # Misc 254 | # --------------- 255 | func get_gut(): 256 | return _gut 257 | 258 | func set_gut(gut): 259 | _gut = gut 260 | if(_gut == null): 261 | _printers.gui = null 262 | else: 263 | if(_printers.gui == null): 264 | _printers.gui = _utils.Printers.GutGuiPrinter.new() 265 | _printers.gui.set_gut(gut) 266 | 267 | func get_indent_level(): 268 | return _indent_level 269 | 270 | func set_indent_level(indent_level): 271 | _indent_level = indent_level 272 | 273 | func get_indent_string(): 274 | return _indent_string 275 | 276 | func set_indent_string(indent_string): 277 | _indent_string = indent_string 278 | 279 | func clear(): 280 | for key in _logs: 281 | _logs[key].clear() 282 | 283 | func inc_indent(): 284 | _indent_level += 1 285 | 286 | func dec_indent(): 287 | _indent_level = max(0, _indent_level -1) 288 | 289 | func is_type_enabled(type): 290 | return _type_data[type].enabled 291 | 292 | func set_type_enabled(type, is_enabled): 293 | _type_data[type].enabled = is_enabled 294 | 295 | func get_less_test_names(): 296 | return _less_test_names 297 | 298 | func set_less_test_names(less_test_names): 299 | _less_test_names = less_test_names 300 | 301 | func disable_printer(name, is_disabled): 302 | _printers[name].set_disabled(is_disabled) 303 | 304 | func is_printer_disabled(name): 305 | return _printers[name].get_disabled() 306 | 307 | func disable_formatting(is_disabled): 308 | for key in _printers: 309 | _printers[key].set_format_enabled(!is_disabled) 310 | 311 | func get_printer(printer_key): 312 | return _printers[printer_key] 313 | 314 | func _yield_text_terminal(text): 315 | var printer = _printers['terminal'] 316 | if(_yield_calls != 0): 317 | printer.clear_line() 318 | printer.back(_last_yield_text.length()) 319 | printer.send(text, fmts.yellow) 320 | 321 | func _end_yield_terminal(): 322 | var printer = _printers['terminal'] 323 | printer.clear_line() 324 | printer.back(_last_yield_text.length()) 325 | 326 | func _yield_text_gui(text): 327 | var lbl = _gut.get_gui().get_waiting_label() 328 | lbl.visible = true 329 | lbl.set_bbcode('[color=yellow]' + text + '[/color]') 330 | 331 | func _end_yield_gui(): 332 | var lbl = _gut.get_gui().get_waiting_label() 333 | lbl.visible = false 334 | lbl.set_text('') 335 | 336 | func yield_text(text): 337 | _yield_text_terminal(text) 338 | _yield_text_gui(text) 339 | _last_yield_text = text 340 | _yield_calls += 1 341 | 342 | func end_yield(): 343 | if(_yield_calls == 0): 344 | return 345 | _end_yield_terminal() 346 | _end_yield_gui() 347 | _yield_calls = 0 348 | _last_yield_text = '' 349 | -------------------------------------------------------------------------------- /addons/gut/plugin_control.gd: -------------------------------------------------------------------------------- 1 | # ############################################################################## 2 | #(G)odot (U)nit (T)est class 3 | # 4 | # ############################################################################## 5 | # The MIT License (MIT) 6 | # ===================== 7 | # 8 | # Copyright (c) 2020 Tom "Butch" Wesley 9 | # 10 | # Permission is hereby granted, free of charge, to any person obtaining a copy 11 | # of this software and associated documentation files (the "Software"), to deal 12 | # in the Software without restriction, including without limitation the rights 13 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | # copies of the Software, and to permit persons to whom the Software is 15 | # furnished to do so, subject to the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be included in 18 | # all copies or substantial portions of the Software. 19 | # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | # THE SOFTWARE. 27 | # 28 | # ############################################################################## 29 | # This is the control that is added via the editor. It exposes GUT settings 30 | # through the editor and delays the creation of the GUT instance until 31 | # Engine.get_main_loop() works as expected. 32 | # ############################################################################## 33 | tool 34 | extends Control 35 | 36 | # ------------------------------------------------------------------------------ 37 | # GUT Settings 38 | # ------------------------------------------------------------------------------ 39 | export(String, 'AnonymousPro', 'CourierPrime', 'LobsterTwo', 'Default') var _font_name = 'AnonymousPro' 40 | export(int) var _font_size = 20 41 | export(Color) var _font_color = Color(.8, .8, .8, 1) 42 | export(Color) var _background_color = Color(.15, .15, .15, 1) 43 | # Enable/Disable coloring of output. 44 | export(bool) var _color_output = true 45 | # The full/partial name of a script to select upon startup 46 | export(String) var _select_script = '' 47 | # The full/partial name of a test. All tests that contain the string will be 48 | # run 49 | export(String) var _tests_like = '' 50 | # The full/partial name of an Inner Class to be run. All Inner Classes that 51 | # contain the string will be run. 52 | export(String) var _inner_class_name = '' 53 | # Start running tests when the scene finishes loading 54 | export var _run_on_load = false 55 | # Maximize the GUT control on startup 56 | export var _should_maximize = false 57 | # Print output to the consol as well 58 | export var _should_print_to_console = true 59 | # Display orphan counts at the end of tests/scripts. 60 | export var _show_orphans = true 61 | # The log level. 62 | export(int, 'Fail/Errors', 'Errors/Warnings/Test Names', 'Everything') var _log_level = 1 63 | # When enabled GUT will yield between tests to give the GUI time to paint. 64 | # Disabling this can make the program appear to hang and can have some 65 | # unwanted consequences with the timing of freeing objects 66 | export var _yield_between_tests = true 67 | # When GUT compares values it first checks the types to prevent runtime errors. 68 | # This behavior can be disabled if desired. This flag was added early in 69 | # development to prevent any breaking changes and will likely be removed in 70 | # the future. 71 | export var _disable_strict_datatype_checks = false 72 | # The prefix used to find test methods. 73 | export var _test_prefix = 'test_' 74 | # The prefix used to find test scripts. 75 | export var _file_prefix = 'test_' 76 | # The file extension for test scripts (I don't think you can change this and 77 | # everythign work). 78 | export var _file_extension = '.gd' 79 | # The prefix used to find Inner Test Classes. 80 | export var _inner_class_prefix = 'Test' 81 | # The directory GUT will use to write any temporary files. This isn't used 82 | # much anymore since there was a change to the double creation implementation. 83 | # This will be removed in a later release. 84 | export(String) var _temp_directory = 'user://gut_temp_directory' 85 | # The path and filename for exported test information. 86 | export(String) var _export_path = '' 87 | # When enabled, any directory added will also include its subdirectories when 88 | # GUT looks for test scripts. 89 | export var _include_subdirectories = false 90 | # Allow user to add test directories via editor. This is done with strings 91 | # instead of an array because the interface for editing arrays is really 92 | # cumbersome and complicates testing because arrays set through the editor 93 | # apply to ALL instances. This also allows the user to use the built in 94 | # dialog to pick a directory. 95 | export(String, DIR) var _directory1 = '' 96 | export(String, DIR) var _directory2 = '' 97 | export(String, DIR) var _directory3 = '' 98 | export(String, DIR) var _directory4 = '' 99 | export(String, DIR) var _directory5 = '' 100 | export(String, DIR) var _directory6 = '' 101 | # Must match the types in _utils for double strategy 102 | export(int, 'FULL', 'PARTIAL') var _double_strategy = 1 103 | # Path and filename to the script to run before all tests are run. 104 | export(String, FILE) var _pre_run_script = '' 105 | # Path and filename to the script to run after all tests are run. 106 | export(String, FILE) var _post_run_script = '' 107 | # ------------------------------------------------------------------------------ 108 | 109 | 110 | # ------------------------------------------------------------------------------ 111 | # Signals 112 | # ------------------------------------------------------------------------------ 113 | # Emitted when all the tests have finished running. 114 | signal tests_finished 115 | # Emitted when GUT is ready to be interacted with, and before any tests are run. 116 | signal gut_ready 117 | 118 | 119 | # ------------------------------------------------------------------------------ 120 | # Private stuff. 121 | # ------------------------------------------------------------------------------ 122 | var _gut = null 123 | var _lgr = null 124 | var _cancel_import = false 125 | var _placeholder = null 126 | 127 | func _init(): 128 | # This min size has to be what the min size of the GutScene's min size is 129 | # but it has to be set here and not inferred i think. 130 | rect_min_size = Vector2(740, 250) 131 | 132 | func _ready(): 133 | # Must call this deferred so that there is enough time for 134 | # Engine.get_main_loop() is populated and the psuedo singleton utils.gd 135 | # can be setup correctly. 136 | if(Engine.editor_hint): 137 | _placeholder = load('res://addons/gut/GutScene.tscn').instance() 138 | call_deferred('add_child', _placeholder) 139 | _placeholder.rect_size = rect_size 140 | else: 141 | call_deferred('_setup_gut') 142 | 143 | connect('resized', self, '_on_resized') 144 | 145 | func _on_resized(): 146 | if(_placeholder != null): 147 | _placeholder.rect_size = rect_size 148 | 149 | 150 | # Templates can be missing if tests are exported and the export config for the 151 | # project does not include '*.txt' files. This check and related flags make 152 | # sure GUT does not blow up and that the error is not lost in all the import 153 | # output that is generated as well as ensuring that no tests are run. 154 | # 155 | # Assumption: This is only a concern when running from the scene since you 156 | # cannot run GUT from the command line in an exported game. 157 | func _check_for_templates(): 158 | var f = File.new() 159 | if(!f.file_exists('res://addons/gut/double_templates/function_template.txt')): 160 | _lgr.error('Templates are missing. Make sure you are exporting "*.txt" or "addons/gut/double_templates/*.txt".') 161 | _run_on_load = false 162 | _cancel_import = true 163 | return false 164 | return true 165 | 166 | func _setup_gut(): 167 | var _utils = load('res://addons/gut/utils.gd').get_instance() 168 | 169 | _lgr = _utils.get_logger() 170 | _gut = load('res://addons/gut/gut.gd').new() 171 | _gut.connect('tests_finished', self, '_on_tests_finished') 172 | 173 | if(!_check_for_templates()): 174 | return 175 | 176 | _gut._select_script = _select_script 177 | _gut._tests_like = _tests_like 178 | _gut._inner_class_name = _inner_class_name 179 | 180 | _gut._test_prefix = _test_prefix 181 | _gut._file_prefix = _file_prefix 182 | _gut._file_extension = _file_extension 183 | _gut._inner_class_prefix = _inner_class_prefix 184 | _gut._temp_directory = _temp_directory 185 | 186 | _gut.set_should_maximize(_should_maximize) 187 | _gut.set_yield_between_tests(_yield_between_tests) 188 | _gut.disable_strict_datatype_checks(_disable_strict_datatype_checks) 189 | _gut.set_export_path(_export_path) 190 | _gut.set_include_subdirectories(_include_subdirectories) 191 | _gut.set_double_strategy(_double_strategy) 192 | _gut.set_pre_run_script(_pre_run_script) 193 | _gut.set_post_run_script(_post_run_script) 194 | _gut.set_color_output(_color_output) 195 | _gut.show_orphans(_show_orphans) 196 | 197 | get_parent().add_child(_gut) 198 | 199 | if(!_utils.is_version_ok()): 200 | return 201 | 202 | _gut.set_log_level(_log_level) 203 | 204 | _gut.add_directory(_directory1) 205 | _gut.add_directory(_directory2) 206 | _gut.add_directory(_directory3) 207 | _gut.add_directory(_directory4) 208 | _gut.add_directory(_directory5) 209 | _gut.add_directory(_directory6) 210 | 211 | _gut.get_logger().disable_printer('console', !_should_print_to_console) 212 | # When file logging enabled then the log will contain terminal escape 213 | # strings. So when running the scene this is disabled. Also if enabled 214 | # this may cause duplicate entries into the logs. 215 | _gut.get_logger().disable_printer('terminal', true) 216 | 217 | _gut.get_gui().set_font_size(_font_size) 218 | _gut.get_gui().set_font(_font_name) 219 | _gut.get_gui().set_default_font_color(_font_color) 220 | _gut.get_gui().set_background_color(_background_color) 221 | _gut.get_gui().rect_size = rect_size 222 | emit_signal('gut_ready') 223 | 224 | if(_run_on_load): 225 | # Run the test scripts. If one has been selected then only run that one 226 | # otherwise all tests will be run. 227 | var run_rest_of_scripts = _select_script == null 228 | _gut.test_scripts(run_rest_of_scripts) 229 | 230 | func _is_ready_to_go(action): 231 | if(_gut == null): 232 | push_error(str('GUT is not ready for ', action, ' yet. Perform actions on GUT in/after the gut_ready signal.')) 233 | return _gut != null 234 | 235 | func _on_tests_finished(): 236 | emit_signal('tests_finished') 237 | 238 | func get_gut(): 239 | return _gut 240 | 241 | func export_if_tests_found(): 242 | if(_is_ready_to_go('export_if_tests_found')): 243 | _gut.export_if_tests_found() 244 | 245 | func import_tests_if_none_found(): 246 | if(_is_ready_to_go('import_tests_if_none_found') and !_cancel_import): 247 | _gut.import_tests_if_none_found() 248 | -------------------------------------------------------------------------------- /addons/synced/SyncManager.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | # 4 | # Synced lib requires this class to be autoloaded as SyncManager singleton. 5 | # 6 | # Does the RPC to send Input frames (keyboard and mouse sampling status) 7 | # from Client to Server. Stores data in child nodes, see SyncPeer.tscn scene. 8 | # Local player has a special SyncPeer id=0 attached at all times, including 9 | # even when no networking is enabled. 10 | # 11 | 12 | # 13 | # Configuration constants 14 | # 15 | 16 | # 17 | # Example net config of calculation. 18 | # * All assuming Engine.iterations_per_second=60 19 | # * Design goal is smooth interpolation that can tolerate 1 frame of packet loss. 20 | # * Suppose we want server_sendrate = 20. Server sends state every 3 frames. 21 | # * If 1 packet is lost, client will have to cope with a 'hole' of 6 frames 22 | # with no data from server. 23 | # * This is compensated by Client Interpolation. Therefore, 24 | # client_interpolation_lag = 6 25 | # (which is 0.1 sec) 26 | # * client_interpolation_history_size has to exceed client_interpolation_lag. 27 | # = 7 28 | # * Another example. Suppose we have resources send more data from server. 29 | # server_sendrate = 30 # instead of 20 30 | # * In case of 1 frame of packet loss client now has a 'hole' of 4 frames. 31 | # client_interpolation_lag = 4 32 | # * client_interpolation_history_size = 5 33 | # * That said, it's perfectly fine to drop a design goal of perfectly smooth 34 | # interpolation in case of packet loss. You can sacrifice smoothness 35 | # and decrease client_interpolation_lag lower. 36 | 37 | # Client will intentionally lag behind last known server state for this many state_ids, 38 | # rendering game world slightly in the past. 39 | # This allows for smooth interpolation between two server states. 40 | # This should be set so that interpolation can proceed even when one frame of 41 | # network data from server gets dropped. This should be a hardcoded setting 42 | # and should be equal between client and server. 43 | # I.e. Engine.iterations_per_second * 2 / server_sendrate 44 | var client_interpolation_lag = 6 45 | 46 | # How long (in state_ids) should history be for each interpolated property on a client. 47 | # This must exceed client_interpolation_lag plus max latency 48 | var client_interpolation_history_size = 40 49 | 50 | # When switching to CSP, clients will smooth slow down over this many times their latency 51 | var client_csp_period_multiplier = 3.0 52 | 53 | # How many times per second to send property values from Server to Client. 54 | var server_sendrate = 20 55 | 56 | # How long (in state_ids) should history be for each property on a server. 57 | # This determines max lag-compensation span and max time depth. 58 | # Changing this will only affect newly created Synced objects. 59 | # Only change before scene is loaded. 60 | var server_property_history_size = 60 61 | 62 | # How many times per second to send batches of input frames from client on server. 63 | # Client samples at a rate of Engine.iterations_per_second and buffers sampled frames. 64 | # Client sends batches to Server at this rate. 65 | var input_sendrate = 30 66 | 67 | # Batch size when sending input frames. This number of frames is sent at input_sendrate. 68 | # In case input_frames_min_batch * input_sendrate exceeds Engine.iterations_per_second 69 | # it adds redundancy, improving tolerance to packet loss, at the cost of increased traffic. 70 | var input_frames_min_batch = 3 71 | 72 | # Server: max number of frames to pool for later processing. 73 | # (input_frames_history_size-2) * input_sendrate should exceed Engine.iterations_per_second 74 | # input_frames_history_size must exceed input_frames_min_batch. 75 | var input_frames_history_size = 7 76 | 77 | # Server: when no input frames are ready from peer at the moment of consumption, 78 | # server is allowed to copy last valid frame this many times. 79 | # Reasonable value allows to tolerate 1-2 input packets go missing. 80 | var input_prediction_max_frames = 4 81 | 82 | # We want first call to _process() after _physics_process() 83 | # to see integer state_id_frac. But we don't want to interfere 84 | # too much with regularity of state_id_frac. The greater this setting is, 85 | # the stronger we try to make first state_id_frac into integer, 86 | # at the cost of possible large jumps in state_id_frac value 87 | # visible inside _process(). 88 | # 1 will force state_id_frac to be integer first time after _physics_process(). 89 | # 0 will disable trying to change state_id_frac. 90 | var state_id_frac_to_integer_reduction = 0.04 91 | 92 | # Max state_ids client is allowed to extrapolate without data from server 93 | var max_offline_extrapolation = 20 94 | 95 | # Delays processing of received packets on Client by this many seconds, 96 | # simulating network latency. Array of two floats means [min,max] sec, 97 | # null to disable. This applies only once, delaying server->client traffic. 98 | # Client->server traffic is unaffected. 99 | var simulate_network_latency = null # [0.2, 0.3] 100 | 101 | # Refuses to deliver this percent of unreliable packets at random. 102 | # Simulates network packet loss. Applies both to sync (server->client) 103 | # and input (client->server) packets. 104 | var simulate_unreliable_packet_loss_percent = 0.0 105 | 106 | # SyncSequence is responsible for state_id and input_id management between client and server 107 | var seq:SyncSequence = SyncSequence.new(self) 108 | 109 | func _ready(): 110 | get_local_peer() 111 | get_tree().connect("network_peer_connected", self, "_player_connected") 112 | get_tree().connect("network_peer_disconnected", self, "_player_disconnected") 113 | get_tree().connect("server_disconnected", self, "_i_disconnected") 114 | 115 | func _process(delta): 116 | seq._process(delta) 117 | 118 | func _physics_process(delta): 119 | seq._physics_process(delta) 120 | 121 | # Called from SyncPeer. RPC goes through this proxy rather than SyncPeer itself 122 | # because of differences in node path between server and client. 123 | master func receive_input_batch(first_input_id: int, first_state_id: int, sendtable_ids: Array, node_paths: Array, values: Array): 124 | var peer = get_sender_peer() 125 | if peer: 126 | peer.receive_input_batch(first_input_id, first_state_id, sendtable_ids, node_paths, values) 127 | 128 | # Returns an object to read player's input through, 129 | # like a (limited) drop-in replacement of Godot's Input class. 130 | # 0 means local player, same as get_tree().multiplayer.get_network_unique_id() 131 | func get_input_facade(peer_unique_id): 132 | if peer_unique_id == null: 133 | return SyncInputFacade.FakeInputFacade.new() 134 | if peer_unique_id > 0 and get_tree().network_peer and peer_unique_id == get_tree().multiplayer.get_network_unique_id(): 135 | peer_unique_id = 0 136 | # find_node() avoids warnings on client from get_node() that node does not exist 137 | var peer = find_node(str(peer_unique_id), false, false) 138 | if not peer: 139 | return SyncInputFacade.FakeInputFacade.new() 140 | return get_node("%s/SyncInputFacade" % peer_unique_id) 141 | 142 | # Instances of Synced report here upon creation 143 | func synced_created(synced:Synced, _spawner=null): 144 | synced.connect("peer_id_changed", seq, "update_synced_belong_to_players", [synced]) 145 | if synced.belongs_to_peer_id != null: 146 | seq.update_synced_belong_to_players(null, synced.belongs_to_peer_id, synced) 147 | 148 | # Remember this Synced if it contains a client-owned property 149 | for prop in synced.synced_properties: 150 | var p:SyncedProperty = synced.synced_properties[prop] 151 | if p and p.ownership == SyncedProperty.OWNERSHIP_CLIENT_IF_PEER: 152 | _synced_with_client_owned[str( 153 | get_tree().get_root().get_path_to(synced.get_parent()) 154 | )] = weakref(synced) 155 | break 156 | 157 | # All Synced objects with at least one client-owned property; node path from current scene => weakref 158 | var _synced_with_client_owned = {} 159 | 160 | # Update client-owned properties using input frame that came from client over the network 161 | func update_client_owned_properties(peer_id:int, cop_values: Dictionary)->void: 162 | assert(SyncManager.is_server() and peer_id != 0) 163 | var remove = [] 164 | for node_path in _synced_with_client_owned: 165 | var synced:Synced = (_synced_with_client_owned[node_path] as WeakRef).get_ref() 166 | if synced == null: 167 | remove.append(node_path) 168 | elif node_path in cop_values and synced.belongs_to_peer_id == peer_id: 169 | synced.set_client_owned_values(cop_values[node_path]) 170 | for node_path in remove: 171 | _synced_with_client_owned.erase(node_path) 172 | 173 | # Get all tracked client-owned propoerties to add to client input frame 174 | func sample_client_owned_properties()->Dictionary: 175 | assert(SyncManager.is_client()) 176 | var values = {} 177 | var remove = [] 178 | for node_path in _synced_with_client_owned: 179 | var synced:Synced = (_synced_with_client_owned[node_path] as WeakRef).get_ref() 180 | if synced == null: 181 | remove.append(node_path) 182 | elif synced.is_local_peer(): 183 | # Will return empty v when not ready to read yet; ignore 184 | var v = synced.get_client_owned_values() 185 | if v and v.size() > 0: 186 | values[node_path] = v 187 | 188 | for node_path in remove: 189 | _synced_with_client_owned.erase(node_path) 190 | return values 191 | 192 | # Common helper to get coordinate of Spatial and Node2D in a similar way 193 | func get_coord(obj): 194 | if obj is Spatial: 195 | return obj.to_global(Vector3(0, 0, 0)) 196 | elif obj is Node2D: 197 | return obj.to_global(Vector2(0, 0)) 198 | 199 | var SyncPeerScene = preload("res://addons/synced/SyncPeer.tscn") 200 | # We use a special peer_id=0 to designate local peer. 201 | # This saves hustle in case get_tree().multiplayer.get_network_unique_id() 202 | # changes when peer connects and disconnects. 203 | # Local peer exists, InputFacade is safe to use even when multiplayer is disabled. 204 | func get_local_peer(): 205 | var local_peer 206 | 207 | # This check allows to avoid "node nont found" warning when called from _ready 208 | if get_child_count() > 0: 209 | local_peer = get_node("0") 210 | 211 | if not local_peer: 212 | local_peer = SyncPeerScene.instance() 213 | local_peer.name = '0' 214 | self.add_child(local_peer) 215 | 216 | return local_peer 217 | 218 | # True if networking enabled and we're the Server 219 | func is_server(): 220 | return get_tree() and get_tree().network_peer and get_tree().is_network_server() 221 | 222 | # Whether connection to server is currently active. Only maintained on Client. 223 | var _is_connected_to_server = false 224 | 225 | # True if networking enabled and we're a Client 226 | func is_client(): 227 | return get_tree() and get_tree().network_peer and _is_connected_to_server and not get_tree().is_network_server() 228 | 229 | # Returns a SyncPeer child of SyncManager that sent an RPC that is currently 230 | # being processed, or null if no RPC in progress or peer not found for any reason. 231 | func get_sender_peer(): 232 | var peer_id = multiplayer.get_rpc_sender_id() 233 | if peer_id <= 0: 234 | return null 235 | return get_node(str(peer_id)) 236 | 237 | # Fetch remote or local SyncPeer by id 238 | func get_peer(peer_id:int): 239 | return get_node(str(peer_id)) 240 | 241 | # Signals from scene tree networking 242 | func _player_connected(peer_id=null): 243 | if is_server() and peer_id > 0: 244 | var peer = SyncPeerScene.instance() 245 | peer.name = str(peer_id) 246 | self.add_child(peer) 247 | elif not is_server() and peer_id == 1: 248 | _is_connected_to_server = true 249 | seq.reset_client_connection_stats() 250 | 251 | func _player_disconnected(peer_id=null): 252 | if is_server() and peer_id > 0: 253 | var peer = get_node(str(peer_id)) 254 | if peer: 255 | peer.queue_free() 256 | 257 | func _i_disconnected(): 258 | _is_connected_to_server = false 259 | -------------------------------------------------------------------------------- /addons/gut/GutScene.gd: -------------------------------------------------------------------------------- 1 | extends Panel 2 | 3 | onready var _script_list = $ScriptsList 4 | onready var _nav = { 5 | prev = $Navigation/Previous, 6 | next = $Navigation/Next, 7 | run = $Navigation/Run, 8 | current_script = $Navigation/CurrentScript, 9 | run_single = $Navigation/RunSingleScript 10 | } 11 | onready var _progress = { 12 | script = $ScriptProgress, 13 | script_xy = $ScriptProgress/xy, 14 | test = $TestProgress, 15 | test_xy = $TestProgress/xy 16 | } 17 | onready var _summary = { 18 | failing = $Summary/Failing, 19 | passing = $Summary/Passing, 20 | fail_count = 0, 21 | pass_count = 0 22 | } 23 | 24 | onready var _extras = $ExtraOptions 25 | onready var _ignore_pauses = $ExtraOptions/IgnorePause 26 | onready var _continue_button = $Continue/Continue 27 | onready var _text_box = $TextDisplay/RichTextLabel 28 | 29 | onready var _titlebar = { 30 | bar = $TitleBar, 31 | time = $TitleBar/Time, 32 | label = $TitleBar/Title 33 | } 34 | 35 | onready var _user_files = $UserFileViewer 36 | 37 | var _mouse = { 38 | down = false, 39 | in_title = false, 40 | down_pos = null, 41 | in_handle = false 42 | } 43 | var _is_running = false 44 | var _start_time = 0.0 45 | var _time = 0.0 46 | 47 | const DEFAULT_TITLE = 'Gut: The Godot Unit Testing tool.' 48 | var _pre_maximize_rect = null 49 | var _font_size = 20 50 | 51 | signal end_pause 52 | signal ignore_pause 53 | signal log_level_changed 54 | signal run_script 55 | signal run_single_script 56 | 57 | func _ready(): 58 | 59 | if(Engine.editor_hint): 60 | return 61 | 62 | _pre_maximize_rect = get_rect() 63 | _hide_scripts() 64 | _update_controls() 65 | _nav.current_script.set_text("No scripts available") 66 | set_title() 67 | clear_summary() 68 | _titlebar.time.set_text("Time 0.0") 69 | 70 | _extras.visible = false 71 | update() 72 | 73 | set_font_size(_font_size) 74 | set_font('CourierPrime') 75 | 76 | _user_files.set_position(Vector2(10, 30)) 77 | 78 | func elapsed_time_as_str(): 79 | return str("%.1f" % (_time / 1000.0), 's') 80 | 81 | func _process(_delta): 82 | if(_is_running): 83 | _time = OS.get_ticks_msec() - _start_time 84 | _titlebar.time.set_text(str('Time: ', elapsed_time_as_str())) 85 | 86 | func _draw(): # needs get_size() 87 | # Draw the lines in the corner to show where you can 88 | # drag to resize the dialog 89 | var grab_margin = 3 90 | var line_space = 3 91 | var grab_line_color = Color(.4, .4, .4) 92 | for i in range(1, 10): 93 | var x = rect_size - Vector2(i * line_space, grab_margin) 94 | var y = rect_size - Vector2(grab_margin, i * line_space) 95 | draw_line(x, y, grab_line_color, 1, true) 96 | 97 | func _on_Maximize_draw(): 98 | # draw the maximize square thing. 99 | var btn = $TitleBar/Maximize 100 | btn.set_text('') 101 | var w = btn.get_size().x 102 | var h = btn.get_size().y 103 | btn.draw_rect(Rect2(0, 0, w, h), Color(0, 0, 0, 1)) 104 | btn.draw_rect(Rect2(2, 4, w - 4, h - 6), Color(1,1,1,1)) 105 | 106 | func _on_ShowExtras_draw(): 107 | var btn = $Continue/ShowExtras 108 | btn.set_text('') 109 | var start_x = 20 110 | var start_y = 15 111 | var pad = 5 112 | var color = Color(.1, .1, .1, 1) 113 | var width = 2 114 | for i in range(3): 115 | var y = start_y + pad * i 116 | btn.draw_line(Vector2(start_x, y), Vector2(btn.get_size().x - start_x, y), color, width, true) 117 | 118 | # #################### 119 | # GUI Events 120 | # #################### 121 | func _on_Run_pressed(): 122 | _run_mode() 123 | emit_signal('run_script', get_selected_index()) 124 | 125 | func _on_CurrentScript_pressed(): 126 | _toggle_scripts() 127 | 128 | func _on_Previous_pressed(): 129 | _select_script(get_selected_index() - 1) 130 | 131 | func _on_Next_pressed(): 132 | _select_script(get_selected_index() + 1) 133 | 134 | func _on_LogLevelSlider_value_changed(_value): 135 | emit_signal('log_level_changed', $LogLevelSlider.value) 136 | 137 | func _on_Continue_pressed(): 138 | _continue_button.disabled = true 139 | emit_signal('end_pause') 140 | 141 | func _on_IgnorePause_pressed(): 142 | var checked = _ignore_pauses.is_pressed() 143 | emit_signal('ignore_pause', checked) 144 | if(checked): 145 | emit_signal('end_pause') 146 | _continue_button.disabled = true 147 | 148 | func _on_RunSingleScript_pressed(): 149 | _run_mode() 150 | emit_signal('run_single_script', get_selected_index()) 151 | 152 | func _on_ScriptsList_item_selected(index): 153 | var tmr = $ScriptsList/DoubleClickTimer 154 | if(!tmr.is_stopped()): 155 | _run_mode() 156 | emit_signal('run_single_script', get_selected_index()) 157 | tmr.stop() 158 | else: 159 | tmr.start() 160 | 161 | _select_script(index) 162 | 163 | func _on_TitleBar_mouse_entered(): 164 | _mouse.in_title = true 165 | 166 | func _on_TitleBar_mouse_exited(): 167 | _mouse.in_title = false 168 | 169 | func _input(event): 170 | if(event is InputEventMouseButton): 171 | if(event.button_index == 1): 172 | _mouse.down = event.pressed 173 | if(_mouse.down): 174 | _mouse.down_pos = event.position 175 | 176 | if(_mouse.in_title): 177 | if(event is InputEventMouseMotion and _mouse.down): 178 | set_position(get_position() + (event.position - _mouse.down_pos)) 179 | _mouse.down_pos = event.position 180 | _pre_maximize_rect = get_rect() 181 | 182 | if(_mouse.in_handle): 183 | if(event is InputEventMouseMotion and _mouse.down): 184 | var new_size = rect_size + event.position - _mouse.down_pos 185 | var new_mouse_down_pos = event.position 186 | rect_size = new_size 187 | _mouse.down_pos = new_mouse_down_pos 188 | _pre_maximize_rect = get_rect() 189 | 190 | func _on_ResizeHandle_mouse_entered(): 191 | _mouse.in_handle = true 192 | 193 | func _on_ResizeHandle_mouse_exited(): 194 | _mouse.in_handle = false 195 | 196 | func _on_RichTextLabel_gui_input(ev): 197 | pass 198 | # leaving this b/c it is wired up and might have to send 199 | # more signals through 200 | 201 | func _on_Copy_pressed(): 202 | OS.clipboard = _text_box.text 203 | 204 | func _on_ShowExtras_toggled(button_pressed): 205 | _extras.visible = button_pressed 206 | 207 | func _on_Maximize_pressed(): 208 | if(get_rect() == _pre_maximize_rect): 209 | maximize() 210 | else: 211 | rect_size = _pre_maximize_rect.size 212 | rect_position = _pre_maximize_rect.position 213 | # #################### 214 | # Private 215 | # #################### 216 | func _run_mode(is_running=true): 217 | if(is_running): 218 | _start_time = OS.get_ticks_msec() 219 | _time = 0.0 220 | clear_summary() 221 | _is_running = is_running 222 | 223 | _hide_scripts() 224 | var ctrls = $Navigation.get_children() 225 | for i in range(ctrls.size()): 226 | ctrls[i].disabled = is_running 227 | 228 | func _select_script(index): 229 | var text = _script_list.get_item_text(index) 230 | var max_len = 50 231 | if(text.length() > max_len): 232 | text = '...' + text.right(text.length() - (max_len - 5)) 233 | $Navigation/CurrentScript.set_text(text) 234 | _script_list.select(index) 235 | _update_controls() 236 | 237 | func _toggle_scripts(): 238 | if(_script_list.visible): 239 | _hide_scripts() 240 | else: 241 | _show_scripts() 242 | 243 | func _show_scripts(): 244 | _script_list.show() 245 | 246 | func _hide_scripts(): 247 | _script_list.hide() 248 | 249 | func _update_controls(): 250 | var is_empty = _script_list.get_selected_items().size() == 0 251 | if(is_empty): 252 | _nav.next.disabled = true 253 | _nav.prev.disabled = true 254 | else: 255 | var index = get_selected_index() 256 | _nav.prev.disabled = index <= 0 257 | _nav.next.disabled = index >= _script_list.get_item_count() - 1 258 | 259 | _nav.run.disabled = is_empty 260 | _nav.current_script.disabled = is_empty 261 | _nav.run_single.disabled = is_empty 262 | 263 | func _update_summary(): 264 | if(!_summary): 265 | return 266 | 267 | var total = _summary.fail_count + _summary.pass_count 268 | $Summary.visible = !total == 0 269 | $Summary/AssertCount.text = str('Failures ', _summary.fail_count, '/', total) 270 | # #################### 271 | # Public 272 | # #################### 273 | func run_mode(is_running=true): 274 | _run_mode(is_running) 275 | 276 | func set_scripts(scripts): 277 | _script_list.clear() 278 | for i in range(scripts.size()): 279 | _script_list.add_item(scripts[i]) 280 | _select_script(0) 281 | _update_controls() 282 | 283 | func select_script(index): 284 | _select_script(index) 285 | 286 | func get_selected_index(): 287 | return _script_list.get_selected_items()[0] 288 | 289 | func get_log_level(): 290 | return $LogLevelSlider.value 291 | 292 | func set_log_level(value): 293 | var new_value = value 294 | if(new_value == null): 295 | new_value = 0 296 | $LogLevelSlider.value = new_value 297 | 298 | func set_ignore_pause(should): 299 | _ignore_pauses.pressed = should 300 | 301 | func get_ignore_pause(): 302 | return _ignore_pauses.pressed 303 | 304 | func get_text_box(): 305 | # due to some timing issue, this cannot return _text_box but can return 306 | # this. 307 | return $TextDisplay/RichTextLabel 308 | 309 | func end_run(): 310 | _run_mode(false) 311 | _update_controls() 312 | 313 | func set_progress_script_max(value): 314 | var max_val = max(value, 1) 315 | _progress.script.set_max(max_val) 316 | _progress.script_xy.set_text(str('0/', max_val)) 317 | 318 | func set_progress_script_value(value): 319 | _progress.script.set_value(value) 320 | var txt = str(value, '/', _progress.test.get_max()) 321 | _progress.script_xy.set_text(txt) 322 | 323 | func set_progress_test_max(value): 324 | var max_val = max(value, 1) 325 | _progress.test.set_max(max_val) 326 | _progress.test_xy.set_text(str('0/', max_val)) 327 | 328 | func set_progress_test_value(value): 329 | _progress.test.set_value(value) 330 | var txt = str(value, '/', _progress.test.get_max()) 331 | _progress.test_xy.set_text(txt) 332 | 333 | func clear_progress(): 334 | _progress.test.set_value(0) 335 | _progress.script.set_value(0) 336 | 337 | func pause(): 338 | _continue_button.disabled = false 339 | 340 | func set_title(title=null): 341 | if(title == null): 342 | $TitleBar/Title.set_text(DEFAULT_TITLE) 343 | else: 344 | $TitleBar/Title.set_text(title) 345 | 346 | func add_passing(amount=1): 347 | if(!_summary): 348 | return 349 | _summary.pass_count += amount 350 | _update_summary() 351 | 352 | func add_failing(amount=1): 353 | if(!_summary): 354 | return 355 | _summary.fail_count += amount 356 | _update_summary() 357 | 358 | func clear_summary(): 359 | _summary.fail_count = 0 360 | _summary.pass_count = 0 361 | _update_summary() 362 | 363 | func maximize(): 364 | if(is_inside_tree()): 365 | var vp_size_offset = get_viewport().size 366 | rect_size = vp_size_offset / get_scale() 367 | set_position(Vector2(0, 0)) 368 | 369 | func clear_text(): 370 | _text_box.bbcode_text = '' 371 | 372 | func scroll_to_bottom(): 373 | pass 374 | #_text_box.cursor_set_line(_gui.get_text_box().get_line_count()) 375 | 376 | func _set_font_size_for_rtl(rtl, new_size): 377 | if(rtl.get('custom_fonts/normal_font') != null): 378 | rtl.get('custom_fonts/bold_italics_font').size = new_size 379 | rtl.get('custom_fonts/bold_font').size = new_size 380 | rtl.get('custom_fonts/italics_font').size = new_size 381 | rtl.get('custom_fonts/normal_font').size = new_size 382 | 383 | 384 | func _set_fonts_for_rtl(rtl, base_font_name): 385 | pass 386 | 387 | 388 | func set_font_size(new_size): 389 | _font_size = new_size 390 | _set_font_size_for_rtl(_text_box, new_size) 391 | _set_font_size_for_rtl(_user_files.get_rich_text_label(), new_size) 392 | 393 | 394 | func _set_font(rtl, font_name, custom_name): 395 | if(font_name == null): 396 | rtl.set('custom_fonts/' + custom_name, null) 397 | else: 398 | var dyn_font = DynamicFont.new() 399 | var font_data = DynamicFontData.new() 400 | font_data.font_path = 'res://addons/gut/fonts/' + font_name + '.ttf' 401 | font_data.antialiased = true 402 | dyn_font.font_data = font_data 403 | rtl.set('custom_fonts/' + custom_name, dyn_font) 404 | 405 | func _set_all_fonts_in_ftl(ftl, base_name): 406 | if(base_name == 'Default'): 407 | _set_font(ftl, null, 'normal_font') 408 | _set_font(ftl, null, 'bold_font') 409 | _set_font(ftl, null, 'italics_font') 410 | _set_font(ftl, null, 'bold_italics_font') 411 | else: 412 | _set_font(ftl, base_name + '-Regular', 'normal_font') 413 | _set_font(ftl, base_name + '-Bold', 'bold_font') 414 | _set_font(ftl, base_name + '-Italic', 'italics_font') 415 | _set_font(ftl, base_name + '-BoldItalic', 'bold_italics_font') 416 | set_font_size(_font_size) 417 | 418 | func set_font(base_name): 419 | _set_all_fonts_in_ftl(_text_box, base_name) 420 | _set_all_fonts_in_ftl(_user_files.get_rich_text_label(), base_name) 421 | 422 | func set_default_font_color(color): 423 | _text_box.set('custom_colors/default_color', color) 424 | 425 | func set_background_color(color): 426 | $TextDisplay.color = color 427 | 428 | func _on_UserFiles_pressed(): 429 | _user_files.show_open() 430 | 431 | func get_waiting_label(): 432 | return $TextDisplay/WaitingLabel 433 | -------------------------------------------------------------------------------- /test/unit/test_SyncedProperty.gd: -------------------------------------------------------------------------------- 1 | extends "res://addons/gut/test.gd" 2 | 3 | func test_no_interpolation(): 4 | var prop = autofree(SyncedProperty.new({})) 5 | assert_eq(SyncedProperty.NO_INTERPOLATION, prop.interpolation) 6 | assert_eq(SyncedProperty.NO_INTERPOLATION, prop.missing_state_interpolation) 7 | prop.resize(10) 8 | prop.write(12, 100.0) 9 | assert_eq(100.0, prop.last()) 10 | assert_eq(10, prop.container.size()) 11 | assert_eq(12, prop.last_state_id) 12 | assert_eq(12, prop.last_changed_state_id) 13 | assert_eq(10, prop.container.count(100.0)) 14 | prop.write(15, 200.0) 15 | assert_eq(15, prop.last_changed_state_id) 16 | assert_eq(100.0, prop.last(1)) 17 | assert_eq(200.0, prop.last(0)) 18 | assert_eq(15, prop.last_state_id) 19 | assert_eq(1, prop.container.count(200.0)) 20 | assert_eq(200.0, prop.container[prop.last_index]) 21 | assert_eq(200.0, prop.read(15)) # int state id, no interpolation 22 | assert_eq(200.0, prop.read(16)) # (no)extrapolation 23 | assert_eq(100.0, prop.read(14.5)) # (no)interpolation between different values 24 | assert_eq(100.0, prop.read(13.5)) # (no)interpolation between equal values 25 | assert_eq(100.0, prop.read(1)) # value older than stored 26 | #gut.p('%s; last_state_id=%s, last_index=%s' % [prop.container, prop.last_state_id, prop.last_index]) 27 | 28 | func test_linear_interpolation(): 29 | var prop = autofree(SyncedProperty.new({ 30 | interpolation = SyncedProperty.LINEAR_INTERPOLATION, 31 | missing_state_interpolation = SyncedProperty.LINEAR_INTERPOLATION 32 | })) 33 | assert_false(prop.ready_to_read()) 34 | assert_false(prop.ready_to_write()) 35 | assert_eq(SyncedProperty.LINEAR_INTERPOLATION, prop.interpolation) 36 | assert_eq(SyncedProperty.LINEAR_INTERPOLATION, prop.missing_state_interpolation) 37 | prop.resize(6) 38 | assert_false(prop.ready_to_read()) 39 | assert_true(prop.ready_to_write()) 40 | prop.write(11, 111.0) 41 | assert_true(prop.ready_to_read()) 42 | assert_true(prop.ready_to_write()) 43 | assert_eq(11, prop.last_state_id) 44 | prop.write(12, 112.0) 45 | assert_eq(12, prop.last_state_id) 46 | prop.write(14, 114.0) 47 | assert_eq(14, prop.last_state_id) 48 | prop.write(18, 118.0) 49 | assert_eq(18, prop.last_state_id) 50 | assert_eq(6, prop.container.size()) 51 | 52 | assert_eq(118.0, prop.container[prop.last_index]) 53 | assert_eq(114.0, prop.read(14)) # int state id, no interpolation 54 | assert_eq(120.0, prop.read(20)) # extrapolation into future int state 55 | assert_eq(120.3, prop.read(20.3)) # extrapolation into future float state 56 | assert_eq(115.0, prop.read(15)) # interpolation between different values, int 57 | assert_eq(113.5, prop.read(13.5)) # interpolation between different values, float 58 | assert_eq(113.0, prop.read(13)) # oldest value stored 59 | assert_eq(113.0, prop.read(12)) # value older than stored 60 | assert_eq(118.0, prop.last()) 61 | assert_eq(117.0, prop.last(1)) 62 | assert_eq(18, prop.last_changed_state_id) 63 | 64 | prop.write(19, 118.0) 65 | assert_eq(18, prop.last_changed_state_id) 66 | assert_eq(118.0, prop.last()) 67 | assert_eq(118.0, prop.last(1)) 68 | assert_eq(117.0, prop.last(2)) 69 | assert_eq(19, prop.last_state_id) 70 | assert_eq(118.0, prop.read(18.5)) # interpolation between equal values 71 | #gut.p('%s; last_state_id=%s, last_index=%s' % [prop.container, prop.last_state_id, prop.last_index]) 72 | 73 | func test_changed(): 74 | var prop = autofree(SyncedProperty.new({})) 75 | prop.resize(10) 76 | prop.write(11, 111.0) 77 | assert_eq(11, prop.last_changed_state_id) 78 | assert_true(prop.changed(10)) 79 | assert_false(prop.changed(11)) 80 | prop.write(12, 112.0) 81 | assert_true(prop.changed(11)) 82 | assert_false(prop.changed(12)) 83 | assert_true(prop.changed(11, 12)) 84 | prop.write(13, 113.0) 85 | assert_eq(13, prop.last_changed_state_id) 86 | prop.write(14, 114.0) 87 | prop.write(15, 115.0) 88 | prop.write(16, 116.0) 89 | assert_eq(16, prop.last_changed_state_id) 90 | prop.write(17, 116.0) 91 | assert_eq(16, prop.last_changed_state_id) 92 | prop.write(20, 116.0) 93 | assert_eq(16, prop.last_changed_state_id) 94 | assert_true(prop.changed(11)) 95 | assert_true(prop.changed(15)) 96 | assert_true(prop.changed(15, 16)) 97 | assert_true(prop.changed(15, 20)) 98 | assert_false(prop.changed(16)) 99 | assert_false(prop.changed(16, 17)) 100 | assert_false(prop.changed(16, 20)) 101 | prop.write(24, 124.0) 102 | assert_eq(24, prop.last_changed_state_id) 103 | assert_true(prop.changed(15)) 104 | assert_true(prop.changed(16)) 105 | assert_true(prop.changed(20)) 106 | 107 | func test_get_negative(): 108 | var prop = autofree(SyncedProperty.new({})) 109 | prop.resize(4) 110 | prop.write(11, 111.0) 111 | prop.write(12, 112.0) 112 | prop.write(13, 113.0) 113 | prop.write(14, 114.0) 114 | assert_eq(114.0, prop._get(-1)) 115 | 116 | func test_get_index(): 117 | var prop = autofree(SyncedProperty.new({})) 118 | prop.resize(4) 119 | prop.write(11, 111.0) 120 | prop.write(12, 112.0) 121 | prop.write(13, 113.0) 122 | prop.write(14, 114.0) 123 | prop.write(15, 115.0) 124 | assert_eq(15, prop.last_state_id) 125 | assert_eq(112.0, prop.container[prop._get_index(11)]) 126 | assert_eq(115.0, prop.container[prop._get_index(16)]) 127 | for i in range(12, 16): 128 | assert_eq(100.0+i, prop.container[prop._get_index(i)]) 129 | 130 | func test_shouldsend_reliable_unreliable(strat=use_parameters([SyncedProperty.RELIABLE_SYNC, SyncedProperty.UNRELIABLE_SYNC])): 131 | var prop = autofree(SyncedProperty.new({ 132 | sync_strategy = strat, 133 | strat_stale_delay = 2 134 | })) 135 | assert_eq(strat, prop.sync_strategy) 136 | prop.resize(10) 137 | prop.write(11, 111.0) 138 | prop.write(15, 115.0) 139 | assert_eq([strat, 115.0], prop.shouldsend(14)) 140 | assert_null(prop.shouldsend(15)) 141 | prop.write(17, 115.0) 142 | assert_eq(15, prop.last_changed_state_id) 143 | assert_false(prop.changed(15)) 144 | assert_eq([strat, 115.0], prop.shouldsend(14)) 145 | assert_eq([strat, 115.0], prop.shouldsend(12)) 146 | assert_null(prop.shouldsend(15)) 147 | assert_null(prop.shouldsend(17)) 148 | assert_null(prop.shouldsend(18)) 149 | prop.write(19, 115.0) 150 | assert_eq([strat, 115.0], prop.shouldsend(12)) 151 | assert_null(prop.shouldsend(17)) 152 | 153 | func test_shouldsend_auto(): 154 | var strat = SyncedProperty.AUTO_SYNC 155 | var prop = autofree(SyncedProperty.new({ 156 | sync_strategy = strat, 157 | strat_stale_delay = 2 158 | })) 159 | assert_eq(strat, prop.sync_strategy) 160 | prop.resize(10) 161 | prop.write(11, 111.0) 162 | prop.write(15, 115.0) 163 | assert_eq([strat, 115.0], prop.shouldsend(14)) 164 | assert_null(prop.shouldsend(15)) 165 | prop.write(17, 115.0) 166 | assert_eq(15, prop.last_changed_state_id) 167 | assert_false(prop.changed(15)) 168 | assert_eq([strat, 115.0], prop.shouldsend(14)) 169 | assert_eq([strat, 115.0], prop.shouldsend(12)) 170 | assert_null(prop.shouldsend(15)) 171 | assert_null(prop.shouldsend(17)) 172 | assert_null(prop.shouldsend(18)) 173 | prop.write(18, 115.0) 174 | assert_eq([SyncedProperty.RELIABLE_SYNC, 115.0], prop.shouldsend(13)) 175 | assert_null(prop.shouldsend(17)) 176 | prop.write(21, 116.0) 177 | assert_eq([strat, 116.0], prop.shouldsend(20)) 178 | 179 | func test_shouldsend_do_not_sync(): 180 | var strat = SyncedProperty.DO_NOT_SYNC 181 | var prop = autofree(SyncedProperty.new({ 182 | sync_strategy = strat, 183 | strat_stale_delay = 2 184 | })) 185 | assert_eq(strat, prop.sync_strategy) 186 | prop.resize(10) 187 | prop.write(11, 111.0) 188 | prop.write(15, 115.0) 189 | assert_null(prop.shouldsend(14)) 190 | assert_null(prop.shouldsend(15)) 191 | prop.write(17, 115.0) 192 | assert_eq(15, prop.last_changed_state_id) 193 | assert_false(prop.changed(15)) 194 | assert_null(prop.shouldsend(14)) 195 | assert_null(prop.shouldsend(12)) 196 | assert_null(prop.shouldsend(15)) 197 | assert_null(prop.shouldsend(17)) 198 | assert_null(prop.shouldsend(18)) 199 | prop.write(18, 115.0) 200 | assert_null(prop.shouldsend(13)) 201 | assert_null(prop.shouldsend(17)) 202 | prop.write(21, 116.0) 203 | assert_null(prop.shouldsend(20)) 204 | 205 | func test_index_to_state_id(): 206 | var prop = autofree(SyncedProperty.new({ 207 | interpolation = SyncedProperty.LINEAR_INTERPOLATION, 208 | missing_state_interpolation = SyncedProperty.LINEAR_INTERPOLATION 209 | })) 210 | prop.resize(9) 211 | prop.write(2, 102.0) 212 | prop.write(11, 111.0) 213 | prop.write(15, 115.0) 214 | assert_eq(4, prop.last_index) 215 | assert_eq(prop._index_to_state_id(0), 11) 216 | assert_eq(prop._index_to_state_id(1), 12) 217 | assert_eq(prop._index_to_state_id(2), 13) 218 | assert_eq(prop._index_to_state_id(3), 14) 219 | assert_eq(prop._index_to_state_id(4), 15) 220 | assert_eq(prop._index_to_state_id(5), 7) 221 | assert_eq(prop._index_to_state_id(6), 8) 222 | assert_eq(prop._index_to_state_id(7), 9) 223 | assert_eq(prop._index_to_state_id(8), 10) 224 | 225 | func test_re_interpolate1(): 226 | var prop = autofree(SyncedProperty.new({ 227 | interpolation = SyncedProperty.LINEAR_INTERPOLATION, 228 | missing_state_interpolation = SyncedProperty.LINEAR_INTERPOLATION 229 | })) 230 | prop.resize(9) 231 | # 11 12 13 14 15 16 17 18 19 232 | # 111 112 113 114 115 114 113 112 111 233 | prop.write(11, 111.0) 234 | prop.write(19, 111.0) 235 | assert_eq(9, prop.container.count(111.0)) 236 | prop.write(15, 115.0) 237 | assert_eq(19, prop.last_state_id) 238 | assert_eq(111.0, prop.read(11)) 239 | assert_eq(112.0, prop.read(12)) 240 | assert_eq(113.0, prop.read(13)) 241 | assert_eq(114.0, prop.read(14)) 242 | assert_eq(115.0, prop.read(15)) 243 | assert_eq(114.0, prop.read(16)) 244 | assert_eq(113.0, prop.read(17)) 245 | assert_eq(112.0, prop.read(18)) 246 | assert_eq(111.0, prop.read(19)) 247 | 248 | func test_re_interpolate2(): 249 | var prop = autofree(SyncedProperty.new({ 250 | interpolation = SyncedProperty.LINEAR_INTERPOLATION, 251 | missing_state_interpolation = SyncedProperty.LINEAR_INTERPOLATION 252 | })) 253 | prop.resize(8) 254 | # 0 1 2 3 4 5 6 7 255 | # i i noi i i noi i noi 256 | # 130 131 132 133 134 (135) 128 129 257 | # 1130 1131 1132 1133 1134 1135 1128 1129 258 | prop.write(122, 1122.0) 259 | prop.write(129, 1129.0) 260 | prop.write(132, 1132.0) 261 | prop.write(135, 1135.0) 262 | assert_eq([true, true, false, true, true, false, true, false], prop.is_interpolated) 263 | assert_eq(135, prop.last_state_id) 264 | assert_eq(5, prop.last_index) 265 | var old_state = prop.container.duplicate() 266 | prop.write(135, 1135.0) 267 | assert_eq(old_state, prop.container) 268 | 269 | func test_rollback1(): 270 | var prop = autofree(SyncedProperty.new({ 271 | interpolation = SyncedProperty.LINEAR_INTERPOLATION, 272 | missing_state_interpolation = SyncedProperty.LINEAR_INTERPOLATION 273 | })) 274 | assert_false(bool(prop.last_rollback_from_state_id)) 275 | assert_false(bool(prop.last_rollback_to_state_id)) 276 | prop.resize(7) 277 | prop.write(22, 122.0) 278 | prop.write(27, 127.0) 279 | prop.rollback(25) 280 | assert_eq(25, prop.last_state_id) 281 | assert_eq(25, prop.last_rollback_to_state_id) 282 | assert_eq(27, prop.last_rollback_from_state_id) 283 | assert_eq(125.0, prop._get(-1)) 284 | assert_eq(122.0, prop.read(3)) 285 | # 22 23 24 25 26 27 28 29 286 | # 122 123 124 125 124 123 122 121 287 | prop.write(26, 124.0) 288 | assert_eq(26, prop.last_state_id) 289 | assert_eq(124.0, prop._get(-1)) 290 | assert_eq(124.0, prop.read(26)) 291 | assert_eq(125.0, prop.read(25)) 292 | prop.write(29, 121.0) 293 | assert_eq(29, prop.last_state_id) 294 | assert_eq(121.0, prop._get(-1)) 295 | assert_eq(121.0, prop.read(29)) 296 | assert_eq(122.0, prop.read(28)) 297 | assert_eq(123.0, prop.read(27)) 298 | assert_eq(124.0, prop.read(26)) 299 | assert_eq(125.0, prop.read(25)) 300 | 301 | # Double rollback 302 | func test_rollback2(): 303 | var prop = autofree(SyncedProperty.new({ 304 | interpolation = SyncedProperty.LINEAR_INTERPOLATION, 305 | missing_state_interpolation = SyncedProperty.LINEAR_INTERPOLATION 306 | })) 307 | assert_false(bool(prop.last_rollback_from_state_id)) 308 | assert_false(bool(prop.last_rollback_to_state_id)) 309 | prop.resize(7) 310 | prop.write(22, 122.0) 311 | prop.write(27, 127.0) 312 | prop.rollback(25) 313 | assert_eq(25, prop.last_state_id) 314 | assert_eq(25, prop.last_rollback_to_state_id) 315 | assert_eq(27, prop.last_rollback_from_state_id) 316 | assert_eq(125.0, prop._get(-1)) 317 | assert_eq(122.0, prop.read(3)) 318 | # 22 23 24 25 26 27 28 29 319 | # 122 123 124 125 124 123 122 121 320 | prop.write(26, 124.0) 321 | assert_eq(26, prop.last_state_id) 322 | assert_eq(124.0, prop._get(-1)) 323 | assert_eq(124.0, prop.read(26)) 324 | assert_eq(125.0, prop.read(25)) 325 | prop.rollback(23) 326 | assert_eq(23, prop.last_state_id) 327 | assert_eq(23, prop.last_rollback_to_state_id) 328 | assert_eq(27, prop.last_rollback_from_state_id) 329 | assert_eq(123.0, prop._get(-1)) 330 | assert_eq(123.0, prop.read(23)) 331 | assert_eq(122.0, prop.read(22)) 332 | prop.write(25, 125.0) 333 | prop.write(29, 121.0) 334 | assert_eq(29, prop.last_state_id) 335 | assert_eq(121.0, prop._get(-1)) 336 | assert_eq(121.0, prop.read(29)) 337 | assert_eq(122.0, prop.read(28)) 338 | assert_eq(123.0, prop.read(27)) 339 | assert_eq(124.0, prop.read(26)) 340 | assert_eq(125.0, prop.read(25)) 341 | assert_eq(124.0, prop.read(24)) 342 | assert_eq(123.0, prop.read(23)) 343 | --------------------------------------------------------------------------------