From cf22890c167c548fc046f940d08f20035a8244e1 Mon Sep 17 00:00:00 2001
From: Melody Becker <me@mstar.dev>
Date: Sun, 26 Jan 2025 08:36:45 +0000
Subject: [PATCH] Initial commit

---
 .gitattributes                                |    2 +
 .gitignore                                    |    3 +
 .plugged/index.cfg                            |    3 +
 addons/gd-plug-ui/Utils.gd                    |   27 +
 addons/gd-plug-ui/assets/icons/add.png        |  Bin 0 -> 149 bytes
 addons/gd-plug-ui/assets/icons/add.png.import |   34 +
 .../gd-plug-ui/assets/icons/edit_internal.png |  Bin 0 -> 361 bytes
 .../assets/icons/edit_internal.png.import     |   34 +
 .../gd-plug-ui/assets/icons/import_check.png  |  Bin 0 -> 418 bytes
 .../assets/icons/import_check.png.import      |   34 +
 .../gd-plug-ui/assets/icons/import_fail.png   |  Bin 0 -> 415 bytes
 .../assets/icons/import_fail.png.import       |   34 +
 .../assets/icons/progress/progress1.png       |  Bin 0 -> 532 bytes
 .../icons/progress/progress1.png.import       |   34 +
 .../assets/icons/progress/progress2.png       |  Bin 0 -> 521 bytes
 .../icons/progress/progress2.png.import       |   34 +
 .../assets/icons/progress/progress3.png       |  Bin 0 -> 529 bytes
 .../icons/progress/progress3.png.import       |   34 +
 .../assets/icons/progress/progress4.png       |  Bin 0 -> 520 bytes
 .../icons/progress/progress4.png.import       |   34 +
 .../assets/icons/progress/progress5.png       |  Bin 0 -> 515 bytes
 .../icons/progress/progress5.png.import       |   34 +
 .../assets/icons/progress/progress6.png       |  Bin 0 -> 502 bytes
 .../icons/progress/progress6.png.import       |   34 +
 .../assets/icons/progress/progress7.png       |  Bin 0 -> 525 bytes
 .../icons/progress/progress7.png.import       |   34 +
 .../assets/icons/progress/progress8.png       |  Bin 0 -> 537 bytes
 .../icons/progress/progress8.png.import       |   34 +
 .../progress/progress_animated_texture.tres   |   29 +
 addons/gd-plug-ui/assets/icons/refresh.png    |  Bin 0 -> 636 bytes
 .../assets/icons/refresh.png.import           |   34 +
 addons/gd-plug-ui/plugin.cfg                  |    7 +
 addons/gd-plug-ui/plugin.gd                   |   80 ++
 .../scene/plugin_settings/PluginSettings.gd   |  306 +++++
 .../scene/plugin_settings/PluginSettings.tscn |  112 ++
 addons/gd-plug/plug.gd                        | 1163 +++++++++++++++++
 icon.svg                                      |    1 +
 icon.svg.import                               |   37 +
 plug.gd                                       |    9 +
 project.godot                                 |   19 +
 40 files changed, 2240 insertions(+)
 create mode 100644 .gitattributes
 create mode 100644 .gitignore
 create mode 100644 .plugged/index.cfg
 create mode 100644 addons/gd-plug-ui/Utils.gd
 create mode 100644 addons/gd-plug-ui/assets/icons/add.png
 create mode 100644 addons/gd-plug-ui/assets/icons/add.png.import
 create mode 100644 addons/gd-plug-ui/assets/icons/edit_internal.png
 create mode 100644 addons/gd-plug-ui/assets/icons/edit_internal.png.import
 create mode 100644 addons/gd-plug-ui/assets/icons/import_check.png
 create mode 100644 addons/gd-plug-ui/assets/icons/import_check.png.import
 create mode 100644 addons/gd-plug-ui/assets/icons/import_fail.png
 create mode 100644 addons/gd-plug-ui/assets/icons/import_fail.png.import
 create mode 100644 addons/gd-plug-ui/assets/icons/progress/progress1.png
 create mode 100644 addons/gd-plug-ui/assets/icons/progress/progress1.png.import
 create mode 100644 addons/gd-plug-ui/assets/icons/progress/progress2.png
 create mode 100644 addons/gd-plug-ui/assets/icons/progress/progress2.png.import
 create mode 100644 addons/gd-plug-ui/assets/icons/progress/progress3.png
 create mode 100644 addons/gd-plug-ui/assets/icons/progress/progress3.png.import
 create mode 100644 addons/gd-plug-ui/assets/icons/progress/progress4.png
 create mode 100644 addons/gd-plug-ui/assets/icons/progress/progress4.png.import
 create mode 100644 addons/gd-plug-ui/assets/icons/progress/progress5.png
 create mode 100644 addons/gd-plug-ui/assets/icons/progress/progress5.png.import
 create mode 100644 addons/gd-plug-ui/assets/icons/progress/progress6.png
 create mode 100644 addons/gd-plug-ui/assets/icons/progress/progress6.png.import
 create mode 100644 addons/gd-plug-ui/assets/icons/progress/progress7.png
 create mode 100644 addons/gd-plug-ui/assets/icons/progress/progress7.png.import
 create mode 100644 addons/gd-plug-ui/assets/icons/progress/progress8.png
 create mode 100644 addons/gd-plug-ui/assets/icons/progress/progress8.png.import
 create mode 100644 addons/gd-plug-ui/assets/icons/progress/progress_animated_texture.tres
 create mode 100644 addons/gd-plug-ui/assets/icons/refresh.png
 create mode 100644 addons/gd-plug-ui/assets/icons/refresh.png.import
 create mode 100644 addons/gd-plug-ui/plugin.cfg
 create mode 100644 addons/gd-plug-ui/plugin.gd
 create mode 100644 addons/gd-plug-ui/scene/plugin_settings/PluginSettings.gd
 create mode 100644 addons/gd-plug-ui/scene/plugin_settings/PluginSettings.tscn
 create mode 100644 addons/gd-plug/plug.gd
 create mode 100644 icon.svg
 create mode 100644 icon.svg.import
 create mode 100644 plug.gd
 create mode 100644 project.godot

diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..8ad74f78
--- /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 00000000..0af181cf
--- /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 00000000..6a781cd3
--- /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 00000000..29049373
--- /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 0000000000000000000000000000000000000000..2fd66b5272b7a0fcabe2e87d9d7def9353671680
GIT binary patch
literal 149
zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|{5@S9Lo9mV
zPCm$az<`JO{9Kpc{!4u?g)e9cSu(+0^XhMvoJ8jdxjE`BUTF(T{Vq+hPWsbiClKI}
wwJUaK;G`7`-yUb)lhA7YyxwE>mQUx@-))t%(6AGe1{%%a>FVdQ&MBb@09W8N=Kufz

literal 0
HcmV?d00001

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 00000000..c18c6a38
--- /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 0000000000000000000000000000000000000000..dac0e4b40377108033219177a58f69ac1102de2c
GIT binary patch
literal 361
zcmV-v0ha!WP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00001b5ch_0Itp)
z=>Px$BS}O-R5*>LlCeqyK@f(&e{w#9oscU6N$D$y*lJ-ZVkPErMC~PA6w->VO<v+|
z+cY*dw|F6A;a$9(LJsr8!p^eu%|E*%DrH%QnIQ>x$K6TOlx?y)88`qIfCHbvb)M(l
z0A_}n9ZK2<khCZ1NK!YzE?SoeA<(So?)3ZpZVo~S0H=KSU$1F(z8%o0`sD6soxJ}A
z_>Xo5Bwa3LS+;)Q_Hb==Pz~8J@Bo~&hid~M>0HvRj@=ze7rkEZ)y!_p>_yU5^LGrO
zC<=<Acw4Pji$?Nu1-)#U+K3HcG#Zg4$-AU+BY9fw=Pkhe7XV-|7y!)N{k|;A@p8FD
z5|ZxR{n6b&+<g*byp1vbwB4c;LZB?m8c2H@V~h-k!*%=x;L?5i8Aw%c00000NkvXX
Hu0mjfumqmW

literal 0
HcmV?d00001

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 00000000..ca1cb0f4
--- /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 0000000000000000000000000000000000000000..f3423e7aca8145d325afba7888b84b88fa9fb15c
GIT binary patch
literal 418
zcmV;T0bTxyP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00001b5ch_0Itp)
z=>Px$TuDShR5*>z(M?Jl0T{;d|1${%r?U_nti~fqY<q=7=qb7{6{KcsVF+YxyU^eP
zJVc>M&LG%^ULdVCE<!QWA!+7u5x*m1R_(k0J?|I%>(Z>C+Kq7p7(UL|W(%lxV}ODq
z4lv(=_xV{d5$!-K8UKbyu5gXrl7cncN29^80`&3#Tm2XWI)j~++AX}<+lC<pBAS;t
zNmvia^%})G!B#&8_yl|g3K!ibxWCQ#$@m$OzesvvXJ3<qb>O5?tOJnOWQn4r*N)ug
zjB>RbpZX~P3wd%)DR^o7Tau^(oa`@OPZ^8`Lq>z)hV3IsB_g+g0<a*tHNjaYNhsgN
zS4VvJ;{%y;i(yDpQpwH^jT}g??5y+m&!{|h?$_ROed@qhUwHd723QP3+LBfQ?5vZf
zsstV%?=DT_TLCvi=0XmHm8N<F95<fV?!{99mmzZ@2$QDbA^d;TIl3a3KgB@9ZU6uP
M07*qoM6N<$g6P__?f?J)

literal 0
HcmV?d00001

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 00000000..7903a60f
--- /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 0000000000000000000000000000000000000000..716c756f9663519e27178598ba405920135f633a
GIT binary patch
literal 415
zcmV;Q0bu@#P)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00001b5ch_0Itp)
z=>Px$SxH1eR5*>LlTA(nK@f$%>LTF+h<DJ4taS%?2PTkkG-CpRJMiO1_awk2&<jjL
zce$9JVHhSR##(i{-utHJRT}<Tk{@g|0uk^Yna^v*mL%X@W}M9caD^G(fDfQ$Nm6fy
z7VrtYMh_7uAaaAqGpd^^q%f;)aPA3_TO=F40YI7pOlUUOh&-UW#{0e{$#-P5P|bQ~
zJJNJ=oxyMjjYb<-Md23jU$Qyf3g;d%+W`Ykr=u(umjD18j{(}i29bNrw(%Fcgs?}X
z4;*DWEj$;13gjJ;yX?{qK(A^(XHYKh0RpPNbQ|zKTxrR-kbk|Lb{?47t_Zef#jeI&
zPP-2bBBX<Jt184*%u7T%gs=w&sD1%QU_c0aoO?udS%zd2sDkPYkuGM3BJIV37}^n$
ze(ZHoouXO-n1NY}*{Uw>0s_6LNsC^QLH?M|hO3Gz{#Enm`T^OxnpDF(QC0u|002ov
JPDHLkV1l=Ps^S0u

literal 0
HcmV?d00001

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 00000000..2c923360
--- /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 0000000000000000000000000000000000000000..622d889617bfd6da1e573d48398fe51afa7237ad
GIT binary patch
literal 532
zcmV+v0_**WP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00001b5ch_0Itp)
z=>Px$&PhZ;R5*>Dlf8=DKoo_)xhsPLAu$B<Ln=pDA=@S3+UcCfQ03P!dV!RxRok^2
zzk(sI>}3_CfrY>(u&`^^#H=URDLi&;g*3S!m^qqrb@=WGYBd-Pu+~zR<w3L7QdJd;
z#iD*3shhp_5{S#P#99kb0-u3b^ZERwfjQn3MFDVbj0wH;B?tll_4|v}YV|(LvWG0o
z{v_yjy8vb~nb2r7DiPWIl@+d>bDw6j*+Z6P#(OaP*h!MS1uU=zLjT-)^G8*!fGZIR
zfmWX9mw*}%>xHVm7LkpJeDzIewOR*=+wB%(OsJ}DRc(mKH{kmo;!^-sZTK%A%=@)5
z1|alj4juzgmL;k?PC*cyn2*a^i*s%VPlPCnPRIB^wPz5k>SLdv0&eRm{C#LT=ic}P
z_dp>cf`}}=xWNPT25Rf|(TMK=%y2jaIO}veG5;Q8)9d?pt?6`H0Kd*gqY=Qi-|yeF
z+ie%eaj2?oW6U$)+Bt`+o&!x|%yK*)uY0}T4WMb7o?c}i^__F>Q4j=MVCJcR@O*v(
z7sq^p;|6)216(Fa@?2G)crF0<BC^c${6~EXCu2<06m~z(HBdxRL=;5;h1b-dIn;09
WF3+@#4^MFb0000<MNUMnLSTZS_wttj

literal 0
HcmV?d00001

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 00000000..7828a362
--- /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 0000000000000000000000000000000000000000..8ee4e2b3ab3f3c54c6888d16078a88ba137c752f
GIT binary patch
literal 521
zcmV+k0`~ohP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00001b5ch_0Itp)
z=>Px$!%0LzR5*==lR=BqKp2LfnIu?2v>>d4rx+Twcv|SiOOM5WkyHQ05_0f=<Z65F
zr3e2&L2rsFvvXa9h264XpwlmVa8lDK`azhPcfR+VciwLTy~y(%Yc0l@BW8@jT1%ei
z{d9Dn?6K!SYK&<Mr5@A$BC5NnstRDNlp1*Gvmeg^nx?6NuK;P9{#G!ZP65<<y(WsH
zORe?(52;%3{T{%B?RNW1K`T4UvR8lwYGB~M8_ypCwTRSB)4UCW;G>AV18$YS<1-O?
zp|##?t+&1iaU26+t>thyAR^-^iar5nzy<K3HSr06h(!M6Be|UiK|mBmuRR&y{n12Y
z3?c$R7=|b0Zdhwi)^QvY{x$O-;hY1&n@Q_IDk6`(f=l40ufm@`bk4o>1@3^V_2AZn
zd%S^Nxt)34wpeSu(^{hziv>V1nM_jt)5gwTyxD9j;1DdAOMqrRpI?tgBbTP>Ktx7L
zsi(k|a}E(114E_MZnav~v)Sw#kfJC~pR$+w#yR&e48sQa;!QjE{`>^Kb^ST%LRppo
z-?J<`6OqRqx9_ypyRs~Q(2JA5u_y`v0j_{5NfMGI0jNACKIa(sh_=Crz7!m#00000
LNkvXXu0mjf8#M8%

literal 0
HcmV?d00001

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 00000000..808edb3c
--- /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 0000000000000000000000000000000000000000..78772d83511037e0a50e053b565057f299d20820
GIT binary patch
literal 529
zcmV+s0`C2ZP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00001b5ch_0Itp)
z=>Px$%Sl8*R5*==lCi7PP!z_0_v8lZAQl`HoJwfK(kalzrGJD&*G~O6LjMnk%<b-w
z*;z9^v66Q|5TD``eb8s1$%%tEHLZ#t9FlzbzLW2s6F5Vfra0#)%ks#TWr=f+G)>Rt
z$hp~b4}rKW%dS!EIXQ1)`VYI^4q#%88F=cIza{`}+g89u9LJ{#=JPp#$@82r3@=6G
z@JCdULl}mad7c9p@4<Ajm&@g2zyTF7@b%66ho0}=jj}9z3#jpDe4wh2MC2eM@4S%F
zXas<Bt_w0y)v>CEBJu(F)D7_-fU1W63rF+W{lIIE9s|GDq^bZ|YfsF5IOnMAx|<16
z6k-3W{0CGOYfmlG+jHV;*gNwPUiS=8UoV|=h=?E}H$K(@KfrzyFGNHxfX|+O0bn+p
z4L~rP&0_wuMpdnvrkR9cxK-7s!1rLg-2$|$)#`dY9@lXk4^(w*jJXe7)pd=kPJp3^
z+%!$|v8t-)z-=7Ir@yj~{iLqzJJ#A3c<ra209b2(0B`pDJ=5tFfS})CI2=+G#n&WB
zR8^&I+iPPC#+bW75PZq9?0r!b?P9U`y@h^*^?Hq;@D;F&qKGJp0Ct{JuQ}o;)A-Tv
T?<|+b00000NkvXXu0mjf5Yzo1

literal 0
HcmV?d00001

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 00000000..1282346c
--- /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 0000000000000000000000000000000000000000..8bb7d9e52b60b30b7728f555f26df67ca81a4732
GIT binary patch
literal 520
zcmV+j0{8uiP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00001b5ch_0Itp)
z=>Px$!bwCyR5*>DlQFB)U=)U*bDBT}!GeQ=Q!S0?Sm@%?{t0J0wfHMSe}#~#bNdf6
zcWd!h2f<46O&4^CT*aZ%c;euf(&k<l4}>pyzw@5FdEYP4s5p*s&XHx=3Cpq!=Nxey
zH`~!B*<+7^D9f^XP~<Uf4w3VR{eBPdLPXjgdgs>xKvh)-;A0d;mkLIs5rCv=N*IQ@
zsvd7iRXv7bn5St9AfAKNvL}<tGr$1{pzW_4&+mA=-s>(Fi%)=w|HemV_C!^Ws`}YC
z((QHuaL(02+Gf@_vrtv9fv>fR4*<+8^lvzk8~1JBbK(g6jmgXa2!h~(d|u8u%Cf9I
zq1WqO#`r%~dk~q~eXk$~Zkj5*51q0spLzv9fxW6Ks`|r&N4$Y&#*ybaRaF5T&LaK}
zAj{<vpfwy0BkqXN6JG;6;JrU&-OXmR2Y$C!s}(>solc8>zh6dC)Hbueh&%+ohzKHb
zB_gkYbiH0*kH_Nzu=#v`d6d2E=Ve*m3xc2muKbqy^+Qn<!z4+LNs<82Iv>#KblB~7
z-v)z$nVD8qRfq_{OW<u$6mNiEv)Sz5vE21LJLhoDvE6QO_5T5cRLyo@et&!b0000<
KMNUMnLSTY=I`Q8C

literal 0
HcmV?d00001

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 00000000..ae098d71
--- /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 0000000000000000000000000000000000000000..58f0068fd2abdf7abb075d22bba4bfa08dc9ad04
GIT binary patch
literal 515
zcmV+e0{s1nP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00001b5ch_0Itp)
z=>Px$y-7qtR5*>Dlf8=DKoo_)xv{{IfEQdOkV<P-ffU)W;M(QBfofOEV+cRVDy>z!
z5u|cyFs_n~gEUo&q}nWj2t2+@u_MP;eu`WWnmM{>=IERm=v11fSZgWE@_?0PiM5tA
zO}p(#m+Y};Kvb4xyHMmY=`Lc9C)@27;H5F9@1eDSUjZ~tQv>g#C^}UznM?poQ4|D0
zP>IOyH>o1KAPB0WC;*J-U|QMJ>GTC)fg0%h=g#xbJl=i_vn=}vsPX^!R8^mg$WBB)
z`9{Jp1i)I`3hArrg{lT3@)`Kjnm7PZ)xhs?Aa}-n-*ey?c!)_=0qFI5C*<R@*5aIN
zJz+E&oyPb-)p`)A>JzV^0`9shybov2xpS}JJFpcIK}7C7xC82@X{f5IO9rTqBK`(o
z=JPqgqw#ng0oEUuOW-x|)))hSh;g-AZGpYSaZH})`!Eb|m&;`hTpDBE8DnmYF*xUN
z&Yi331<+qC7Ihegw|SoL<2e3xl#?VONs?eV9Ik*@9v1KVmG|cd@Zs2>le@59uMv^Q
zgTdepa1C7hcq0+H&$8?*ptITR@3Cw)8?3d*#{Kg;{{&?G$8pGdGDrXb002ovPDHLk
FV1mQZ^EUth

literal 0
HcmV?d00001

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 00000000..1c749536
--- /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 0000000000000000000000000000000000000000..2370d0184d27766b67a6a4a0840f5c9af7d432af
GIT binary patch
literal 502
zcmV<S0SW$zP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00001b5ch_0Itp)
z=>Px$ut`KgR5*=|lh2FOKorM6c}yTuaQCEwr|e)v>BT*I=|3So=RXPkPjWBpt(Tm|
zOF=Kix{>Uh1l^M^TP$RD>cKbNHVXP+m^a^f-+4d22@I^NDx7n4UAM=&uERM;RaL`q
zWJp%*2~c!h7auAVli?%D;h}9?fR{;<WD334{TyJo+iii5MNu4AFqupMlBQ`$)AUM2
z`aen)>C-g5YMKThQ4UEgyDZD+fCIKbrt7=%PZf`Md0p3+KuEO5$03BLBGQY<XALCJ
za{!!kv5+i;@GOLoipUq>TO8sEKnNk#414mx&opMwN#M6kAp`)7F-PP>bI#$tk2Aqq
zi-`vj`77yPmS}%(f!B(?0WOABXxypy{!EkJ0&PsY(ES=%9}pP$+bFS!+=vKKg%c5Z
zr`gy1hm6bRvIXwMT1(&eKQ^1qEARapl?uoKy!UwT&lJlRi^Vq2^H1~n{BAm(-haxg
z)r!$*1n|Op|G*ft13u`~&jE}vKY{h(d`xu4<8l0%F3Ylf6he5Yy#m~d$W2|>*FzPK
sw42Rl00H<4wANazwE(SRL1Xsz3(Bv+MX^Q4rT_o{07*qoM6N<$f>5*SIsgCw

literal 0
HcmV?d00001

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 00000000..acfbca1f
--- /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 0000000000000000000000000000000000000000..9d514c4c073044ecb8038dafce33a724bea52124
GIT binary patch
literal 525
zcmV+o0`mQdP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00001b5ch_0Itp)
z=>Px$$4Nv%R5*=&lRs<QP!z>~?@FR7l*Uj9U0q?(W(;^XbTRY;boS&g(LsoO5zY4O
z#UG%Hp`Dr%8Tn0xG!O_TP=j8tGvvvU)jt=+I~?x0I`3QwI+bM^)>^8nI&f80VXY<0
zvTi%l6?^VAkXBXI3`#vG-65s{Wmz`x%or1U{KcQ=0Q>!Z2VACUdhB4mUIRQ65uzyC
zh)Dg1RgpT1qRn!-1Tdq~=+S|<cp@U7opT?JG2en9h=CZm^ZFjJ^G^))i=wy!)L3f)
zJ^>fNO<mXTMdUIF0{Z<v0KNwxR@H&3Mk4Y9_}PT`!Jns56uoVWfi{j+HS+l65Mt{M
zUBOXVYjMssnUEw2A%Lo)ssMyx_*e12stGYw)faxi2Dt0G@Gg7Kxi{XyJy42>AR=2&
z);uPCbgdTNH7gd8t%zV|vl&1z91c^!^0cb|u5q<mmB8;{KA!{Zr_<@}U@&lLn#QU+
zFvh$BuAOtJ>KV{8#%vdh#cn(v-vXNF`O~M|vii<B_dE>4J@D11e(v-61$=Mka};?|
z6aZI~$>da3U;4WO+>6MzD2kiz7LNYLJkJ3XxCY83Nl20ep!A&jo<ltV-mbuX&;<x*
P00000NkvXXu0mjf8Sd_k

literal 0
HcmV?d00001

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 00000000..895a5e9d
--- /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 0000000000000000000000000000000000000000..5bffcffa059092077eca6c4ffcea5f51b18cd5eb
GIT binary patch
literal 537
zcmV+!0_OdRP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00001b5ch_0Itp)
z=>Px$(@8`@R5*>Dl1+;mK@f(YuAK=6Lm~w7K_H-m6SBvE=Q&FL#hmsqe?sOD<mh?M
zdiLTiXCa<6tYK^j1U3r`8(E0NPML$PJG!?NH1rfzHBY~9!AZ>LbC$~`*4jg^EK97l
z^m@J1a-{k(o6P`T1D}A>T1#1$05R|ikkcV%f620pEXy9%>-8_d`ThsaIe-rU+wFD@
z%;PvdCP<Qme!p*~)9D}Q+(+R115}Ys7>3L7cnn~YB-s&oBuUfs9bi>;4ZJnRG=a~?
z7=Q+__NN9ki=wy$)c7~PRMj^kvJsJQ9+g(B1yB{VrmAgK4MpS!@N+NXGXPZ$ftoSq
z&<xOQHmd`|YL9`PO{xk&5CqkRqp;TEMMXr2qUhMh|B1Z@v8q1t1WVxdl)~%KaL%<n
z!5y#=5kW*&KDlA1^wB0(?rUEI@C(2U1_OYzPNx$CmV2vez^yNSVB=&mSpa{}hQlGi
z_Tu8=rrmD4IF4(o+BU{K2d<oRsOmY;FvhG#qtUwC?cM;I=lT6AS5V(O=N<<^umz^o
zRStsSH}JKpPf#6D6a~O#nx-#Q^{Lkd;7&wVMNwRzx^QG;p637xTmg$Hiin~JVBvG>
bYYy=bd6dq$oaSW<00000NkvXXu0mjfWH0kb

literal 0
HcmV?d00001

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 00000000..eb3f8fbd
--- /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 00000000..5ffbeac3
--- /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 0000000000000000000000000000000000000000..c33438bf9481162297cd230ba335510773a38728
GIT binary patch
literal 636
zcmV-?0)zdDP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00001b5ch_0Itp)
z=>Px%Hc3Q5R5*=&lRs!wK@i4&-|i)lAORzyrH^-BNMR!fB4QPhfT)EM)Xu^pg<xZ0
zAt7~&bP+)?Ng>7}U?B+UEYv0h8%1*O9;k(iaDo!ey?Z+r*%uT1Gq5atGyCn#H!}jr
zvJB@Ok^nF>k|f#2D=UhEMx((_b6J)F901+{7SG3ayA5y#=m9T*8^BPj)!I#vrYXQD
zGdpW$WyA-o07+ibF-fz1eSPn}_g5nM7C{Wc%#KL%6+s9A;7JHUy<Tt3%$6h#0Sjh!
zaeaM#>w`Sc0S*GMfjwq+UD8ZZ6#IeAMx#Ni)xvuZFez!y%(_KUq`(JfW|-N8q`kl_
za5sc-p)AY(vfQeuUaylR31)UDgz!*Of2~%#TC3HlBp#3V{n&{W;M?fv=q|y^vZO4_
znOOFOBx=z&YG%D8N#61wTU%QLc-3q+f5wbaRP>FcWPW~r5D3%L)Bhw{U0r2(cz6@|
z4(zK$d?x9nnH>ezk|g=#;SM1WNy^RaIdH;-5C|bG0+`uNGyCgfU@W%wQiN9kT&L5a
z)9E}gv(L`C%aSJZJSWRCrlzI<@ZRISUyyVTxMgNPy4~(#x7)=vn@t7=1~!245W-hU
zbHF3u6yTPZm#ajabMA2*rrX~8_Q=Qxs=QH)<G`Y%dWDx|c}>ziNi9if#7+Y%X_{`&
z0a3UYW_CD)a6W|a)XY|$b6?_YZ0ty7320<l_B(@}RxL}l-bs>>=lQO64<V4IDZc<Z
WZVOiXY(;+n0000<MNUMnLSTZ%2pSgv

literal 0
HcmV?d00001

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 00000000..848a8a86
--- /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 00000000..a20a3944
--- /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 00000000..1f6a402b
--- /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 00000000..b1ea009a
--- /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 00000000..cf418770
--- /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 00000000..60291cf9
--- /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 00000000..9d8b7fa1
--- /dev/null
+++ b/icon.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128"><rect width="124" height="124" x="2" y="2" fill="#363d52" stroke="#212532" stroke-width="4" rx="14"/><g fill="#fff" transform="translate(12.322 12.322)scale(.101)"><path d="M105 673v33q407 354 814 0v-33z"/><path fill="#478cbf" d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 814 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H446l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z"/><path d="M483 600c0 34 58 34 58 0v-86c0-34-58-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042" transform="translate(12.322 12.322)scale(.101)"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></svg>
\ No newline at end of file
diff --git a/icon.svg.import b/icon.svg.import
new file mode 100644
index 00000000..1b9f4e3d
--- /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 00000000..0128e216
--- /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 00000000..da1e6897
--- /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")