commit 2747ffa9b0bf91a5d6dced5929f4de52487e338c Author: mstar Date: Thu Nov 28 18:31:55 2024 +0100 Init diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8ad74f7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Normalize EOL for all files that Git considers text files. +* text=auto eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0af181c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# Godot 4+ specific ignores +.godot/ +/android/ diff --git a/.plugged/index.cfg b/.plugged/index.cfg new file mode 100644 index 0000000..6a781cd --- /dev/null +++ b/.plugged/index.cfg @@ -0,0 +1,3 @@ +[plugin] + +installed={} diff --git a/addons/gd-plug-ui/Utils.gd b/addons/gd-plug-ui/Utils.gd new file mode 100644 index 0000000..2904937 --- /dev/null +++ b/addons/gd-plug-ui/Utils.gd @@ -0,0 +1,27 @@ +static func expected_version(current, later_or_equal, before=""): + var is_expected = false + is_expected = compare_version_string(current, later_or_equal) >= 0 + if not is_expected: + return is_expected + + if before.length() > 0: + is_expected = compare_version_string(current, before) < 0 + + return is_expected + +static func compare_version_string(v1, v2): + var v1_arr = v1.split(".") + var v2_arr = v2.split(".") + assert(v1_arr.size() == 3) + assert(v2_arr.size() == 3) + var value = 0 + for i in 3: + var n1 = int(v1_arr[i]) + var n2 = int(v2_arr[i]) + value = compare_number(n1, n2) + if value != 0: + break + return value + +static func compare_number(v1, v2): + return 0 if v1 == v2 else (1 if v1 > v2 else -1) diff --git a/addons/gd-plug-ui/assets/icons/add.png b/addons/gd-plug-ui/assets/icons/add.png new file mode 100644 index 0000000..2fd66b5 Binary files /dev/null and b/addons/gd-plug-ui/assets/icons/add.png differ diff --git a/addons/gd-plug-ui/assets/icons/add.png.import b/addons/gd-plug-ui/assets/icons/add.png.import new file mode 100644 index 0000000..c18c6a3 --- /dev/null +++ b/addons/gd-plug-ui/assets/icons/add.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://djvngsd55wxhs" +path="res://.godot/imported/add.png-621a4be1b942ed06c9140cb74c31abe2.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gd-plug-ui/assets/icons/add.png" +dest_files=["res://.godot/imported/add.png-621a4be1b942ed06c9140cb74c31abe2.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gd-plug-ui/assets/icons/edit_internal.png b/addons/gd-plug-ui/assets/icons/edit_internal.png new file mode 100644 index 0000000..dac0e4b Binary files /dev/null and b/addons/gd-plug-ui/assets/icons/edit_internal.png differ diff --git a/addons/gd-plug-ui/assets/icons/edit_internal.png.import b/addons/gd-plug-ui/assets/icons/edit_internal.png.import new file mode 100644 index 0000000..ca1cb0f --- /dev/null +++ b/addons/gd-plug-ui/assets/icons/edit_internal.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://vv157kc8qi2c" +path="res://.godot/imported/edit_internal.png-061b166639fc3fd2f8ef9610c485c3fa.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gd-plug-ui/assets/icons/edit_internal.png" +dest_files=["res://.godot/imported/edit_internal.png-061b166639fc3fd2f8ef9610c485c3fa.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gd-plug-ui/assets/icons/import_check.png b/addons/gd-plug-ui/assets/icons/import_check.png new file mode 100644 index 0000000..f3423e7 Binary files /dev/null and b/addons/gd-plug-ui/assets/icons/import_check.png differ diff --git a/addons/gd-plug-ui/assets/icons/import_check.png.import b/addons/gd-plug-ui/assets/icons/import_check.png.import new file mode 100644 index 0000000..7903a60 --- /dev/null +++ b/addons/gd-plug-ui/assets/icons/import_check.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bcqbcs5lk7njk" +path="res://.godot/imported/import_check.png-2f45ffda2b43464b03cd006cdaf7e772.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gd-plug-ui/assets/icons/import_check.png" +dest_files=["res://.godot/imported/import_check.png-2f45ffda2b43464b03cd006cdaf7e772.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gd-plug-ui/assets/icons/import_fail.png b/addons/gd-plug-ui/assets/icons/import_fail.png new file mode 100644 index 0000000..716c756 Binary files /dev/null and b/addons/gd-plug-ui/assets/icons/import_fail.png differ diff --git a/addons/gd-plug-ui/assets/icons/import_fail.png.import b/addons/gd-plug-ui/assets/icons/import_fail.png.import new file mode 100644 index 0000000..2c92336 --- /dev/null +++ b/addons/gd-plug-ui/assets/icons/import_fail.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://lffm4bx4h2y2" +path="res://.godot/imported/import_fail.png-051695dac0afab3cc15844d66713cd8d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gd-plug-ui/assets/icons/import_fail.png" +dest_files=["res://.godot/imported/import_fail.png-051695dac0afab3cc15844d66713cd8d.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gd-plug-ui/assets/icons/progress/progress1.png b/addons/gd-plug-ui/assets/icons/progress/progress1.png new file mode 100644 index 0000000..622d889 Binary files /dev/null and b/addons/gd-plug-ui/assets/icons/progress/progress1.png differ diff --git a/addons/gd-plug-ui/assets/icons/progress/progress1.png.import b/addons/gd-plug-ui/assets/icons/progress/progress1.png.import new file mode 100644 index 0000000..7828a36 --- /dev/null +++ b/addons/gd-plug-ui/assets/icons/progress/progress1.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cgts01urkfo46" +path="res://.godot/imported/progress1.png-b701146896a0cd13b3b1213eca4aefaf.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gd-plug-ui/assets/icons/progress/progress1.png" +dest_files=["res://.godot/imported/progress1.png-b701146896a0cd13b3b1213eca4aefaf.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gd-plug-ui/assets/icons/progress/progress2.png b/addons/gd-plug-ui/assets/icons/progress/progress2.png new file mode 100644 index 0000000..8ee4e2b Binary files /dev/null and b/addons/gd-plug-ui/assets/icons/progress/progress2.png differ diff --git a/addons/gd-plug-ui/assets/icons/progress/progress2.png.import b/addons/gd-plug-ui/assets/icons/progress/progress2.png.import new file mode 100644 index 0000000..808edb3 --- /dev/null +++ b/addons/gd-plug-ui/assets/icons/progress/progress2.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cj7jy1jh5cutb" +path="res://.godot/imported/progress2.png-b7faf28624cb0f60e429a592a58c6ee4.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gd-plug-ui/assets/icons/progress/progress2.png" +dest_files=["res://.godot/imported/progress2.png-b7faf28624cb0f60e429a592a58c6ee4.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gd-plug-ui/assets/icons/progress/progress3.png b/addons/gd-plug-ui/assets/icons/progress/progress3.png new file mode 100644 index 0000000..78772d8 Binary files /dev/null and b/addons/gd-plug-ui/assets/icons/progress/progress3.png differ diff --git a/addons/gd-plug-ui/assets/icons/progress/progress3.png.import b/addons/gd-plug-ui/assets/icons/progress/progress3.png.import new file mode 100644 index 0000000..1282346 --- /dev/null +++ b/addons/gd-plug-ui/assets/icons/progress/progress3.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dymxcsmwminoi" +path="res://.godot/imported/progress3.png-f5d97e18b2237ef205acfdce7d4133d6.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gd-plug-ui/assets/icons/progress/progress3.png" +dest_files=["res://.godot/imported/progress3.png-f5d97e18b2237ef205acfdce7d4133d6.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gd-plug-ui/assets/icons/progress/progress4.png b/addons/gd-plug-ui/assets/icons/progress/progress4.png new file mode 100644 index 0000000..8bb7d9e Binary files /dev/null and b/addons/gd-plug-ui/assets/icons/progress/progress4.png differ diff --git a/addons/gd-plug-ui/assets/icons/progress/progress4.png.import b/addons/gd-plug-ui/assets/icons/progress/progress4.png.import new file mode 100644 index 0000000..ae098d7 --- /dev/null +++ b/addons/gd-plug-ui/assets/icons/progress/progress4.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://wosx7pjrwtiw" +path="res://.godot/imported/progress4.png-ea4cf0010c5c85d77c4678da413c2ae8.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gd-plug-ui/assets/icons/progress/progress4.png" +dest_files=["res://.godot/imported/progress4.png-ea4cf0010c5c85d77c4678da413c2ae8.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gd-plug-ui/assets/icons/progress/progress5.png b/addons/gd-plug-ui/assets/icons/progress/progress5.png new file mode 100644 index 0000000..58f0068 Binary files /dev/null and b/addons/gd-plug-ui/assets/icons/progress/progress5.png differ diff --git a/addons/gd-plug-ui/assets/icons/progress/progress5.png.import b/addons/gd-plug-ui/assets/icons/progress/progress5.png.import new file mode 100644 index 0000000..1c74953 --- /dev/null +++ b/addons/gd-plug-ui/assets/icons/progress/progress5.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://kqh2l4lmud66" +path="res://.godot/imported/progress5.png-1a5a4b8c789b45a8292beed39d7d3a40.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gd-plug-ui/assets/icons/progress/progress5.png" +dest_files=["res://.godot/imported/progress5.png-1a5a4b8c789b45a8292beed39d7d3a40.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gd-plug-ui/assets/icons/progress/progress6.png b/addons/gd-plug-ui/assets/icons/progress/progress6.png new file mode 100644 index 0000000..2370d01 Binary files /dev/null and b/addons/gd-plug-ui/assets/icons/progress/progress6.png differ diff --git a/addons/gd-plug-ui/assets/icons/progress/progress6.png.import b/addons/gd-plug-ui/assets/icons/progress/progress6.png.import new file mode 100644 index 0000000..acfbca1 --- /dev/null +++ b/addons/gd-plug-ui/assets/icons/progress/progress6.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cgpnu44qyu6k3" +path="res://.godot/imported/progress6.png-291be2c54ac04191c46b210bc746b5d8.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gd-plug-ui/assets/icons/progress/progress6.png" +dest_files=["res://.godot/imported/progress6.png-291be2c54ac04191c46b210bc746b5d8.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gd-plug-ui/assets/icons/progress/progress7.png b/addons/gd-plug-ui/assets/icons/progress/progress7.png new file mode 100644 index 0000000..9d514c4 Binary files /dev/null and b/addons/gd-plug-ui/assets/icons/progress/progress7.png differ diff --git a/addons/gd-plug-ui/assets/icons/progress/progress7.png.import b/addons/gd-plug-ui/assets/icons/progress/progress7.png.import new file mode 100644 index 0000000..895a5e9 --- /dev/null +++ b/addons/gd-plug-ui/assets/icons/progress/progress7.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://sred54ktpynr" +path="res://.godot/imported/progress7.png-b7f8b3304beb293d02138afa69666fa3.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gd-plug-ui/assets/icons/progress/progress7.png" +dest_files=["res://.godot/imported/progress7.png-b7f8b3304beb293d02138afa69666fa3.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gd-plug-ui/assets/icons/progress/progress8.png b/addons/gd-plug-ui/assets/icons/progress/progress8.png new file mode 100644 index 0000000..5bffcff Binary files /dev/null and b/addons/gd-plug-ui/assets/icons/progress/progress8.png differ diff --git a/addons/gd-plug-ui/assets/icons/progress/progress8.png.import b/addons/gd-plug-ui/assets/icons/progress/progress8.png.import new file mode 100644 index 0000000..eb3f8fb --- /dev/null +++ b/addons/gd-plug-ui/assets/icons/progress/progress8.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dvgd0gmpoo216" +path="res://.godot/imported/progress8.png-e052617913242937886f27a9f5422ac5.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gd-plug-ui/assets/icons/progress/progress8.png" +dest_files=["res://.godot/imported/progress8.png-e052617913242937886f27a9f5422ac5.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gd-plug-ui/assets/icons/progress/progress_animated_texture.tres b/addons/gd-plug-ui/assets/icons/progress/progress_animated_texture.tres new file mode 100644 index 0000000..5ffbeac --- /dev/null +++ b/addons/gd-plug-ui/assets/icons/progress/progress_animated_texture.tres @@ -0,0 +1,29 @@ +[gd_resource type="AnimatedTexture" load_steps=9 format=2] + +[ext_resource path="res://addons/gd-plug-ui/assets/icons/progress/progress1.png" type="Texture" id=1] +[ext_resource path="res://addons/gd-plug-ui/assets/icons/progress/progress6.png" type="Texture" id=2] +[ext_resource path="res://addons/gd-plug-ui/assets/icons/progress/progress7.png" type="Texture" id=3] +[ext_resource path="res://addons/gd-plug-ui/assets/icons/progress/progress5.png" type="Texture" id=4] +[ext_resource path="res://addons/gd-plug-ui/assets/icons/progress/progress2.png" type="Texture" id=5] +[ext_resource path="res://addons/gd-plug-ui/assets/icons/progress/progress4.png" type="Texture" id=6] +[ext_resource path="res://addons/gd-plug-ui/assets/icons/progress/progress3.png" type="Texture" id=7] +[ext_resource path="res://addons/gd-plug-ui/assets/icons/progress/progress8.png" type="Texture" id=8] + +[resource] +flags = 4 +frames = 8 +frame_0/texture = ExtResource( 1 ) +frame_1/texture = ExtResource( 5 ) +frame_1/delay_sec = 0.0 +frame_2/texture = ExtResource( 7 ) +frame_2/delay_sec = 0.0 +frame_3/texture = ExtResource( 6 ) +frame_3/delay_sec = 0.0 +frame_4/texture = ExtResource( 4 ) +frame_4/delay_sec = 0.0 +frame_5/texture = ExtResource( 2 ) +frame_5/delay_sec = 0.0 +frame_6/texture = ExtResource( 3 ) +frame_6/delay_sec = 0.0 +frame_7/texture = ExtResource( 8 ) +frame_7/delay_sec = 0.0 diff --git a/addons/gd-plug-ui/assets/icons/refresh.png b/addons/gd-plug-ui/assets/icons/refresh.png new file mode 100644 index 0000000..c33438b Binary files /dev/null and b/addons/gd-plug-ui/assets/icons/refresh.png differ diff --git a/addons/gd-plug-ui/assets/icons/refresh.png.import b/addons/gd-plug-ui/assets/icons/refresh.png.import new file mode 100644 index 0000000..848a8a8 --- /dev/null +++ b/addons/gd-plug-ui/assets/icons/refresh.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cole0s5mc4l31" +path="res://.godot/imported/refresh.png-751efcf923f713368de638e48ebe9d26.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gd-plug-ui/assets/icons/refresh.png" +dest_files=["res://.godot/imported/refresh.png-751efcf923f713368de638e48ebe9d26.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gd-plug-ui/plugin.cfg b/addons/gd-plug-ui/plugin.cfg new file mode 100644 index 0000000..a20a394 --- /dev/null +++ b/addons/gd-plug-ui/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="gd-plug-ui" +description="" +author="imjp94" +version="0.2.1" +script="plugin.gd" diff --git a/addons/gd-plug-ui/plugin.gd b/addons/gd-plug-ui/plugin.gd new file mode 100644 index 0000000..1f6a402 --- /dev/null +++ b/addons/gd-plug-ui/plugin.gd @@ -0,0 +1,80 @@ +@tool +extends EditorPlugin +const Utils = preload("Utils.gd") +const PluginSettings = preload("scene/plugin_settings/PluginSettings.tscn") + +var plugin_config = ConfigFile.new() +var plugin_settings = PluginSettings.instantiate() +var plugins_tab +var plugins_tab_update_btn + + +func _enter_tree(): + add_control_to_container(EditorPlugin.CONTAINER_PROJECT_SETTING_TAB_LEFT, plugin_settings) + plugin_settings.connect("updated", _on_plugin_settings_updated) + plugin_settings.connect("gd_plug_loaded", _on_plugin_settings_gd_plug_loaded) + var tab_container = plugin_settings.get_parent() + for child in tab_container.get_children(): + if child.name == "Plugins": + plugins_tab = child + break + if plugins_tab: + tab_container.move_child(plugin_settings, plugins_tab.get_index()) + else: + tab_container.move_child(plugin_settings, tab_container.get_child_count()-1) + + for child in plugins_tab.get_children(): + if child is HBoxContainer: + for grandchild in child.get_children(): + if grandchild is Button: + if grandchild.text == "Update": + plugins_tab_update_btn = grandchild + plugins_tab_update_btn.connect("pressed", self, "_on_plugins_tab_update_btn_pressed") + break + plugin_settings.load_gd_plug() + +func _on_plugin_settings_gd_plug_loaded(gd_plug): + check_compatibility(gd_plug.VERSION) + +func _on_plugin_settings_updated(): + if plugins_tab_update_btn: + plugins_tab_update_btn.emit_signal("pressed") # Programmatically press update button in "Plugins" tab + +func _exit_tree(): + if is_instance_valid(plugin_settings): + remove_control_from_container(EditorPlugin.CONTAINER_PROJECT_SETTING_TAB_LEFT, plugin_settings) + plugin_settings.queue_free() + +func check_compatibility(gd_plug_version): + plugin_config.load("res://addons/gd-plug-ui/plugin.cfg") + var gd_plug_ui_version = plugin_config.get_value("plugin", "version", "0.0.0") + var later_or_equal = "" + var before = "" + match gd_plug_ui_version: + "0.2.0": + later_or_equal = "0.2.5" + "0.1.0": + later_or_equal = "0.1.4" + before = "0.2.0" + "0.0.0": + print("Failed to read gd-plug-ui version string") + _: + later_or_equal = "0.1.3" + + var is_version_expected = Utils.expected_version(gd_plug_version, later_or_equal, before) + if not is_version_expected: + var dialog = AcceptDialog.new() + var text = "gd-plug-ui(%s) is not compatible with " % gd_plug_ui_version + text += "current gd-plug(%s), " % gd_plug_version + text += "expected >=%s" % later_or_equal if before.length() == 0 else " expected >=%s or %s<" % [later_or_equal, before] + dialog.dialog_text = text + plugin_settings.add_child(dialog) + # add_control_to_container(EditorPlugin.CONTAINER_PROJECT_SETTING_TAB_LEFT, dialog) + dialog.popup_centered() + + await dialog.confirmed + + dialog.queue_free() + if is_instance_valid(plugin_settings): + remove_control_from_container(EditorPlugin.CONTAINER_PROJECT_SETTING_TAB_LEFT, plugin_settings) + plugin_settings.queue_free() diff --git a/addons/gd-plug-ui/scene/plugin_settings/PluginSettings.gd b/addons/gd-plug-ui/scene/plugin_settings/PluginSettings.gd new file mode 100644 index 0000000..b1ea009 --- /dev/null +++ b/addons/gd-plug-ui/scene/plugin_settings/PluginSettings.gd @@ -0,0 +1,306 @@ +@tool +extends Control + +signal gd_plug_loaded(gd_plug) +signal updated() + +enum PLUGIN_STATUS { + PLUGGED, UNPLUGGED, INSTALLED, CHANGED, UPDATE +} +const PLUGIN_STATUS_ICON = [ + preload("../../assets/icons/add.png"), preload("../../assets/icons/import_fail.png"), + preload("../../assets/icons/import_check.png"), preload("../../assets/icons/edit_internal.png"), + preload("../../assets/icons/refresh.png") +] + +@onready var tree = $Tree +@onready var init_btn = $"%InitBtn" +@onready var check_for_update_btn = $"%CheckForUpdateBtn" +@onready var update_section = $"%UpdateSection" +@onready var force_check = $"%ForceCheck" +@onready var production_check = $"%ProductionCheck" +@onready var update_btn = $"%UpdateBtn" +@onready var loading_overlay = $"%LoadingOverlay" +@onready var loading_label = $"%LoadingLabel" + +var gd_plug +var project_dir + +var _is_executing = false +var _check_for_update_task_id = -1 + + +func _ready(): + project_dir = DirAccess.open("res://") + load_gd_plug() + update_plugin_list(get_plugged_plugins(), get_installed_plugins()) + + tree.set_column_title(0, "Name") + tree.set_column_title(1, "Arguments") + tree.set_column_title(2, "Status") + + connect("visibility_changed", _on_visibility_changed) + +func _process(delta): + if not is_instance_valid(gd_plug): + return + + if "threadpool" in gd_plug: + gd_plug.threadpool.process(delta) + + if _check_for_update_task_id >= 0: + if WorkerThreadPool.is_task_completed(_check_for_update_task_id): + _check_for_update_task_id = -1 + show_overlay(false) + disable_ui(false) + +func _notification(what): + match what: + NOTIFICATION_PREDELETE: + if is_instance_valid(gd_plug): + gd_plug.threadpool.stop() + gd_plug.free() + NOTIFICATION_APPLICATION_FOCUS_IN: + load_gd_plug() + update_plugin_list(get_plugged_plugins(), get_installed_plugins()) + +func load_gd_plug(): + if is_instance_valid(gd_plug): + gd_plug.free() # Free instance in order to reload script + if project_dir.file_exists("plug.gd"): + init_btn.hide() + check_for_update_btn.show() + update_section.show() + update_btn.show() # Not sure why it is always hidden + + var gd_plug_script = load("plug.gd") + gd_plug_script.reload(true) # Reload gd-plug script to get updated + gd_plug = gd_plug_script.new() + gd_plug._plug_start() + gd_plug._plugging() + else: + if project_dir.file_exists("addons/gd-plug/plug.gd"): + init_btn.show() + check_for_update_btn.hide() + update_section.hide() + + gd_plug = load("addons/gd-plug/plug.gd").new() + else: + print("Missing dependency: gd-plug") + + if is_instance_valid(gd_plug): + emit_signal("gd_plug_loaded", gd_plug) + +func update_plugin_list(plugged, installed): + var plugin_names = [] + for plugin_name in plugged.keys(): + plugin_names.append(plugin_name) + for plugin_name in installed.keys(): + if plugin_name in plugin_names: + continue + plugin_names.append(plugin_name) + + tree.clear() + tree.create_item() # root + for plugin_name in plugin_names: + var plugin_plugged = plugged.get(plugin_name, {}) + var plugin_installed = installed.get(plugin_name, {}) + var plugin = plugin_plugged if plugin_name in plugged else plugin_installed + var plugin_status = get_plugin_status(plugin_name) + + var plugin_args = [] + for plugin_arg in plugin.keys(): + var value = plugin[plugin_arg] + + if value != null: + if not (value is bool): + if value.is_empty(): + continue + else: + continue + + match plugin_arg: + "install_root": + plugin_args.append("install root: %s" % str(value)) + "include": + plugin_args.append("include %s" % str(value)) + "exclude": + plugin_args.append("exclude %s" % str(value)) + "branch": + plugin_args.append("branch: %s" % str(value)) + "tag": + plugin_args.append("tag: %s" % str(value)) + "commit": + plugin_args.append(str(value).left(8)) + "dev": + if value: + plugin_args.append("dev") + "on_updated": + plugin_args.append("on_updated: %s" % str(value)) + + var plugin_args_text = "" + for i in plugin_args.size(): + var text = plugin_args[i] + plugin_args_text += text + if i < plugin_args.size() - 1: + plugin_args_text += ", " + + var child = tree.create_item(tree.get_root()) + child.set_text_alignment(0, HORIZONTAL_ALIGNMENT_LEFT) + child.set_text_alignment(1, HORIZONTAL_ALIGNMENT_CENTER) + child.set_text_alignment(2, HORIZONTAL_ALIGNMENT_CENTER) + child.set_meta("plugin", plugin) + child.set_text(0, plugin_name) + child.set_tooltip_text(0, plugin.url) + child.set_text(1, plugin_args_text) + child.set_tooltip_text(2, PLUGIN_STATUS.keys()[plugin_status].capitalize()) + child.set_icon(2, PLUGIN_STATUS_ICON[plugin_status]) + +func disable_ui(disabled=true): + init_btn.disabled = disabled + check_for_update_btn.disabled = disabled + update_btn.disabled = disabled + +func show_overlay(show=true, text=""): + loading_overlay.visible = show + loading_label.text = text + +func gd_plug_execute_threaded(name): + if not is_instance_valid(gd_plug): + return + if _is_executing: + return + + _is_executing = true + disable_ui(true) + gd_plug._plug_start() + gd_plug._plugging() + gd_plug.call(name) + + await gd_plug.threadpool.all_thread_finished + + # Make sure to use call_deferred for thread safe function calling while waiting thread to finish + gd_plug._plug_end() + call_deferred("disable_ui", false) + _is_executing = false + clear_environment() + + call_deferred("update_plugin_list", get_plugged_plugins(), get_installed_plugins()) + +func gd_plug_execute(name): + if not is_instance_valid(gd_plug): + return + if _is_executing: + return + + _is_executing = true + disable_ui(true) + gd_plug._plug_start() + gd_plug._plugging() + gd_plug.call(name) + gd_plug._plug_end() + disable_ui(false) + _is_executing = false + clear_environment() + + update_plugin_list(get_plugged_plugins(), get_installed_plugins()) + +func clear_environment(): + OS.unset_environment("production") + OS.unset_environment("test") + OS.unset_environment("force") + +func _on_visibility_changed(): + if visible: + load_gd_plug() + update_plugin_list(get_plugged_plugins(), get_installed_plugins()) + +func _on_Init_pressed(): + gd_plug_execute("_plug_init") + load_gd_plug() + +func _on_CheckForUpdateBtn_pressed(): + var children = tree.get_root().get_children() + if tree.get_root().get_children().size() > 0: + show_overlay(true, "Checking for Updates...") + disable_ui(true) + if _check_for_update_task_id < 0: + var task_id = WorkerThreadPool.add_task(check_for_update.bind(children[0])) + _check_for_update_task_id = (task_id) + +func _on_UpdateBtn_pressed(): + if force_check.button_pressed: + OS.set_environment("force", "true") + if production_check.button_pressed: + OS.set_environment("production", "true") + show_overlay(true, "Updating...") + gd_plug_execute_threaded("_plug_install") + + await gd_plug.threadpool.all_thread_finished + + # Make sure to use call_deferred for thread safe function calling while waiting thread to finish + call_deferred("show_overlay", false) + call_deferred("emit_signal", "updated") + +func get_plugged_plugins(): + return gd_plug._plugged_plugins if is_instance_valid(gd_plug) else {} + +func get_installed_plugins(): + return gd_plug.installation_config.get_value("plugin", "installed", {}) if is_instance_valid(gd_plug) else {} + +func get_plugin_status(plugin_name): + var plugged_plugins = get_plugged_plugins() + var installed_plugins = get_installed_plugins() + var plugin_plugged = plugged_plugins.get(plugin_name, {}) + var plugin_installed = installed_plugins.get(plugin_name, {}) + var plugin = plugin_plugged if plugin_name in plugged_plugins else plugin_installed + + var is_plugged = plugin.name in plugged_plugins + var is_installed = plugin.name in installed_plugins + var changes = gd_plug.compare_plugins(plugin_plugged, plugin_installed) if is_installed else {} + var is_changed = changes.size() > 0 + + var plugin_status = 0 + if is_installed: + if is_plugged: + if is_changed: + plugin_status = 3 + else: + plugin_status = 2 + else: + plugin_status = 1 + else: + plugin_status = 0 + + return plugin_status + +func has_update(plugin): + if not is_instance_valid(gd_plug): + return false + if plugin == null: + return false + var git = gd_plug._GitExecutable.new(ProjectSettings.globalize_path(plugin.plug_dir), gd_plug.logger) + + var ahead_behind = [] + if git.fetch("origin " + plugin.branch if plugin.branch else "origin").exit == OK: + ahead_behind = git.get_commit_comparison("HEAD", "origin/" + plugin.branch if plugin.branch else "origin") + var is_commit_behind = !!ahead_behind[1] if ahead_behind.size() == 2 else false + if is_commit_behind: + gd_plug.logger.info("%s %d commits behind, update required" % [plugin.name, ahead_behind[1]]) + return true + else: + gd_plug.logger.info("%s up to date" % plugin.name) + return false + +func check_for_update(child): + var plugin = child.get_meta("plugin") + var plugin_status = get_plugin_status(plugin.name) + if plugin_status == PLUGIN_STATUS.INSTALLED: + var has_update = has_update(plugin) + if has_update: + child.set_icon(2, PLUGIN_STATUS_ICON[PLUGIN_STATUS.UPDATE]) + child.set_tooltip_text(2, PLUGIN_STATUS.keys()[PLUGIN_STATUS.UPDATE].capitalize()) + if is_instance_valid(child): + var next_child = child.get_next() + if next_child: + check_for_update(next_child) diff --git a/addons/gd-plug-ui/scene/plugin_settings/PluginSettings.tscn b/addons/gd-plug-ui/scene/plugin_settings/PluginSettings.tscn new file mode 100644 index 0000000..cf41877 --- /dev/null +++ b/addons/gd-plug-ui/scene/plugin_settings/PluginSettings.tscn @@ -0,0 +1,112 @@ +[gd_scene load_steps=4 format=3 uid="uid://hr5ks54660xu"] + +[ext_resource type="Script" path="res://addons/gd-plug-ui/scene/plugin_settings/PluginSettings.gd" id="1"] +[ext_resource type="Texture2D" path="res://addons/gd-plug-ui/assets/icons/progress/progress_animated_texture.tres" id="2_30o7o"] + +[sub_resource type="StyleBoxEmpty" id="1"] + +[node name="gd-plug" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1") + +[node name="Tree" type="Tree" parent="."] +layout_mode = 0 +anchor_right = 1.0 +anchor_bottom = 0.9 +columns = 3 +column_titles_visible = true +hide_root = true + +[node name="MarginContainer" type="MarginContainer" parent="."] +layout_mode = 0 +anchor_top = 0.9 +anchor_right = 1.0 +anchor_bottom = 1.0 +theme_override_constants/margin_left = 16 +theme_override_constants/margin_top = 8 +theme_override_constants/margin_right = 16 +theme_override_constants/margin_bottom = 8 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer"] +layout_mode = 2 + +[node name="InitBtn" type="Button" parent="MarginContainer/HBoxContainer"] +unique_name_in_owner = true +visible = false +layout_mode = 2 +size_flags_horizontal = 6 +size_flags_vertical = 4 +text = "Init" + +[node name="CheckForUpdateBtn" type="Button" parent="MarginContainer/HBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 2 +size_flags_vertical = 4 +text = "Check for Update" + +[node name="UpdateSection" type="HBoxContainer" parent="MarginContainer/HBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 10 +size_flags_vertical = 6 + +[node name="ForceCheck" type="CheckButton" parent="MarginContainer/HBoxContainer/UpdateSection"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 10 +size_flags_vertical = 4 +tooltip_text = "Force installation of plugin, may overwrite project files in case of conflicts." +theme_override_styles/focus = SubResource("1") +text = "Force" + +[node name="ProductionCheck" type="CheckButton" parent="MarginContainer/HBoxContainer/UpdateSection"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 10 +size_flags_vertical = 4 +tooltip_text = "Install plugins not marked as \"dev\", and uninstall plugins marked as \"dev\"" +theme_override_styles/focus = SubResource("1") +text = "Production" + +[node name="UpdateBtn" type="Button" parent="MarginContainer/HBoxContainer/UpdateSection"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 10 +size_flags_vertical = 4 +text = "Update" + +[node name="LoadingOverlay" type="PanelContainer" parent="."] +unique_name_in_owner = true +visible = false +layout_mode = 1 +anchors_preset = -1 +anchor_right = 1.0 +anchor_bottom = 0.9 +grow_horizontal = 2 +grow_vertical = 2 +metadata/_edit_use_anchors_ = true + +[node name="CenterContainer" type="CenterContainer" parent="LoadingOverlay"] +layout_mode = 2 + +[node name="HBoxContainer" type="HBoxContainer" parent="LoadingOverlay/CenterContainer"] +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="LoadingOverlay/CenterContainer/HBoxContainer"] +layout_mode = 2 +texture = ExtResource("2_30o7o") +stretch_mode = 3 + +[node name="LoadingLabel" type="Label" parent="LoadingOverlay/CenterContainer/HBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 + +[connection signal="pressed" from="MarginContainer/HBoxContainer/InitBtn" to="." method="_on_Init_pressed"] +[connection signal="pressed" from="MarginContainer/HBoxContainer/CheckForUpdateBtn" to="." method="_on_CheckForUpdateBtn_pressed"] +[connection signal="pressed" from="MarginContainer/HBoxContainer/UpdateSection/UpdateBtn" to="." method="_on_UpdateBtn_pressed"] diff --git a/addons/gd-plug/plug.gd b/addons/gd-plug/plug.gd new file mode 100644 index 0000000..60291cf --- /dev/null +++ b/addons/gd-plug/plug.gd @@ -0,0 +1,1163 @@ +@tool +extends SceneTree + +signal updated(plugin) + +const VERSION = "0.2.6" +const DEFAULT_PLUGIN_URL = "https://git::@github.com/%s.git" +const DEFAULT_PLUG_DIR = "res://.plugged" +const DEFAULT_CONFIG_PATH = DEFAULT_PLUG_DIR + "/index.cfg" +const DEFAULT_USER_PLUG_SCRIPT_PATH = "res://plug.gd" +const DEFAULT_BASE_PLUG_SCRIPT_PATH = "res://addons/gd-plug/plug.gd" + +const ENV_PRODUCTION = "production" +const ENV_TEST = "test" +const ENV_FORCE = "force" +const ENV_KEEP_IMPORT_FILE = "keep_import_file" +const ENV_KEEP_IMPORT_RESOURCE_FILE = "keep_import_resource_file" + +const MSG_PLUG_START_ASSERTION = "_plug_start() must be called first" + +var project_dir +var installation_config = ConfigFile.new() +var logger = _Logger.new() + +var _installed_plugins +var _plugged_plugins = {} + +var _threads = [] +var _mutex = Mutex.new() +var _start_time = 0 +var threadpool = _ThreadPool.new(logger) + + +func _init(): + threadpool.connect("all_thread_finished", request_quit) + project_dir = DirAccess.open("res://") + +func _initialize(): + var args = OS.get_cmdline_args() + # Trim unwanted args passed to godot executable + for arg in Array(args): + args.remove_at(0) + if "plug.gd" in arg: + break + + var help = false + var help_config = false + for arg in args: + # NOTE: "--key" or "-key" will always be consumed by godot executable, see https://github.com/godotengine/godot/issues/8721 + var key = arg.to_lower() + match key: + "help": + help = true + "help-config": + help_config = true + "detail": + logger.log_format = _Logger.DEFAULT_LOG_FORMAT_DETAIL + "debug", "d": + logger.log_level = _Logger.LogLevel.DEBUG + "quiet", "q", "silent": + logger.log_level = _Logger.LogLevel.NONE + "production": + OS.set_environment(ENV_PRODUCTION, "true") + "test": + OS.set_environment(ENV_TEST, "true") + "force": + OS.set_environment(ENV_FORCE, "true") + "keep-import-file": + OS.set_environment(ENV_KEEP_IMPORT_FILE, "true") + "keep-import-resource-file": + OS.set_environment(ENV_KEEP_IMPORT_RESOURCE_FILE, "true") + + logger.debug("cmdline_args: %s" % args) + _start_time = Time.get_ticks_msec() + _plug_start() + if help_config: + show_config_syntax() + elif help or args.size() == 0: + show_syntax() + else: + _plugging() + match args[0]: + "init": + _plug_init() + "install", "update": + _plug_install() + "uninstall": + _plug_uninstall() + "clean": + _plug_clean() + "upgrade": + _plug_upgrade() + "status": + _plug_status() + "version": + logger.info(VERSION) + _: + logger.error("Unknown command %s" % args[0]) + show_syntax() + # NOTE: Do no put anything after this line except request_quit(), as _plug_*() may call request_quit() + request_quit() + +func show_syntax(): + logger.info("gd-plug - Minimal plugin manager for Godot") + logger.info("") + logger.info("Usage: godot --headless -s plug.gd action [options...]") + logger.info("") + logger.info("Actions:") + var actions = { + "init": "Initialize current project by creating plug.gd at root", + "status": "Check the status of plugins(installed, added or removed), execute this command whenever in doubts", + "install(alias update)": "Install or update plugins based on plug.gd", + "uninstall": "Uninstall all plugins, regardless of plug.gd", + "clean": "Clean unused files/folders from /.plugged", + "upgrade": "Upgrade addons/gd-plug/plug.gd to the latest version", + "version": "Print current version of gd-plug", + } + logger.indent() + logger.table_start() + for action_name in actions: + logger.table_row([action_name, actions[action_name]]) + logger.table_end() + logger.dedent() + + logger.info("") + logger.info("Options:") + var options = { + "production": "Install only plugins not marked as dev, or uninstall already installed dev plugins", + "test": "Testing mode, no files will actually be installed/uninstalled", + "force": "Force gd-plug to overwrite destination files when running install command. *WARNING: Check README for more details*", + "keep-import-file": "Keep \".import\" files generated by plugin, when run uninstall command", + "keep-import-resource-file": "Keep files located in \".import\" that generated by plugin, when run uninstall command", + "debug(alias d)": "Print debug message", + "detail": "Print with datetime and log level, \"[{time}] [{level}] {msg}\"", + "quiet(alias q, silent)": "Disable logging", + "help": "Show this help", + "help-config": "plug.gd configuration documentation" + } + logger.indent() + logger.table_start() + for option_name in options: + logger.table_row([option_name, options[option_name]]) + logger.table_end() + logger.dedent() + logger.info("") + +func show_config_syntax(): + logger.info("Configs: plug(src, args={})") + logger.info("") + logger.info("Sources:") + logger.indent() + logger.info("Github repo: \"username/repo\", for example, \"imjp94/gd-plug\"") + logger.info("or") + logger.info("Any valid git url, for example, \"git@github.com:username/repo.git\"") + logger.dedent() + logger.info("") + logger.info("Arguments:") + var arguments = { + "include": "Array of strings that define what files or directory to include. Only \"addons/\" will be included if omitted", + "exclude": "Array of strings that define what files or directory to exclude", + "branch": "Name of branch to freeze to", + "tag": "Name of tag to freeze to", + "commit": "Commit hash string to freeze to, must be full length 40 digits commit-hash, for example, 7a642f90d3fb88976dd913051de994e58e838d1a", + "dev": "Boolean to mark the plugin as dev or not, plugin marked as dev will not be installed when production command given", + "on-updated": "Post update hook, a function name declared in plug.gd that will be called whenever the plugin installed/updated" + } + logger.indent() + logger.table_start() + for argument_name in arguments: + logger.table_row([argument_name, arguments[argument_name]]) + logger.table_end() + logger.dedent() + logger.info("") + +func _process(delta): + threadpool.process(delta) + +func _finalize(): + _plug_end() + threadpool.stop() + logger.info("Finished, elapsed %.3fs" % ((Time.get_ticks_msec() - _start_time) / 1000.0)) + +func _on_updated(plugin): + pass + +func _plugging(): + pass + +func request_quit(exit_code=-1): + if threadpool.is_all_thread_finished() and threadpool.is_all_task_finished(): + quit(exit_code) + return true + logger.debug("Request quit declined, threadpool is still running") + return false + +# Index installed plugins, or create directory "plugged" if not exists +func _plug_start(): + logger.debug("Plug start") + if not project_dir.dir_exists(DEFAULT_PLUG_DIR): + if project_dir.make_dir(ProjectSettings.globalize_path(DEFAULT_PLUG_DIR)) == OK: + logger.debug("Make dir %s for plugin installation") + if installation_config.load(DEFAULT_CONFIG_PATH) == OK: + logger.debug("Installation config loaded") + else: + logger.debug("Installation config not found") + _installed_plugins = installation_config.get_value("plugin", "installed", {}) + +# Install plugin or uninstall plugin if unlisted +func _plug_end(): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + var test = !OS.get_environment(ENV_TEST).is_empty() + if not test: + installation_config.set_value("plugin", "installed", _installed_plugins) + if installation_config.save(DEFAULT_CONFIG_PATH) == OK: + logger.debug("Plugged config saved") + else: + logger.error("Failed to save plugged config") + else: + logger.warn("Skipped saving of plugged config in test mode") + _installed_plugins = null + +func _plug_init(): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + logger.info("Init gd-plug...") + if FileAccess.file_exists(DEFAULT_USER_PLUG_SCRIPT_PATH): + logger.warn("%s already exists!" % DEFAULT_USER_PLUG_SCRIPT_PATH) + else: + var file = FileAccess.open(DEFAULT_USER_PLUG_SCRIPT_PATH, FileAccess.WRITE) + file.store_string(INIT_PLUG_SCRIPT) + file.close() + logger.info("Created %s" % DEFAULT_USER_PLUG_SCRIPT_PATH) + +func _plug_install(): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + threadpool.active = false + logger.info("Installing...") + for plugin in _plugged_plugins.values(): + var installed = plugin.name in _installed_plugins + if installed: + var installed_plugin = get_installed_plugin(plugin.name) + if (installed_plugin.dev or plugin.dev) and OS.get_environment(ENV_PRODUCTION): + logger.info("Remove dev plugin for production: %s" % plugin.name) + threadpool.enqueue_task(uninstall_plugin.bind(installed_plugin)) + else: + threadpool.enqueue_task(update_plugin.bind(plugin)) + else: + threadpool.enqueue_task(install_plugin.bind(plugin)) + + var removed_plugins = [] + for plugin in _installed_plugins.values(): + var removed = not (plugin.name in _plugged_plugins) + if removed: + removed_plugins.append(plugin) + if removed_plugins: + threadpool.disconnect("all_thread_finished", request_quit) + if not threadpool.is_all_thread_finished(): + threadpool.active = true + await threadpool.all_thread_finished + threadpool.active = false + logger.debug("All installation finished! Ready to uninstall removed plugins...") + threadpool.connect("all_thread_finished", request_quit) + for plugin in removed_plugins: + threadpool.enqueue_task(uninstall_plugin.bind(plugin), Thread.PRIORITY_LOW) + threadpool.active = true + +func _plug_uninstall(): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + threadpool.active = false + logger.info("Uninstalling...") + for plugin in _installed_plugins.values(): + var installed_plugin = get_installed_plugin(plugin.name) + threadpool.enqueue_task(uninstall_plugin.bind(installed_plugin), Thread.PRIORITY_LOW) + threadpool.active = true + +func _plug_clean(): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + threadpool.active = false + logger.info("Cleaning...") + var plugged_dir = DirAccess.open(DEFAULT_PLUG_DIR) + plugged_dir.include_hidden = true + plugged_dir.list_dir_begin() # TODOGODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547 + var file = plugged_dir.get_next() + while not file.is_empty(): + if plugged_dir.current_is_dir(): + if not (file in _installed_plugins): + logger.info("Remove %s" % file) + threadpool.enqueue_task(directory_delete_recursively.bind(plugged_dir.get_current_dir() + "/" + file)) + file = plugged_dir.get_next() + plugged_dir.list_dir_end() + threadpool.active = true + +func _plug_upgrade(): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + threadpool.active = false + logger.info("Upgrading gd-plug...") + plug("imjp94/gd-plug") + var gd_plug = _plugged_plugins["gd-plug"] + OS.set_environment(ENV_FORCE, "true") # Required to overwrite res://addons/gd-plug/plug.gd + threadpool.enqueue_task(install_plugin.bind(gd_plug)) + threadpool.disconnect("all_thread_finished", request_quit) + if not threadpool.is_all_thread_finished(): + threadpool.active = true + await threadpool.all_thread_finished + threadpool.active = false + logger.debug("All installation finished! Ready to uninstall removed plugins...") + threadpool.connect("all_thread_finished", request_quit) + threadpool.enqueue_task(directory_delete_recursively.bind(gd_plug.plug_dir)) + threadpool.active = true + +func _plug_status(): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + threadpool.active = false + logger.info("Installed %d plugin%s" % [_installed_plugins.size(), "s" if _installed_plugins.size() > 1 else ""]) + var new_plugins = _plugged_plugins.duplicate() + var has_checking_plugin = false + var removed_plugins = [] + for plugin in _installed_plugins.values(): + logger.info("- {name} - {url}".format(plugin)) + new_plugins.erase(plugin.name) + var removed = not (plugin.name in _plugged_plugins) + if removed: + removed_plugins.append(plugin) + else: + threadpool.enqueue_task(check_plugin.bind(_plugged_plugins[plugin.name])) + has_checking_plugin = true + if has_checking_plugin: + logger.info("\n", true) + threadpool.disconnect("all_thread_finished", request_quit) + threadpool.active = true + await threadpool.all_thread_finished + threadpool.active = false + threadpool.connect("all_thread_finished", request_quit) + logger.debug("Finished checking plugins, ready to proceed") + if new_plugins: + logger.info("\nPlugged %d plugin%s" % [new_plugins.size(), "s" if new_plugins.size() > 1 else ""]) + for plugin in new_plugins.values(): + var is_new = not (plugin.name in _installed_plugins) + if is_new: + logger.info("- {name} - {url}".format(plugin)) + if removed_plugins: + logger.info("\nUnplugged %d plugin%s" % [removed_plugins.size(), "s" if removed_plugins.size() > 1 else ""]) + for plugin in removed_plugins: + logger.info("- %s removed" % plugin.name) + var plug_directory = DirAccess.open(DEFAULT_PLUG_DIR) + var orphan_dirs = [] + if plug_directory.get_open_error() == OK: + plug_directory.list_dir_begin() # TODOGODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547 + var file = plug_directory.get_next() + while not file.is_empty(): + if plug_directory.current_is_dir(): + if not (file in _installed_plugins): + orphan_dirs.append(file) + file = plug_directory.get_next() + plug_directory.list_dir_end() + if orphan_dirs: + logger.info("\nOrphan directory, %d found in %s, execute \"clean\" command to remove" % [orphan_dirs.size(), DEFAULT_PLUG_DIR]) + for dir in orphan_dirs: + logger.info("- %s" % dir) + threadpool.active = true + + if has_checking_plugin: + request_quit() + +# Index & validate plugin +func plug(repo, args={}): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + repo = repo.strip_edges() + var plugin_name = get_plugin_name_from_repo(repo) + if plugin_name in _plugged_plugins: + logger.info("Plugin already plugged: %s" % plugin_name) + return + var plugin = {} + plugin.name = plugin_name + plugin.url = "" + if ":" in repo: + plugin.url = repo + elif repo.find("/") == repo.rfind("/"): + plugin.url = DEFAULT_PLUGIN_URL % repo + else: + logger.error("Invalid repo: %s" % repo) + plugin.plug_dir = DEFAULT_PLUG_DIR + "/" + plugin.name + + var is_valid = true + plugin.include = args.get("include", []) + is_valid = is_valid and validate_var_type(plugin, "include", TYPE_ARRAY, "Array") + plugin.exclude = args.get("exclude", []) + is_valid = is_valid and validate_var_type(plugin, "exclude", TYPE_ARRAY, "Array") + plugin.branch = args.get("branch", "") + is_valid = is_valid and validate_var_type(plugin, "branch", TYPE_STRING, "String") + plugin.tag = args.get("tag", "") + is_valid = is_valid and validate_var_type(plugin, "tag", TYPE_STRING, "String") + plugin.commit = args.get("commit", "") + is_valid = is_valid and validate_var_type(plugin, "commit", TYPE_STRING, "String") + if not plugin.commit.is_empty(): + var is_valid_commit = plugin.commit.length() == 40 + if not is_valid_commit: + logger.error("Expected full length 40 digits commit-hash string, given %s" % plugin.commit) + is_valid = is_valid and is_valid_commit + plugin.dev = args.get("dev", false) + is_valid = is_valid and validate_var_type(plugin, "dev", TYPE_BOOL, "Boolean") + plugin.on_updated = args.get("on_updated", "") + is_valid = is_valid and validate_var_type(plugin, "on_updated", TYPE_STRING, "String") + plugin.install_root = args.get("install_root", "") + is_valid = is_valid and validate_var_type(plugin, "install_root", TYPE_STRING, "String") + + if is_valid: + _plugged_plugins[plugin.name] = plugin + logger.debug("Plug: %s" % plugin) + else: + logger.error("Failed to plug %s, validation error" % plugin.name) + +func install_plugin(plugin): + var test = !OS.get_environment(ENV_TEST).is_empty() + var can_install = OS.get_environment(ENV_PRODUCTION).is_empty() if plugin.dev else true + if can_install: + logger.info("Installing plugin %s..." % plugin.name) + var result = is_plugin_downloaded(plugin) + if result != OK: + result = download(plugin) + else: + logger.info("Plugin already downloaded") + + if result == OK: + install(plugin) + else: + logger.error("Failed to install plugin %s with error code %d" % [plugin.name, result]) + +func uninstall_plugin(plugin): + var test = !OS.get_environment(ENV_TEST).is_empty() + logger.info("Uninstalling plugin %s..." % plugin.name) + uninstall(plugin) + directory_delete_recursively(plugin.plug_dir, {"exclude": [DEFAULT_CONFIG_PATH], "test": test}) + +func update_plugin(plugin, checking=false): + if not (plugin.name in _installed_plugins): + logger.info("%s new plugin" % plugin.name) + return true + + var git = _GitExecutable.new(ProjectSettings.globalize_path(plugin.plug_dir), logger) + var installed_plugin = get_installed_plugin(plugin.name) + var changes = compare_plugins(plugin, installed_plugin) + var should_clone = false + var should_pull = false + var should_reinstall = false + + if plugin.tag or plugin.commit: + for rev in ["tag", "commit"]: + var freeze_at = plugin[rev] + if freeze_at: + logger.info("%s frozen at %s \"%s\"" % [plugin.name, rev, freeze_at]) + break + else: + var ahead_behind = [] + if git.fetch("origin " + plugin.branch if plugin.branch else "origin").exit == OK: + ahead_behind = git.get_commit_comparison("HEAD", "origin/" + plugin.branch if plugin.branch else "origin") + var is_commit_behind = !!ahead_behind[1] if ahead_behind.size() == 2 else false + if is_commit_behind: + logger.info("%s %d commits behind, update required" % [plugin.name, ahead_behind[1]]) + should_pull = true + else: + logger.info("%s up to date" % plugin.name) + + if changes: + logger.info("%s changed %s" % [plugin.name, changes]) + should_reinstall = true + if "url" in changes or "branch" in changes or "tag" in changes or "commit" in changes: + logger.info("%s repository setting changed, update required" % plugin.name) + should_clone = true + + if not checking: + if should_clone: + logger.info("%s cloning from %s..." % [plugin.name, plugin.url]) + var test = !OS.get_environment(ENV_TEST).is_empty() + uninstall(get_installed_plugin(plugin.name)) + directory_delete_recursively(plugin.plug_dir, {"exclude": [DEFAULT_CONFIG_PATH], "test": test}) + if download(plugin) == OK: + install(plugin) + elif should_pull: + logger.info("%s pulling updates from %s..." % [plugin.name, plugin.url]) + uninstall(get_installed_plugin(plugin.name)) + if git.pull().exit == OK: + install(plugin) + elif should_reinstall: + logger.info("%s reinstalling..." % plugin.name) + uninstall(get_installed_plugin(plugin.name)) + install(plugin) + +func check_plugin(plugin): + update_plugin(plugin, true) + +func download(plugin): + logger.info("Downloading %s from %s..." % [plugin.name, plugin.url]) + var test = !OS.get_environment(ENV_TEST).is_empty() + var global_dest_dir = ProjectSettings.globalize_path(plugin.plug_dir) + if project_dir.dir_exists(plugin.plug_dir): + directory_delete_recursively(plugin.plug_dir) + project_dir.make_dir(plugin.plug_dir) + var result = _GitExecutable.new(global_dest_dir, logger).clone(plugin.url, global_dest_dir, {"branch": plugin.branch, "tag": plugin.tag, "commit": plugin.commit}) + if result.exit == OK: + logger.info("Successfully download %s" % [plugin.name]) + else: + logger.info("Failed to download %s" % plugin.name) + # Make sure plug_dir is clean when failed + directory_delete_recursively(plugin.plug_dir, {"exclude": [DEFAULT_CONFIG_PATH], "test": test}) + project_dir.remove(plugin.plug_dir) # Remove empty directory + return result.exit + +func install(plugin): + var include = plugin.get("include", []) + if include.is_empty(): # Auto include "addons/" folder if not explicitly specified + include = ["addons/"] + if OS.get_environment(ENV_FORCE).is_empty() and OS.get_environment(ENV_TEST).is_empty(): + var is_exists = false + var dest_files = directory_copy_recursively(plugin.plug_dir, "res://" + plugin.install_root, {"include": include, "exclude": plugin.exclude, "test": true, "silent_test": true}) + for dest_file in dest_files: + if project_dir.file_exists(dest_file): + logger.warn("%s attempting to overwrite file %s" % [plugin.name, dest_file]) + is_exists = true + if is_exists: + logger.warn("Installation of %s terminated to avoid overwriting user files, you may disable safe mode with command \"force\"" % plugin.name) + return ERR_ALREADY_EXISTS + + logger.info("Installing files for %s..." % plugin.name) + var test = !OS.get_environment(ENV_TEST).is_empty() + var dest_files = directory_copy_recursively(plugin.plug_dir, "res://" + plugin.install_root, {"include": include, "exclude": plugin.exclude, "test": test}) + plugin.dest_files = dest_files + logger.info("Installed %d file%s for %s" % [dest_files.size(), "s" if dest_files.size() > 1 else "", plugin.name]) + if plugin.name != "gd-plug": + set_installed_plugin(plugin) + if plugin.on_updated: + if has_method(plugin.on_updated): + logger.info("Execute post-update function for %s" % plugin.name) + _on_updated(plugin) + call(plugin.on_updated, plugin.duplicate()) + emit_signal("updated", plugin) + return OK + +func uninstall(plugin): + var test = !OS.get_environment(ENV_TEST).is_empty() + var keep_import_file = !OS.get_environment(ENV_KEEP_IMPORT_FILE).is_empty() + var keep_import_resource_file = !OS.get_environment(ENV_KEEP_IMPORT_RESOURCE_FILE).is_empty() + var dest_files = plugin.get("dest_files", []) + logger.info("Uninstalling %d file%s for %s..." % [dest_files.size(), "s" if dest_files.size() > 1 else "",plugin.name]) + directory_remove_batch(dest_files, {"test": test, "keep_import_file": keep_import_file, "keep_import_resource_file": keep_import_resource_file}) + logger.info("Uninstalled %d file%s for %s" % [dest_files.size(), "s" if dest_files.size() > 1 else "",plugin.name]) + remove_installed_plugin(plugin.name) + +func is_plugin_downloaded(plugin): + if not project_dir.dir_exists(plugin.plug_dir + "/.git"): + return + + var git = _GitExecutable.new(ProjectSettings.globalize_path(plugin.plug_dir), logger) + return git.is_up_to_date(plugin) + +# Get installed plugin, thread safe +func get_installed_plugin(plugin_name): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + _mutex.lock() + var installed_plugin = _installed_plugins[plugin_name] + _mutex.unlock() + return installed_plugin + +# Set installed plugin, thread safe +func set_installed_plugin(plugin): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + _mutex.lock() + _installed_plugins[plugin.name] = plugin + _mutex.unlock() + +# Remove installed plugin, thread safe +func remove_installed_plugin(plugin_name): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + _mutex.lock() + var result = _installed_plugins.erase(plugin_name) + _mutex.unlock() + return result + +func directory_copy_recursively(from, to, args={}): + var include = args.get("include", []) + var exclude = args.get("exclude", []) + var test = args.get("test", false) + var silent_test = args.get("silent_test", false) + var dir = DirAccess.open(from) + dir.include_hidden = true + var dest_files = [] + if dir.get_open_error() == OK: + dir.list_dir_begin() # TODOGODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547 + var file_name = dir.get_next() + while not file_name.is_empty(): + var source = dir.get_current_dir() + ("/" if dir.get_current_dir() != "res://" else "") + file_name + var dest = to + ("/" if to != "res://" else "") + file_name + + if dir.current_is_dir(): + dest_files += directory_copy_recursively(source, dest, args) + else: + for include_key in include: + if include_key in source: + var is_excluded = false + for exclude_key in exclude: + if exclude_key in source: + is_excluded = true + break + if not is_excluded: + if test: + if not silent_test: logger.warn("[TEST] Writing to %s" % dest) + else: + dir.make_dir_recursive(to) + if dir.copy(source, dest) == OK: + logger.debug("Copy from %s to %s" % [source, dest]) + dest_files.append(dest) + break + file_name = dir.get_next() + dir.list_dir_end() + else: + logger.error("Failed to access path: %s" % from) + + return dest_files + +func directory_delete_recursively(dir_path, args={}): + var remove_empty_directory = args.get("remove_empty_directory", true) + var exclude = args.get("exclude", []) + var test = args.get("test", false) + var silent_test = args.get("silent_test", false) + var dir = DirAccess.open(dir_path) + dir.include_hidden = true + if dir.get_open_error() == OK: + dir.list_dir_begin() # TODOGODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547 + var file_name = dir.get_next() + while not file_name.is_empty(): + var source = dir.get_current_dir() + ("/" if dir.get_current_dir() != "res://" else "") + file_name + + if dir.current_is_dir(): + var sub_dir = directory_delete_recursively(source, args) + if remove_empty_directory: + if test: + if not silent_test: logger.warn("[TEST] Remove empty directory: %s" % sub_dir.get_current_dir()) + else: + if source.get_file() == ".git": + var empty_dir_path = ProjectSettings.globalize_path(source) + var exit = FAILED + match OS.get_name(): + "Windows": + empty_dir_path = "\"%s\"" % empty_dir_path + empty_dir_path = empty_dir_path.replace("/", "\\") + var cmd = "rd /s /q %s" % empty_dir_path + exit = OS.execute("cmd", ["/C", cmd]) + "X11", "OSX", "Server": + empty_dir_path = "\'%s\'" % empty_dir_path + var cmd = "rm -rf %s" % empty_dir_path + exit = OS.execute("bash", ["-c", cmd]) + # Hacks to remove .git, as git pack files stop it from being removed + # See https://stackoverflow.com/questions/1213430/how-to-fully-delete-a-git-repository-created-with-init + if exit == OK: + logger.debug("Remove empty directory: %s" % sub_dir.get_current_dir()) + else: + logger.debug("Failed to remove empty directory: %s" % sub_dir.get_current_dir()) + else: + if dir.remove(sub_dir.get_current_dir()) == OK: + logger.debug("Remove empty directory: %s" % sub_dir.get_current_dir()) + else: + var excluded = false + for exclude_key in exclude: + if source in exclude_key: + excluded = true + break + if not excluded: + if test: + if not silent_test: logger.warn("[TEST] Remove file: %s" % source) + else: + if dir.remove(file_name) == OK: + logger.debug("Remove file: %s" % source) + file_name = dir.get_next() + dir.list_dir_end() + else: + logger.error("Failed to access path: %s" % dir_path) + + if remove_empty_directory: + dir.remove(dir.get_current_dir()) + + return dir + +func directory_remove_batch(files, args={}): + var remove_empty_directory = args.get("remove_empty_directory", true) + var keep_import_file = args.get("keep_import_file", false) + var keep_import_resource_file = args.get("keep_import_resource_file", false) + var test = args.get("test", false) + var silent_test = args.get("silent_test", false) + var dirs = {} + for file in files: + var file_dir = file.get_base_dir() + var file_name =file.get_file() + var dir = dirs.get(file_dir) + + if not dir: + dir = DirAccess.open(file_dir) + dirs[file_dir] = dir + + if file.ends_with(".import"): + if not keep_import_file: + _remove_import_file(dir, file, keep_import_resource_file, test, silent_test) + else: + if test: + if not silent_test: logger.warn("[TEST] Remove file: " + file) + else: + if dir.remove(file_name) == OK: + logger.debug("Remove file: " + file) + if not keep_import_file: + _remove_import_file(dir, file + ".import", keep_import_resource_file, test, silent_test) + + for dir in dirs.values(): + var slash_count = dir.get_current_dir().count("/") - 2 # Deduct 2 slash from "res://" + if test: + if not silent_test: logger.warn("[TEST] Remove empty directory: %s" % dir.get_current_dir()) + else: + if dir.remove(dir.get_current_dir()) == OK: + logger.debug("Remove empty directory: %s" % dir.get_current_dir()) + # Dumb method to clean empty ancestor directories + logger.debug("Removing emoty ancestor directory for %s..." % dir.get_current_dir()) + var current_dir = dir.get_current_dir() + for i in slash_count: + current_dir = current_dir.get_base_dir() + var d = DirAccess.open(current_dir) + if d.get_open_error() == OK: + if test: + if not silent_test: logger.warn("[TEST] Remove empty ancestor directory: %s" % d.get_current_dir()) + else: + if d.remove(d.get_current_dir()) == OK: + logger.debug("Remove empty ancestor directory: %s" % d.get_current_dir()) + +func _remove_import_file(dir, file, keep_import_resource_file=false, test=false, silent_test=false): + if not dir.file_exists(file): + return + + if not keep_import_resource_file: + var import_config = ConfigFile.new() + if import_config.load(file) == OK: + var metadata = import_config.get_value("remap", "metadata", {}) + var imported_formats = metadata.get("imported_formats", []) + if imported_formats: + for format in imported_formats: + _remove_import_resource_file(dir, import_config, "." + format, test) + else: + _remove_import_resource_file(dir, import_config, "", test) + if test: + if not silent_test: logger.warn("[TEST] Remove import file: " + file) + else: + if dir.remove(file) == OK: + logger.debug("Remove import file: " + file) + else: + # TODO: Sometimes Directory.remove() unable to remove random .import file and return error code 1(Generic Error) + # Maybe enforce the removal from shell? + logger.warn("Failed to remove import file: " + file) + +func _remove_import_resource_file(dir, import_config, import_format="", test=false): + var import_resource_file = import_config.get_value("remap", "path" + import_format, "") + var checksum_file = import_resource_file.trim_suffix("." + import_resource_file.get_extension()) + ".md5" if import_resource_file else "" + if import_resource_file: + if dir.file_exists(import_resource_file): + if test: + logger.info("[IMPORT] Remove import resource file: " + import_resource_file) + else: + if dir.remove(import_resource_file) == OK: + logger.debug("Remove import resource file: " + import_resource_file) + if checksum_file: + checksum_file = checksum_file.replace(import_format, "") + if dir.file_exists(checksum_file): + if test: + logger.info("[IMPORT] Remove import checksum file: " + checksum_file) + else: + if dir.remove(checksum_file) == OK: + logger.debug("Remove import checksum file: " + checksum_file) + +func compare_plugins(p1, p2): + var changed_keys = [] + for key in p1.keys(): + var v1 = p1[key] + var v2 = p2[key] + if v1 != v2: + changed_keys.append(key) + return changed_keys + +func get_plugin_name_from_repo(repo): + repo = repo.replace(".git", "").trim_suffix("/") + return repo.get_file() + +func validate_var_type(obj, var_name, type, type_string): + var value = obj.get(var_name) + var is_valid = typeof(value) == type + if not is_valid: + logger.error("Expected variable \"%s\" to be %s, given %s" % [var_name, type_string, value]) + return is_valid + +const INIT_PLUG_SCRIPT = \ +"""extends "res://addons/gd-plug/plug.gd" + +func _plugging(): + # Declare plugins with plug(repo, args) + # For example, clone from github repo("user/repo_name") + # plug("imjp94/gd-YAFSM") # By default, gd-plug will only install anything from "addons/" directory + # Or you can explicitly specify which file/directory to include + # plug("imjp94/gd-YAFSM", {"include": ["addons/"]}) # By default, gd-plug will only install anything from "addons/" directory + pass +""" + +class _GitExecutable extends RefCounted: + var cwd = "" + var logger + + func _init(p_cwd, p_logger): + cwd = p_cwd + logger = p_logger + + func _execute(command, output=[], read_stderr=false): + var cmd = "cd '%s' && %s" % [cwd, command] + # NOTE: OS.execute() seems to ignore read_stderr + var exit = FAILED + match OS.get_name(): + "Windows": + cmd = cmd.replace("\'", "\"") # cmd doesn't accept single-quotes + cmd = cmd if read_stderr else "%s 2> nul" % cmd + logger.debug("Execute \"%s\"" % cmd) + exit = OS.execute("cmd", ["/C", cmd], output, read_stderr) + "macOS", "Linux", "FreeBSD", "NetBSD", "OpenBSD", "BSD": + cmd if read_stderr else "%s 2>/dev/null" % cmd + logger.debug("Execute \"%s\"" % cmd) + exit = OS.execute("bash", ["-c", cmd], output, read_stderr) + var unhandled_os: + logger.error("Unexpected OS: %s" % unhandled_os) + logger.debug("Execution ended(code:%d): %s" % [exit, output]) + return exit + + func init(): + logger.debug("Initializing git at %s..." % cwd) + var output = [] + var exit = _execute("git init", output) + logger.debug("Successfully init" if exit == OK else "Failed to init") + return {"exit": exit, "output": output} + + func clone(src, dest, args={}): + logger.debug("Cloning from %s to %s..." % [src, dest]) + var output = [] + var branch = args.get("branch", "") + var tag = args.get("tag", "") + var commit = args.get("commit", "") + var command = "git clone --depth=1 --progress '%s' '%s'" % [src, dest] + if branch or tag: + command = "git clone --depth=1 --single-branch --branch %s '%s' '%s'" % [branch if branch else tag, src, dest] + elif commit: + return clone_commit(src, dest, commit) + var exit = _execute(command, output) + logger.debug("Successfully cloned from %s" % src if exit == OK else "Failed to clone from %s" % src) + return {"exit": exit, "output": output} + + func clone_commit(src, dest, commit): + var output = [] + if commit.length() < 40: + logger.error("Expected full length 40 digits commit-hash to clone specific commit, given {%s}" % commit) + return {"exit": FAILED, "output": output} + + logger.debug("Cloning from %s to %s @ %s..." % [src, dest, commit]) + var result = init() + if result.exit == OK: + result = remote_add("origin", src) + if result.exit == OK: + result = fetch("%s %s" % ["origin", commit]) + if result.exit == OK: + result = reset("--hard", "FETCH_HEAD") + return result + + func fetch(rm="--all"): + logger.debug("Fetching %s..." % rm.replace("--", "")) + var output = [] + var exit = _execute("git fetch %s" % rm, output) + logger.debug("Successfully fetched" if exit == OK else "Failed to fetch") + return {"exit": exit, "output": output} + + func pull(): + logger.debug("Pulling...") + var output = [] + var exit = _execute("git pull --rebase", output) + logger.debug("Successfully pulled" if exit == OK else "Failed to pull") + return {"exit": exit, "output": output} + + func remote_add(name, src): + logger.debug("Adding remote %s@%s..." % [name, src]) + var output = [] + var exit = _execute("git remote add %s '%s'" % [name, src], output) + logger.debug("Successfully added remote" if exit == OK else "Failed to add remote") + return {"exit": exit, "output": output} + + func reset(mode, to): + logger.debug("Resetting %s %s..." % [mode, to]) + var output = [] + var exit = _execute("git reset %s %s" % [mode, to], output) + logger.debug("Successfully reset" if exit == OK else "Failed to reset") + return {"exit": exit, "output": output} + + func get_commit_comparison(branch_a, branch_b): + var output = [] + var exit = _execute("git rev-list --count --left-right %s...%s" % [branch_a, branch_b], output) + var raw_ahead_behind = output[0].split("\t") + var ahead_behind = [] + for msg in raw_ahead_behind: + ahead_behind.append(msg.to_int()) + return ahead_behind if exit == OK else [] + + func get_current_branch(): + var output = [] + var exit = _execute("git rev-parse --abbrev-ref HEAD", output) + return output[0] if exit == OK else "" + + func get_current_tag(): + var output = [] + var exit = _execute("git describe --tags --exact-match", output) + return output[0] if exit == OK else "" + + func get_current_commit(): + var output = [] + var exit = _execute("git rev-parse --short HEAD", output) + return output[0] if exit == OK else "" + + func is_detached_head(): + var output = [] + var exit = _execute("git rev-parse --short HEAD", output) + return (!!output[0]) if exit == OK else true + + func is_up_to_date(args={}): + if fetch().exit == OK: + var branch = args.get("branch", "") + var tag = args.get("tag", "") + var commit = args.get("commit", "") + + if branch: + if branch == get_current_branch(): + return FAILED if is_detached_head() else OK + elif tag: + if tag == get_current_tag(): + return OK + elif commit: + if commit == get_current_commit(): + return OK + + var ahead_behind = get_commit_comparison("HEAD", "origin") + var is_commit_behind = !!ahead_behind[1] if ahead_behind.size() == 2 else false + return FAILED if is_commit_behind else OK + return FAILED + +class _ThreadPool extends RefCounted: + signal all_thread_finished() + + var active = true + + var _threads = [] + var _finished_threads = [] + var _mutex = Mutex.new() + var _tasks = [] + var logger + + func _init(p_logger): + logger = p_logger + _threads.resize(OS.get_processor_count()) + + func _execute_task(task): + var thread = _get_thread() + var can_execute = thread + if can_execute: + task.thread = weakref(thread) + var callable = task.get("callable") + thread.start(_execute.bind(task), task.priority) + logger.debug("Execute task %s.%s() " % [callable.get_object(), callable.get_method()]) + return can_execute + + func _execute(args): + var callable = args.get("callable") + callable.call() + _mutex.lock() + var thread = args.thread.get_ref() + _threads[_threads.find(thread)] = null + _finished_threads.append(thread) + var all_finished = is_all_thread_finished() + _mutex.unlock() + + logger.debug("Execution finished %s.%s() " % [callable.get_object(), callable.get_method()]) + if all_finished: + logger.debug("All thread finished") + emit_signal("all_thread_finished") + + func _flush_tasks(): + if _tasks.size() == 0: + return + + var executed = true + while executed: + var task = _tasks.pop_front() + if task != null: + executed = _execute_task(task) + if not executed: + _tasks.push_front(task) + else: + executed = false + + func _flush_threads(): + for i in _finished_threads.size(): + var thread = _finished_threads.pop_front() + if not thread.is_alive(): + thread.wait_to_finish() + + func enqueue_task(callable, priority=1): + enqueue({"callable": callable, "priority": priority}) + + func enqueue(task): + var can_execute = false + if active: + can_execute = _execute_task(task) + if not can_execute: + _tasks.append(task) + + func process(delta): + if active: + _flush_tasks() + _flush_threads() + + func stop(): + _tasks.clear() + _flush_threads() + + func _get_thread(): + var thread + for i in OS.get_processor_count(): + var t = _threads[i] + if t: + if not t.is_started(): + thread = t + break + else: + thread = Thread.new() + _threads[i] = thread + break + return thread + + func is_all_thread_finished(): + for i in _threads.size(): + if _threads[i]: + return false + return true + + func is_all_task_finished(): + for i in _tasks.size(): + if _tasks[i]: + return false + return true + +class _Logger extends RefCounted: + enum LogLevel { + ALL, DEBUG, INFO, WARN, ERROR, NONE + } + const DEFAULT_LOG_FORMAT_DETAIL = "[{time}] [{level}] {msg}" + const DEFAULT_LOG_FORMAT_NORMAL = "{msg}" + + var log_level = LogLevel.INFO + var log_format = DEFAULT_LOG_FORMAT_NORMAL + var log_time_format = "{year}/{month}/{day} {hour}:{minute}:{second}" + var indent_level = 0 + var is_locked = false + + var _rows + var _max_column_length = [] + var _max_column_size = 0 + + func debug(msg, raw=false): + _log(LogLevel.DEBUG, msg, raw) + + func info(msg, raw=false): + _log(LogLevel.INFO, msg, raw) + + func warn(msg, raw=false): + _log(LogLevel.WARN, msg, raw) + + func error(msg, raw=false): + _log(LogLevel.ERROR, msg, raw) + + func _log(level, msg, raw=false): + if is_locked: + return + + if typeof(msg) != TYPE_STRING: + msg = str(msg) + if log_level <= level: + match level: + LogLevel.WARN: + push_warning(format_log(level, msg)) + LogLevel.ERROR: + push_error(format_log(level, msg)) + _: + if raw: + printraw(format_log(level, msg)) + else: + print(format_log(level, msg)) + + func format_log(level, msg): + return log_format.format({ + "time": log_time_format.format(get_formatted_datatime()), + "level": LogLevel.keys()[level], + "msg": msg.indent(" ".repeat(indent_level)) + }) + + func indent(): + indent_level += 1 + + func dedent(): + indent_level -= 1 + max(indent_level, 0) + + func lock(): + is_locked = true + + func unlock(): + is_locked = false + + func table_start(): + _rows = [] + + func table_end(): + assert(_rows != null, "Expected table_start() to be called first") + for columns in _rows: + var text = "" + for i in columns.size(): + var column = columns[i] + var max_tab_count = ceil(float(_max_column_length[i]) / 4.0) + var tab_count = max_tab_count - ceil(float(column.length()) / 4.0) + var extra_spaces = ceil(float(column.length()) / 4.0) * 4 - column.length() + if i < _max_column_size - 1: + text += column + " ".repeat(extra_spaces) + " ".repeat(tab_count) + else: + text += column + info(text) + + _rows.clear() + _rows = null + _max_column_length.clear() + _max_column_size = 0 + + func table_row(columns=[]): + assert(_rows != null, "Expected table_start() to be called first") + _rows.append(columns) + _max_column_size = max(_max_column_size, columns.size()) + for i in columns.size(): + var column = columns[i] + if _max_column_length.size() >= i + 1: + var max_column_length = _max_column_length[i] + _max_column_length[i] = max(max_column_length, column.length()) + else: + _max_column_length.append(column.length()) + + func get_formatted_datatime(): + var datetime = Time.get_datetime_dict_from_system() + datetime.year = "%04d" % datetime.year + datetime.month = "%02d" % datetime.month + datetime.day = "%02d" % datetime.day + datetime.hour = "%02d" % datetime.hour + datetime.minute = "%02d" % datetime.minute + datetime.second = "%02d" % datetime.second + return datetime diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..9d8b7fa --- /dev/null +++ b/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icon.svg.import b/icon.svg.import new file mode 100644 index 0000000..1b9f4e3 --- /dev/null +++ b/icon.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cea4tcn8jcvsh" +path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://icon.svg" +dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/plug.gd b/plug.gd new file mode 100644 index 0000000..0128e21 --- /dev/null +++ b/plug.gd @@ -0,0 +1,9 @@ +extends "res://addons/gd-plug/plug.gd" + +func _plugging(): + # Declare plugins with plug(repo, args) + # For example, clone from github repo("user/repo_name") + # plug("imjp94/gd-YAFSM") # By default, gd-plug will only install anything from "addons/" directory + # Or you can explicitly specify which file/directory to include + # plug("imjp94/gd-YAFSM", {"include": ["addons/"]}) # By default, gd-plug will only install anything from "addons/" directory + pass diff --git a/project.godot b/project.godot new file mode 100644 index 0000000..da1e689 --- /dev/null +++ b/project.godot @@ -0,0 +1,19 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="Godot Template" +config/features=PackedStringArray("4.3", "Forward Plus") +config/icon="res://icon.svg" + +[editor_plugins] + +enabled=PackedStringArray("res://addons/gd-plug-ui/plugin.cfg")