diff --git a/addons/godot-rapier2d/Fluid2D.svg b/addons/godot-rapier2d/Fluid2D.svg
deleted file mode 100644
index e55507f..0000000
--- a/addons/godot-rapier2d/Fluid2D.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
diff --git a/addons/godot-rapier2d/LICENSE b/addons/godot-rapier2d/LICENSE
deleted file mode 100644
index 556baa8..0000000
--- a/addons/godot-rapier2d/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-MIT License
-
-Copyright (c) 2023 Fabrice Cipolla, Sp3ctralCat and Dragos Daian
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
diff --git a/addons/godot-rapier2d/Radial2D.svg b/addons/godot-rapier2d/Radial2D.svg
deleted file mode 100644
index e7f0c49..0000000
--- a/addons/godot-rapier2d/Radial2D.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/addons/godot-rapier2d/THIRDPARTY.txt b/addons/godot-rapier2d/THIRDPARTY.txt
deleted file mode 100644
index d115c87..0000000
--- a/addons/godot-rapier2d/THIRDPARTY.txt
+++ /dev/null
@@ -1,288 +0,0 @@
-Godot Rapier incorporates third-party material from the projects listed below.
-
-Godot Engine (https://github.com/godotengine/godot)
-
- Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md).
- Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to
- deal in the Software without restriction, including without limitation the
- rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
- sell copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in
- all copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
- IN THE SOFTWARE.
-
-godot-cpp (https://github.com/godotengine/godot-cpp)
-
- Copyright (c) 2017-present Godot Engine contributors.
- Copyright (c) 2022-present Mikael Hermansson.
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to
- deal in the Software without restriction, including without limitation the
- rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
- sell copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in
- all copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
- IN THE SOFTWARE.
-
-rapier (https://github.com/dimforge/rapier)
-
- Copyright 2020 Sébastien Crozet
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-
-
-Godot Jolt (https://github.com/godot-jolt/godot-jolt)
-
- Copyright (c) Mikael Hermansson and Godot Jolt contributors.
-
- Permission is hereby granted, free of charge, to any person obtaining a copy of
- this software and associated documentation files (the "Software"), to deal in
- the Software without restriction, including without limitation the rights to
- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
- the Software, and to permit persons to whom the Software is furnished to do so,
- subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-salva (https://github.com/dimforge/salva)
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright 2020 Sébastien Crozet
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
diff --git a/addons/godot-rapier2d/bin/godot_rapier.wasm b/addons/godot-rapier2d/bin/godot_rapier.wasm
deleted file mode 100644
index a2c8c6d..0000000
Binary files a/addons/godot-rapier2d/bin/godot_rapier.wasm and /dev/null differ
diff --git a/addons/godot-rapier2d/bin/libgodot_rapier.android.aarch64-linux-android.so b/addons/godot-rapier2d/bin/libgodot_rapier.android.aarch64-linux-android.so
deleted file mode 100644
index 38fb157..0000000
Binary files a/addons/godot-rapier2d/bin/libgodot_rapier.android.aarch64-linux-android.so and /dev/null differ
diff --git a/addons/godot-rapier2d/bin/libgodot_rapier.android.i686-linux-android.so b/addons/godot-rapier2d/bin/libgodot_rapier.android.i686-linux-android.so
deleted file mode 100644
index b7e8859..0000000
Binary files a/addons/godot-rapier2d/bin/libgodot_rapier.android.i686-linux-android.so and /dev/null differ
diff --git a/addons/godot-rapier2d/bin/libgodot_rapier.android.x86_64-linux-android.so b/addons/godot-rapier2d/bin/libgodot_rapier.android.x86_64-linux-android.so
deleted file mode 100644
index aea50f0..0000000
Binary files a/addons/godot-rapier2d/bin/libgodot_rapier.android.x86_64-linux-android.so and /dev/null differ
diff --git a/addons/godot-rapier2d/bin/libgodot_rapier.ios.framework/Info.plist b/addons/godot-rapier2d/bin/libgodot_rapier.ios.framework/Info.plist
deleted file mode 100644
index 56c6af5..0000000
--- a/addons/godot-rapier2d/bin/libgodot_rapier.ios.framework/Info.plist
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
-
-
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleDevelopmentRegion
- en
- CFBundleExecutable
- libgodot_rapier.ios
- CFBundleName
- Godot Rapier2D
- CFBundleDisplayName
- Godot Rapier2D
- CFBundleIdentifier
- org.godot-rapier2d.godot-rapier2d
- NSHumanReadableCopyright
- Copyright (c) 2023-present Fabrice Cipolla, Sp3ctralCat and Dragos Daian.
- CFBundleVersion
- 0.12.0
- CFBundleShortVersionString
- 0.12.0
- CFBundlePackageType
- FMWK
- CSResourcesFileMapped
-
- DTPlatformName
- iphoneos
- MinimumOSVersion
- 12.0
-
-
diff --git a/addons/godot-rapier2d/bin/libgodot_rapier.ios.framework/libgodot_rapier.ios b/addons/godot-rapier2d/bin/libgodot_rapier.ios.framework/libgodot_rapier.ios
deleted file mode 100644
index 02effc9..0000000
Binary files a/addons/godot-rapier2d/bin/libgodot_rapier.ios.framework/libgodot_rapier.ios and /dev/null differ
diff --git a/addons/godot-rapier2d/bin/libgodot_rapier.linux.x86_64-unknown-linux-gnu.so b/addons/godot-rapier2d/bin/libgodot_rapier.linux.x86_64-unknown-linux-gnu.so
deleted file mode 100644
index 4128396..0000000
Binary files a/addons/godot-rapier2d/bin/libgodot_rapier.linux.x86_64-unknown-linux-gnu.so and /dev/null differ
diff --git a/addons/godot-rapier2d/bin/libgodot_rapier.macos.framework/Resources/Info.plist b/addons/godot-rapier2d/bin/libgodot_rapier.macos.framework/Resources/Info.plist
deleted file mode 100644
index bde2089..0000000
--- a/addons/godot-rapier2d/bin/libgodot_rapier.macos.framework/Resources/Info.plist
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
- CFBundleExecutable
- libgodot_rapier.macos.dylib
- CFBundleIdentifier
- org.godot-rapier2d.godot-rapier2d
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- Godot Rapier2D
- CFBundlePackageType
- FMWK
- CFBundleShortVersionString
- 1.0.0
- CFBundleSupportedPlatforms
-
- MacOSX
-
- NSHumanReadableCopyright
- Copyright (c) 2023-present Fabrice Cipolla, Sp3ctralCat and Dragos Daian.
- CFBundleVersion
- 1.0.0
- LSMinimumSystemVersion
- 10.12
-
-
diff --git a/addons/godot-rapier2d/bin/libgodot_rapier.macos.framework/_CodeSignature/CodeResources b/addons/godot-rapier2d/bin/libgodot_rapier.macos.framework/_CodeSignature/CodeResources
deleted file mode 100644
index 1eaabd7..0000000
--- a/addons/godot-rapier2d/bin/libgodot_rapier.macos.framework/_CodeSignature/CodeResources
+++ /dev/null
@@ -1,128 +0,0 @@
-
-
-
-
- files
-
- Resources/Info.plist
-
- FZy7+eYDJuYSbntBY+mwtyopSvE=
-
-
- files2
-
- Resources/Info.plist
-
- hash2
-
- MMDYIVdI76poLGbcF02jJpJ+oHcs7q1NjyfgsaN1jus=
-
-
-
- rules
-
- ^Resources/
-
- ^Resources/.*\.lproj/
-
- optional
-
- weight
- 1000
-
- ^Resources/.*\.lproj/locversion.plist$
-
- omit
-
- weight
- 1100
-
- ^Resources/Base\.lproj/
-
- weight
- 1010
-
- ^version.plist$
-
-
- rules2
-
- .*\.dSYM($|/)
-
- weight
- 11
-
- ^(.*/)?\.DS_Store$
-
- omit
-
- weight
- 2000
-
- ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/
-
- nested
-
- weight
- 10
-
- ^.*
-
- ^Info\.plist$
-
- omit
-
- weight
- 20
-
- ^PkgInfo$
-
- omit
-
- weight
- 20
-
- ^Resources/
-
- weight
- 20
-
- ^Resources/.*\.lproj/
-
- optional
-
- weight
- 1000
-
- ^Resources/.*\.lproj/locversion.plist$
-
- omit
-
- weight
- 1100
-
- ^Resources/Base\.lproj/
-
- weight
- 1010
-
- ^[^/]+$
-
- nested
-
- weight
- 10
-
- ^embedded\.provisionprofile$
-
- weight
- 20
-
- ^version\.plist$
-
- weight
- 20
-
-
-
-
diff --git a/addons/godot-rapier2d/bin/libgodot_rapier.macos.framework/libgodot_rapier.macos.dylib b/addons/godot-rapier2d/bin/libgodot_rapier.macos.framework/libgodot_rapier.macos.dylib
deleted file mode 100644
index f710e7b..0000000
Binary files a/addons/godot-rapier2d/bin/libgodot_rapier.macos.framework/libgodot_rapier.macos.dylib and /dev/null differ
diff --git a/addons/godot-rapier2d/bin/libgodot_rapier.windows.aarch64-pc-windows-msvc.dll b/addons/godot-rapier2d/bin/libgodot_rapier.windows.aarch64-pc-windows-msvc.dll
deleted file mode 100644
index 971cae5..0000000
Binary files a/addons/godot-rapier2d/bin/libgodot_rapier.windows.aarch64-pc-windows-msvc.dll and /dev/null differ
diff --git a/addons/godot-rapier2d/bin/libgodot_rapier.windows.i686-pc-windows-msvc.dll b/addons/godot-rapier2d/bin/libgodot_rapier.windows.i686-pc-windows-msvc.dll
deleted file mode 100644
index 524761b..0000000
Binary files a/addons/godot-rapier2d/bin/libgodot_rapier.windows.i686-pc-windows-msvc.dll and /dev/null differ
diff --git a/addons/godot-rapier2d/bin/libgodot_rapier.windows.x86_64-pc-windows-msvc.dll b/addons/godot-rapier2d/bin/libgodot_rapier.windows.x86_64-pc-windows-msvc.dll
deleted file mode 100644
index 9847e37..0000000
Binary files a/addons/godot-rapier2d/bin/libgodot_rapier.windows.x86_64-pc-windows-msvc.dll and /dev/null differ
diff --git a/addons/godot-rapier2d/bin/wasm-nothreads/godot_rapier.wasm b/addons/godot-rapier2d/bin/wasm-nothreads/godot_rapier.wasm
deleted file mode 100644
index 2a9fdf3..0000000
Binary files a/addons/godot-rapier2d/bin/wasm-nothreads/godot_rapier.wasm and /dev/null differ
diff --git a/addons/godot-rapier2d/bin/~libgodot_rapier.windows.x86_64-pc-windows-msvc.dll b/addons/godot-rapier2d/bin/~libgodot_rapier.windows.x86_64-pc-windows-msvc.dll
deleted file mode 100644
index 9847e37..0000000
Binary files a/addons/godot-rapier2d/bin/~libgodot_rapier.windows.x86_64-pc-windows-msvc.dll and /dev/null differ
diff --git a/addons/godot-rapier2d/circle_mesh.tres b/addons/godot-rapier2d/circle_mesh.tres
deleted file mode 100644
index 630e7ea..0000000
--- a/addons/godot-rapier2d/circle_mesh.tres
+++ /dev/null
@@ -1,15 +0,0 @@
-[gd_resource type="ArrayMesh" format=3 uid="uid://dahp28qij58i1"]
-
-[resource]
-_surfaces = [{
-"2d": true,
-"aabb": AABB(-16, -16, 0, 32, 32, 0),
-"attribute_data": PackedByteArray(0, 0, 0, 0, 0, 0, 128, 63, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 63, 0, 0, 0, 0, 0, 0, 128, 63, 0, 0, 128, 63),
-"format": 34393296913,
-"index_count": 6,
-"index_data": PackedByteArray(3, 0, 0, 0, 1, 0, 1, 0, 2, 0, 3, 0),
-"primitive": 3,
-"uv_scale": Vector4(0, 0, 0, 0),
-"vertex_count": 4,
-"vertex_data": PackedByteArray(0, 0, 128, 193, 0, 0, 128, 65, 0, 0, 128, 193, 0, 0, 128, 193, 0, 0, 128, 65, 0, 0, 128, 193, 0, 0, 128, 65, 0, 0, 128, 65)
-}]
diff --git a/addons/godot-rapier2d/faucet_2d.gd b/addons/godot-rapier2d/faucet_2d.gd
deleted file mode 100644
index 6fd8653..0000000
--- a/addons/godot-rapier2d/faucet_2d.gd
+++ /dev/null
@@ -1,27 +0,0 @@
-class_name Faucet2D
-extends Fluid2D
-
-@export var interval := 0.06
-@export var max_particles: int = 1000
-@export var width: int = 4
-@export var height: int = 2
-
-var points_new: PackedVector2Array
-var velocities_new: PackedVector2Array
-
-
-func _ready():
- points_new = create_rectangle_points(width, height)
- velocities_new.resize(points_new.size())
- var gravity_value = ProjectSettings.get("physics/2d/default_gravity")
- var gravity_dir = ProjectSettings.get("physics/2d/default_gravity_vector")
- var dir = global_transform.basis_xform(gravity_dir * gravity_value)
- velocities_new.fill(dir)
- get_tree().create_timer(interval).timeout.connect(_on_timer_timeout)
-
-
-func _on_timer_timeout():
- get_tree().create_timer(interval).timeout.connect(_on_timer_timeout)
- if len(points) > max_particles:
- return
- add_points_and_velocities(points_new, velocities_new)
diff --git a/addons/godot-rapier2d/fluid_2d_circle.gd b/addons/godot-rapier2d/fluid_2d_circle.gd
deleted file mode 100644
index 5c93e74..0000000
--- a/addons/godot-rapier2d/fluid_2d_circle.gd
+++ /dev/null
@@ -1,10 +0,0 @@
-@tool
-extends Fluid2D
-
-@export var circle_radius := 10:
- set(value):
- if circle_radius != value:
- circle_radius = value
- points = create_circle_points(circle_radius)
- get:
- return circle_radius
diff --git a/addons/godot-rapier2d/fluid_2d_rectangle.gd b/addons/godot-rapier2d/fluid_2d_rectangle.gd
deleted file mode 100644
index d1eb6a2..0000000
--- a/addons/godot-rapier2d/fluid_2d_rectangle.gd
+++ /dev/null
@@ -1,17 +0,0 @@
-@tool
-extends Fluid2D
-
-@export var height := 10:
- set(value):
- if height != value:
- height = value
- points = create_rectangle_points(width, height)
- get:
- return height
-@export var width := 10:
- set(value):
- if width != value:
- width = value
- points = create_rectangle_points(width, height)
- get:
- return width
diff --git a/addons/godot-rapier2d/fluid_2d_renderer.gd b/addons/godot-rapier2d/fluid_2d_renderer.gd
deleted file mode 100644
index dca76dd..0000000
--- a/addons/godot-rapier2d/fluid_2d_renderer.gd
+++ /dev/null
@@ -1,31 +0,0 @@
-@tool
-class_name Fluid2DRenderer
-extends MultiMeshInstance2D
-
-@export var fluid: Fluid2D
-@export var color: Color = Color(0.8, 0.8, 0.8, 0.3)
-@export var mesh_scale: Vector2 = Vector2(5, 5)
-
-
-func _ready():
- if multimesh == null:
- multimesh = MultiMesh.new()
- multimesh.mesh = load("res://addons/godot-rapier2d/circle_mesh.tres").duplicate()
- multimesh.use_colors = true
- if texture == null:
- texture = load("res://addons/godot-rapier2d/Radial2D.svg")
-
-
-func _process(_delta):
- if fluid == null:
- return
- global_transform = fluid.global_transform
- var index = 0
- multimesh.instance_count = fluid.points.size()
- var points = fluid.points
- for i in points.size():
- var point = points[i]
- var new_transform: Transform2D = Transform2D(0, mesh_scale, 0, point)
- multimesh.set_instance_transform_2d(index, new_transform)
- multimesh.set_instance_color(index, color)
- index += 1
diff --git a/addons/godot-rapier2d/fluid_2d_shader_renderer.gd b/addons/godot-rapier2d/fluid_2d_shader_renderer.gd
deleted file mode 100644
index f2a4c0c..0000000
--- a/addons/godot-rapier2d/fluid_2d_shader_renderer.gd
+++ /dev/null
@@ -1,87 +0,0 @@
-@tool
-class_name Fluid2DShaderRenderer
-extends CanvasLayer
-
-@export var fluid: Fluid2D:
- set(value):
- fluid = value
- update_configuration_warnings()
-@export var camera: Camera2D
-@export var water_material: Material = load("res://addons/godot-rapier2d/water_shader.tres")
-@export var mesh_scale: Vector2 = Vector2(5, 5)
-var fluid_renderer: Fluid2DRenderer
-var inside_camera: Camera2D:
- set(value):
- inside_camera = value
- update_configuration_warnings()
-var sub_viewport_container: SubViewportContainer
-var sub_viewport: SubViewport
-
-
-func _get_configuration_warnings():
- var warnings = []
- if camera == null:
- warnings += ["Camera property is empty."]
- if fluid == null:
- warnings += ["Fluid property is empty."]
- return warnings
-
-
-func _create_subviewport_container():
- sub_viewport_container = SubViewportContainer.new()
- sub_viewport_container.name = "SubViewportContainer"
- add_child(sub_viewport_container)
- sub_viewport_container.material = water_material
- sub_viewport_container.size = Vector2(
- ProjectSettings.get("display/window/size/viewport_width"),
- ProjectSettings.get("display/window/size/viewport_height")
- )
-
-
-func _create_subviewport():
- sub_viewport = SubViewport.new()
- sub_viewport.name = "SubViewport"
- sub_viewport_container.add_child(sub_viewport)
- sub_viewport.transparent_bg = true
- sub_viewport.size = sub_viewport_container.size
-
-
-func _create_fluid_renderer():
- fluid_renderer = Fluid2DRenderer.new()
- fluid_renderer.name = "Fluid2DRenderer"
- fluid_renderer.color = Color(255, 0, 255)
- fluid_renderer.mesh_scale = mesh_scale
- fluid_renderer.fluid = fluid
- sub_viewport.add_child(fluid_renderer)
-
-
-func _create_inside_camera():
- inside_camera = Camera2D.new()
- inside_camera.name = "Camera2D"
- inside_camera.material = water_material
- sub_viewport.add_child(inside_camera)
-
-
-func _ready() -> void:
- _create_subviewport_container()
- _create_subviewport()
- _create_fluid_renderer()
- _create_inside_camera()
- if fluid:
- fluid.debug_draw = false
-
-
-func _process(_delta: float) -> void:
- if camera != null:
- inside_camera.offset = camera.offset
- inside_camera.zoom = camera.zoom
- inside_camera.transform = camera.transform
- sub_viewport_container.scale = Vector2(1.0 / camera.zoom.x, 1.0 / camera.zoom.y)
- sub_viewport_container.position = camera.global_position
- sub_viewport.size = sub_viewport_container.size
- if camera.anchor_mode == Camera2D.AnchorMode.ANCHOR_MODE_FIXED_TOP_LEFT:
- sub_viewport_container.position -= sub_viewport_container.size / 2
- if !camera.ignore_rotation:
- sub_viewport_container.rotation = camera.global_rotation
- else:
- sub_viewport_container.rotation = 0
diff --git a/addons/godot-rapier2d/godot-rapier2d.gdextension b/addons/godot-rapier2d/godot-rapier2d.gdextension
deleted file mode 100644
index de8926f..0000000
--- a/addons/godot-rapier2d/godot-rapier2d.gdextension
+++ /dev/null
@@ -1,34 +0,0 @@
-[configuration]
-
-entry_symbol = "gdext_rust_init"
-compatibility_minimum = 4.3
-
-
-[libraries]
-
-macos.debug = "bin/libgodot_rapier.macos.framework"
-macos.release = "bin/libgodot_rapier.macos.framework"
-windows.debug.x86_64 = "bin/libgodot_rapier.windows.x86_64-pc-windows-msvc.dll"
-windows.release.x86_64 = "bin/libgodot_rapier.windows.x86_64-pc-windows-msvc.dll"
-windows.debug.x86_32 = "bin/libgodot_rapier.windows.i686-pc-windows-msvc.dll"
-windows.release.x86_32 = "bin/libgodot_rapier.windows.i686-pc-windows-msvc.dll"
-windows.debug.arm64 = "bin/libgodot_rapier.windows.aarch64-pc-windows-msvc.dll"
-windows.release.arm64 = "bin/libgodot_rapier.windows.aarch64-pc-windows-msvc.dll"
-linux.debug.x86_64 = "bin/libgodot_rapier.linux.x86_64-unknown-linux-gnu.so"
-linux.release.x86_64 = "bin/libgodot_rapier.linux.x86_64-unknown-linux-gnu.so"
-android.debug.x86_64 = "bin/libgodot_rapier.android.x86_64-linux-android.so"
-android.release.x86_64 = "bin/libgodot_rapier.android.x86_64-linux-android.so"
-android.debug.x86_32 = "bin/libgodot_rapier.android.i686-linux-android.so"
-android.release.x86_32 = "bin/libgodot_rapier.android.i686-linux-android.so"
-android.debug.arm64 = "bin/libgodot_rapier.android.aarch64-linux-android.so"
-android.release.arm64 = "bin/libgodot_rapier.android.aarch64-linux-android.so"
-ios.debug = "bin/libgodot_rapier.ios.framework"
-ios.release = "bin/libgodot_rapier.ios.framework"
-web.debug.threads.wasm32 = "bin/godot_rapier.wasm"
-web.release.threads.wasm32 = "bin/godot_rapier.wasm"
-web.debug.wasm32 = "bin/wasm-nothreads/godot_rapier.wasm"
-web.release.wasm32 = "bin/wasm-nothreads/godot_rapier.wasm"
-
-[icons]
-
-Fluid2D = "Fluid2D.svg"
diff --git a/addons/godot-rapier2d/logo_square_2d.png b/addons/godot-rapier2d/logo_square_2d.png
deleted file mode 100644
index 3a0eb7f..0000000
Binary files a/addons/godot-rapier2d/logo_square_2d.png and /dev/null differ
diff --git a/addons/godot-rapier2d/plugin.info.cfg b/addons/godot-rapier2d/plugin.info.cfg
deleted file mode 100644
index 363abc1..0000000
--- a/addons/godot-rapier2d/plugin.info.cfg
+++ /dev/null
@@ -1,8 +0,0 @@
-[plugin]
-
-name="Godot Rapier 2D"
-description="A 2D and 3D drop-in replacement for the Godot engine that adds stability and fluids."
-author="appsinacup"
-version="0.8.8"
-flavour="godot-rapier-2d-single-simd-parallel"
-script=""
diff --git a/addons/godot-rapier2d/rapier_state_2d.gd b/addons/godot-rapier2d/rapier_state_2d.gd
deleted file mode 100644
index ee5a98f..0000000
--- a/addons/godot-rapier2d/rapier_state_2d.gd
+++ /dev/null
@@ -1,105 +0,0 @@
-### Supports [CollisionObject2D], [Joint2D] and [CollisionShape2D].
-@icon("res://addons/godot-rapier2d/logo_square_2d.png")
-class_name Rapier2DState
-extends Node
-
-var state: Dictionary = {}
-
-
-func _is_physics_object(node: Node) -> bool:
- return node is CollisionObject2D or node is Joint2D
-
-
-func _get_all_physics_nodes(p_node: Node, path: String = "/root/") -> Array[String]:
- var results: Array[String] = []
- if path == "/root/" && _is_physics_object(p_node):
- results.append(path + p_node.name)
- path += p_node.name + "/"
- for node in p_node.get_children():
- if _is_physics_object(node):
- results.append(path + node.name)
- if node.get_child_count() > 0:
- results.append_array(_get_all_physics_nodes(node, path))
- return results
-
-
-## Save a node's physics state
-func save_node(rid: RID, save_json: bool):
- if save_json:
- return JSON.parse_string(RapierPhysicsServer2D.export_json(rid))
- return RapierPhysicsServer2D.export_binary(rid)
-
-
-## Load a node's physics state
-func load_node(rid: RID, data: PackedByteArray):
- RapierPhysicsServer2D.import_binary(rid, data)
-
-
-## Save the state of whole world (single space)
-func save_state(save_json: bool = false) -> int:
- var physics_nodes := _get_all_physics_nodes(get_tree().current_scene)
- for node_path in physics_nodes:
- var node := get_node(node_path)
- var rid: RID
- if node is CollisionObject2D:
- rid = node.get_rid()
- for owner_id in node.get_shape_owners():
- for owner_shape_id in node.shape_owner_get_shape_count(owner_id):
- var shape_rid = node.shape_owner_get_shape(owner_id, owner_shape_id).get_rid()
- state[node_path + "/" + str(owner_id) + "/" + str(owner_shape_id)] = save_node(
- shape_rid, save_json
- )
- if node is Joint2D:
- rid = node.get_rid()
- state[node_path] = save_node(rid, save_json)
- var space_rid = get_viewport().world_2d.space
- state["space"] = save_node(space_rid, save_json)
- state["id"] = RapierPhysicsServer2D.get_global_id()
- return hash(JSON.stringify(state))
-
-
-## Load the state of whole world (single space)
-func load_state() -> int:
- var physics_nodes := _get_all_physics_nodes(get_tree().current_scene)
- for node_path in physics_nodes:
- var node := get_node(node_path)
- var rid: RID
- if node is CollisionObject2D:
- rid = node.get_rid()
- for owner_id in node.get_shape_owners():
- for owner_shape_id in node.shape_owner_get_shape_count(owner_id):
- var shape_rid = node.shape_owner_get_shape(owner_id, owner_shape_id).get_rid()
- var shape_state = state[
- node_path + "/" + str(owner_id) + "/" + str(owner_shape_id)
- ]
- load_node(shape_rid, JSON.parse_string(shape_state))
- if node is Joint2D:
- rid = node.get_rid()
- var node_state = state[node_path]
- load_node(rid, JSON.parse_string(node_state))
- var space_rid = get_viewport().world_2d.space
- load_node(space_rid, JSON.parse_string(state["space"]))
- RapierPhysicsServer2D.set_global_id(int(state["id"]))
- return hash(JSON.stringify(state))
-
-
-## Export the state to file
-func export_state(file_name: String = "user://state.json"):
- save_state(false)
- FileAccess.open(file_name, FileAccess.WRITE).store_string(JSON.stringify(state, " "))
-
-
-## Import the state from file
-func import_state(file_name: String = "user://state.json"):
- state = JSON.parse_string(FileAccess.open(file_name, FileAccess.READ).get_as_text())
- load_state()
-
-
-func _notification(what: int) -> void:
- if what == NOTIFICATION_ENTER_TREE:
- print("enter tree")
- if what == NOTIFICATION_EXIT_TREE:
- save_state(false)
- FileAccess.open("user://save.json", FileAccess.WRITE).store_string(
- JSON.stringify(state, " ")
- )
diff --git a/addons/godot-rapier2d/water_shader.gdshader b/addons/godot-rapier2d/water_shader.gdshader
deleted file mode 100644
index ce31fd6..0000000
--- a/addons/godot-rapier2d/water_shader.gdshader
+++ /dev/null
@@ -1,22 +0,0 @@
-shader_type canvas_item;
-
-uniform float threshold = 0.8;
-uniform vec4 water_color: source_color = vec4(0.12,0.24,0.45,0.65);
-uniform vec4 test_color: source_color = vec4(1,0,1,1);
-
-uniform float speed = 0.1; // Speed of movement
-uniform float amplitude = 0.1; // Amplitude of movement
-
-uniform sampler2D water_texture;
-
-void fragment(){
- float displacement = sin(TIME * speed) * amplitude;
- vec4 screen_tex = texture(TEXTURE, SCREEN_UV).rgba;
-
- float color_distance = screen_tex.r;
- if (color_distance > threshold) {
- COLOR = texture(water_texture, SCREEN_UV + displacement).rgba * water_color;
- } else {
- COLOR = vec4(0.0);
- }
-}
\ No newline at end of file
diff --git a/addons/godot-rapier2d/water_shader.tres b/addons/godot-rapier2d/water_shader.tres
deleted file mode 100644
index 1566b3c..0000000
--- a/addons/godot-rapier2d/water_shader.tres
+++ /dev/null
@@ -1,11 +0,0 @@
-[gd_resource type="ShaderMaterial" load_steps=2 format=3 uid="uid://cysnk7s2ll173"]
-
-[ext_resource type="Shader" path="res://addons/godot-rapier2d/water_shader.gdshader" id="1_wgm3x"]
-
-[resource]
-shader = ExtResource("1_wgm3x")
-shader_parameter/threshold = 0.8
-shader_parameter/water_color = Color(0.12, 0.24, 0.45, 0.65)
-shader_parameter/test_color = Color(1, 0, 1, 1)
-shader_parameter/speed = 0.1
-shader_parameter/amplitude = 0.1
diff --git a/addons/guide/debugger/guide_debugger.gd b/addons/guide/debugger/guide_debugger.gd
new file mode 100644
index 0000000..346a5d1
--- /dev/null
+++ b/addons/guide/debugger/guide_debugger.gd
@@ -0,0 +1,104 @@
+extends MarginContainer
+
+@onready var _actions:Container = %Actions
+@onready var _inputs:Container = %Inputs
+@onready var _priorities:Container = %Priorities
+@onready var _formatter:GUIDEInputFormatter = GUIDEInputFormatter.for_active_contexts()
+
+
+func _ready():
+ process_mode = Node.PROCESS_MODE_ALWAYS
+ GUIDE.input_mappings_changed.connect(_update_priorities)
+ _update_priorities()
+
+func _process(delta):
+ if not is_visible_in_tree():
+ return
+
+ var index:int = 0
+ for mapping in GUIDE._active_action_mappings:
+ var action:GUIDEAction = mapping.action
+
+ var action_name:String = action.name
+ if action_name == "":
+ action_name = action._editor_name()
+
+ var action_state:String = ""
+ match(action._last_state):
+ GUIDEAction.GUIDEActionState.COMPLETED:
+ action_state = "Completed"
+ GUIDEAction.GUIDEActionState.ONGOING:
+ action_state = "Ongoing"
+ GUIDEAction.GUIDEActionState.TRIGGERED:
+ action_state = "Triggered"
+
+ var action_value:String = ""
+ match(action.action_value_type):
+ GUIDEAction.GUIDEActionValueType.BOOL:
+ action_value = str(action.value_bool)
+ GUIDEAction.GUIDEActionValueType.AXIS_1D:
+ action_value = str(action.value_axis_1d)
+ GUIDEAction.GUIDEActionValueType.AXIS_2D:
+ action_value = str(action.value_axis_2d)
+ GUIDEAction.GUIDEActionValueType.AXIS_3D:
+ action_value = str(action.value_axis_3d)
+
+
+
+
+ var label := _get_label(_actions, index)
+ label.text = "[%s] %s - %s" % [action_name, action_state, action_value]
+
+ index += 1
+
+ # Clean out all labels we don't need anymore
+ _cleanup(_actions, index)
+
+ index = 0
+ for input in GUIDE._active_inputs:
+ var input_label = _formatter.input_as_text(input, false)
+ var input_value:String = str(input._value)
+
+ var label := _get_label(_inputs, index)
+ label.text = "%s - %s" % [input_label, input_value]
+ index += 1
+
+ _cleanup(_inputs, index)
+
+
+func _get_label(container:Container, index:int) -> Label:
+ var label:Label = null
+ if container.get_child_count() > index:
+ # reuse existing label
+ label = container.get_child(index)
+ else:
+ # make a new one
+ label = Label.new()
+ label.mouse_filter = Control.MOUSE_FILTER_IGNORE
+ container.add_child(label)
+ return label
+
+func _cleanup(container:Container, index:int) -> void:
+ while container.get_child_count() > index:
+ var to_free = container.get_child(index)
+ container.remove_child(to_free)
+ to_free.queue_free()
+
+func _update_priorities():
+ # since we don't update these per frame, we can just clear them out and
+ # rebuild them when mapping contexts change
+ _cleanup(_priorities, 0)
+
+ for mapping:GUIDEActionMapping in GUIDE._active_action_mappings:
+ var action := mapping.action
+ if GUIDE._actions_sharing_input.has(action):
+ var label := Label.new()
+ var names = ", ".join(GUIDE._actions_sharing_input[action].map(func(it): return it._editor_name()))
+ label.text = "[%s] > [%s]" % [action._editor_name(), names]
+ _priorities.add_child(label)
+
+
+ if _priorities.get_child_count() == 0:
+ var label := Label.new()
+ label.text = ""
+ _priorities.add_child(label)
diff --git a/addons/guide/debugger/guide_debugger.tscn b/addons/guide/debugger/guide_debugger.tscn
new file mode 100644
index 0000000..0e7caf4
--- /dev/null
+++ b/addons/guide/debugger/guide_debugger.tscn
@@ -0,0 +1,50 @@
+[gd_scene load_steps=2 format=3 uid="uid://dkr80d2pi0d41"]
+
+[ext_resource type="Script" path="res://addons/guide/debugger/guide_debugger.gd" id="1_ckdvj"]
+
+[node name="GuideDebugger" type="MarginContainer"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_filter = 2
+script = ExtResource("1_ckdvj")
+
+[node name="VBoxContainer" type="VBoxContainer" parent="."]
+layout_mode = 2
+mouse_filter = 2
+
+[node name="Label" type="Label" parent="VBoxContainer"]
+layout_mode = 2
+text = "G.U.I.D.E - Debugger"
+
+[node name="Label2" type="Label" parent="VBoxContainer"]
+layout_mode = 2
+text = "Actions"
+
+[node name="Actions" type="VFlowContainer" parent="VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 3
+mouse_filter = 2
+
+[node name="Label3" type="Label" parent="VBoxContainer"]
+layout_mode = 2
+text = "Inputs"
+
+[node name="Inputs" type="VFlowContainer" parent="VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 3
+mouse_filter = 2
+
+[node name="Label4" type="Label" parent="VBoxContainer"]
+layout_mode = 2
+text = "Action Priority"
+
+[node name="Priorities" type="VFlowContainer" parent="VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 3
+mouse_filter = 2
diff --git a/addons/guide/editor/action_mapping_editor/action_mapping_editor.gd b/addons/guide/editor/action_mapping_editor/action_mapping_editor.gd
new file mode 100644
index 0000000..03dbdd8
--- /dev/null
+++ b/addons/guide/editor/action_mapping_editor/action_mapping_editor.gd
@@ -0,0 +1,138 @@
+@tool
+extends MarginContainer
+
+const ActionSlot = preload("../action_slot/action_slot.gd")
+const Utils = preload("../utils.gd")
+const ArrayEdit = preload("../array_edit/array_edit.gd")
+
+signal delete_requested()
+signal duplicate_requested()
+
+@export var input_mapping_editor_scene:PackedScene
+@onready var _action_slot:ActionSlot = %ActionSlot
+@onready var _input_mappings:ArrayEdit = %InputMappings
+
+const ClassScanner = preload("../class_scanner.gd")
+
+var _plugin:EditorPlugin
+var _scanner:ClassScanner
+var _undo_redo:EditorUndoRedoManager
+
+var _mapping:GUIDEActionMapping
+
+func _ready():
+ _action_slot.action_changed.connect(_on_action_changed)
+ _input_mappings.delete_requested.connect(_on_input_mapping_delete_requested)
+ _input_mappings.add_requested.connect(_on_input_mappings_add_requested)
+ _input_mappings.move_requested.connect(_on_input_mappings_move_requested)
+ _input_mappings.clear_requested.connect(_on_input_mappings_clear_requested)
+ _input_mappings.duplicate_requested.connect(_on_input_mappings_duplicate_requested)
+ _input_mappings.collapse_state_changed.connect(_on_input_mappings_collapse_state_changed)
+
+func initialize(plugin:EditorPlugin, scanner:ClassScanner):
+ _plugin = plugin
+ _scanner = scanner
+ _undo_redo = _plugin.get_undo_redo()
+
+
+func edit(mapping:GUIDEActionMapping):
+ assert(_mapping == null)
+ _mapping = mapping
+
+ _mapping.changed.connect(_update)
+
+ _update()
+
+
+func _update():
+ _input_mappings.clear()
+
+ _action_slot.action = _mapping.action
+
+ for i in _mapping.input_mappings.size():
+ var input_mapping = _mapping.input_mappings[i]
+ var input_mapping_editor = input_mapping_editor_scene.instantiate()
+ _input_mappings.add_item(input_mapping_editor)
+
+ input_mapping_editor.initialize(_plugin, _scanner)
+ input_mapping_editor.edit(input_mapping)
+
+ _input_mappings.collapsed = _mapping.get_meta("_guide_input_mappings_collapsed", false)
+
+func _on_action_changed():
+ _undo_redo.create_action("Change action")
+ _undo_redo.add_do_property(_mapping, "action", _action_slot.action)
+ _undo_redo.add_undo_property(_mapping, "action", _mapping.action)
+ _undo_redo.commit_action()
+
+
+func _on_input_mappings_add_requested():
+ var values = _mapping.input_mappings.duplicate()
+ var new_mapping = GUIDEInputMapping.new()
+ values.append(new_mapping)
+
+ _undo_redo.create_action("Add input mapping")
+
+ _undo_redo.add_do_property(_mapping, "input_mappings", values)
+ _undo_redo.add_undo_property(_mapping, "input_mappings", _mapping.input_mappings)
+
+ _undo_redo.commit_action()
+
+
+func _on_input_mapping_delete_requested(index:int):
+ var values = _mapping.input_mappings.duplicate()
+ values.remove_at(index)
+
+ _undo_redo.create_action("Delete input mapping")
+ _undo_redo.add_do_property(_mapping, "input_mappings", values)
+ _undo_redo.add_undo_property(_mapping, "input_mappings", _mapping.input_mappings)
+
+ _undo_redo.commit_action()
+
+
+func _on_input_mappings_move_requested(from:int, to:int):
+ var values = _mapping.input_mappings.duplicate()
+ var mapping = values[from]
+ values.remove_at(from)
+ if from < to:
+ to -= 1
+ values.insert(to, mapping)
+
+ _undo_redo.create_action("Move input mapping")
+ _undo_redo.add_do_property(_mapping, "input_mappings", values)
+ _undo_redo.add_undo_property(_mapping, "input_mappings", _mapping.input_mappings)
+
+ _undo_redo.commit_action()
+
+
+func _on_input_mappings_clear_requested():
+ var values:Array[GUIDEInputMapping] = []
+ _undo_redo.create_action("Clear input mappings")
+ _undo_redo.add_do_property(_mapping, "input_mappings", values)
+ _undo_redo.add_undo_property(_mapping, "input_mappings", _mapping.input_mappings)
+
+ _undo_redo.commit_action()
+
+func _on_input_mappings_duplicate_requested(index:int):
+ var values = _mapping.input_mappings.duplicate()
+ var copy:GUIDEInputMapping = values[index].duplicate()
+ copy.input = Utils.duplicate_if_inline(copy.input)
+
+ for i in copy.modifiers.size():
+ copy.modifiers[i] = Utils.duplicate_if_inline(copy.modifiers[i])
+
+ for i in copy.triggers.size():
+ copy.triggers[i] = Utils.duplicate_if_inline(copy.triggers[i])
+
+ # insert copy after original
+ values.insert(index+1, copy)
+
+ _undo_redo.create_action("Duplicate input mapping")
+ _undo_redo.add_do_property(_mapping, "input_mappings", values)
+ _undo_redo.add_undo_property(_mapping, "input_mappings", _mapping.input_mappings)
+
+ _undo_redo.commit_action()
+
+
+func _on_input_mappings_collapse_state_changed(new_state:bool):
+ _mapping.set_meta("_guide_input_mappings_collapsed", new_state)
diff --git a/addons/guide/editor/action_mapping_editor/action_mapping_editor.tscn b/addons/guide/editor/action_mapping_editor/action_mapping_editor.tscn
new file mode 100644
index 0000000..3730f92
--- /dev/null
+++ b/addons/guide/editor/action_mapping_editor/action_mapping_editor.tscn
@@ -0,0 +1,43 @@
+[gd_scene load_steps=5 format=3 uid="uid://361aipcef24h"]
+
+[ext_resource type="Script" path="res://addons/guide/editor/action_mapping_editor/action_mapping_editor.gd" id="1_2k0pi"]
+[ext_resource type="PackedScene" uid="uid://du4x7ng6ntuk4" path="res://addons/guide/editor/action_slot/action_slot.tscn" id="1_hguf2"]
+[ext_resource type="PackedScene" uid="uid://c323mdijdhktg" path="res://addons/guide/editor/input_mapping_editor/input_mapping_editor.tscn" id="2_a8nbp"]
+[ext_resource type="PackedScene" uid="uid://cly0ff32fvpb2" path="res://addons/guide/editor/array_edit/array_edit.tscn" id="4_ehr5j"]
+
+[node name="ActionMappingEditor" type="MarginContainer"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_vertical = 0
+theme_override_constants/margin_bottom = 5
+script = ExtResource("1_2k0pi")
+input_mapping_editor_scene = ExtResource("2_a8nbp")
+
+[node name="HBoxContainer" type="HBoxContainer" parent="."]
+layout_mode = 2
+size_flags_vertical = 0
+
+[node name="HBoxContainer" type="HBoxContainer" parent="HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 0
+
+[node name="ActionSlot" parent="HBoxContainer/HBoxContainer" instance=ExtResource("1_hguf2")]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="VBoxContainer" type="VBoxContainer" parent="HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 0
+size_flags_stretch_ratio = 4.0
+
+[node name="InputMappings" parent="HBoxContainer/VBoxContainer" instance=ExtResource("4_ehr5j")]
+unique_name_in_owner = true
+layout_mode = 2
+title = "Input mappings"
+add_tooltip = "Add input mapping"
+clear_tooltip = "Clear input mappings"
diff --git a/addons/guide/editor/action_slot/action_slot.gd b/addons/guide/editor/action_slot/action_slot.gd
new file mode 100644
index 0000000..4108d3b
--- /dev/null
+++ b/addons/guide/editor/action_slot/action_slot.gd
@@ -0,0 +1,57 @@
+@tool
+extends LineEdit
+
+signal action_changed()
+
+var index:int
+
+var action:GUIDEAction:
+ set(value):
+ if is_instance_valid(action):
+ action.changed.disconnect(_refresh)
+
+ action = value
+
+ if is_instance_valid(action):
+ action.changed.connect(_refresh)
+
+ # action_changed can only be emitted by
+ # dragging an action into this, not when setting
+ # the property
+ _refresh()
+
+
+func _refresh():
+ if not is_instance_valid(action):
+ text = ""
+ tooltip_text = ""
+ else:
+ text = action._editor_name()
+ tooltip_text = action.resource_path
+
+func _can_drop_data(at_position, data) -> bool:
+ if not data is Dictionary:
+ return false
+
+ if data.has("files"):
+ for file in data["files"]:
+ if ResourceLoader.load(file) is GUIDEAction:
+ return true
+
+ return false
+
+
+func _drop_data(at_position, data) -> void:
+
+ for file in data["files"]:
+ var item = ResourceLoader.load(file)
+ if item is GUIDEAction:
+ action = item
+ action_changed.emit()
+
+func _gui_input(event):
+ if event is InputEventMouseButton:
+ if event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
+ if is_instance_valid(action):
+ EditorInterface.edit_resource(action)
+
diff --git a/addons/guide/editor/action_slot/action_slot.tscn b/addons/guide/editor/action_slot/action_slot.tscn
new file mode 100644
index 0000000..956be79
--- /dev/null
+++ b/addons/guide/editor/action_slot/action_slot.tscn
@@ -0,0 +1,16 @@
+[gd_scene load_steps=2 format=3 uid="uid://du4x7ng6ntuk4"]
+
+[ext_resource type="Script" path="res://addons/guide/editor/action_slot/action_slot.gd" id="1_w5nxd"]
+
+[node name="ActionSlot" type="LineEdit"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_horizontal = 3
+size_flags_vertical = 0
+text = "Name"
+editable = false
+selecting_enabled = false
+script = ExtResource("1_w5nxd")
diff --git a/addons/guide/editor/array_edit/array_edit.gd b/addons/guide/editor/array_edit/array_edit.gd
new file mode 100644
index 0000000..0c5bfaa
--- /dev/null
+++ b/addons/guide/editor/array_edit/array_edit.gd
@@ -0,0 +1,113 @@
+@tool
+extends Container
+const Utils = preload("../utils.gd")
+
+@export var item_scene:PackedScene
+
+@export var title:String = "":
+ set(value):
+ title = value
+ _refresh()
+
+@export var add_tooltip:String:
+ set(value):
+ add_tooltip = value
+ _refresh()
+
+@export var clear_tooltip:String:
+ set(value):
+ clear_tooltip = value
+ _refresh()
+
+@export var item_separation:int = 8:
+ set(value):
+ item_separation = value
+ _refresh()
+
+
+@export var collapsed:bool = false:
+ set(value):
+ collapsed = value
+ _refresh()
+
+signal add_requested()
+signal delete_requested(index:int)
+signal move_requested(from:int, to:int)
+signal insert_requested(index:int)
+signal duplicate_requested(index:int)
+signal clear_requested()
+signal collapse_state_changed(collapsed:bool)
+
+@onready var _add_button:Button = %AddButton
+@onready var _clear_button:Button = %ClearButton
+@onready var _contents:Container = %Contents
+@onready var _title_label:Label = %TitleLabel
+@onready var _collapse_button:Button = %CollapseButton
+@onready var _expand_button:Button = %ExpandButton
+@onready var _count_label:Label = %CountLabel
+
+func _ready():
+ _add_button.icon = get_theme_icon("Add", "EditorIcons")
+ _add_button.pressed.connect(func(): add_requested.emit())
+
+ _clear_button.icon = get_theme_icon("Clear", "EditorIcons")
+ _clear_button.pressed.connect(func(): clear_requested.emit())
+
+ _collapse_button.icon = get_theme_icon("Collapse", "EditorIcons")
+ _collapse_button.pressed.connect(_on_collapse_pressed)
+
+ _expand_button.icon = get_theme_icon("Forward", "EditorIcons")
+ _expand_button.pressed.connect(_on_expand_pressed)
+
+
+ _refresh()
+
+
+func _refresh():
+ if is_instance_valid(_add_button):
+ _add_button.tooltip_text = add_tooltip
+ if is_instance_valid(_clear_button):
+ _clear_button.tooltip_text = clear_tooltip
+ _clear_button.visible = _contents.get_child_count() > 0
+
+ if is_instance_valid(_contents):
+ _contents.add_theme_constant_override("separation", item_separation)
+ _contents.visible = not collapsed
+
+ if is_instance_valid(_collapse_button):
+ _collapse_button.visible = not collapsed
+
+ if is_instance_valid(_expand_button):
+ _expand_button.visible = collapsed
+
+ if is_instance_valid(_title_label):
+ _title_label.text = title
+
+ if is_instance_valid(_count_label):
+ _count_label.text = "(%s)" % [_contents.get_child_count()]
+
+
+func clear():
+ Utils.clear(_contents)
+ _refresh()
+
+
+func add_item(new_item:Control):
+ var item_wrapper = item_scene.instantiate()
+ _contents.add_child(item_wrapper)
+ item_wrapper.initialize(new_item)
+ item_wrapper.move_requested.connect(func(from:int, to:int): move_requested.emit(from, to))
+ item_wrapper.delete_requested.connect(func(idx:int): delete_requested.emit(idx) )
+ item_wrapper.duplicate_requested.connect(func(idx:int): duplicate_requested.emit(idx) )
+ _refresh()
+
+
+func _on_collapse_pressed():
+ collapsed = true
+ collapse_state_changed.emit(true)
+
+
+func _on_expand_pressed():
+ collapsed = false
+ collapse_state_changed.emit(false)
+
diff --git a/addons/guide/editor/array_edit/array_edit.tscn b/addons/guide/editor/array_edit/array_edit.tscn
new file mode 100644
index 0000000..c6c7408
--- /dev/null
+++ b/addons/guide/editor/array_edit/array_edit.tscn
@@ -0,0 +1,88 @@
+[gd_scene load_steps=5 format=3 uid="uid://cly0ff32fvpb2"]
+
+[ext_resource type="Script" path="res://addons/guide/editor/array_edit/array_edit.gd" id="1_y3qyt"]
+[ext_resource type="PackedScene" uid="uid://cjabwsa4gmlpp" path="res://addons/guide/editor/array_edit/array_edit_item.tscn" id="2_n3ncl"]
+
+[sub_resource type="Image" id="Image_efj5n"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_uapko"]
+image = SubResource("Image_efj5n")
+
+[node name="Array" type="MarginContainer"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_y3qyt")
+item_scene = ExtResource("2_n3ncl")
+item_separation = 10
+
+[node name="VBoxContainer" type="VBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer"]
+layout_mode = 2
+
+[node name="Panel" type="Panel" parent="VBoxContainer/MarginContainer"]
+layout_mode = 2
+
+[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/MarginContainer"]
+layout_mode = 2
+
+[node name="CollapseButton" type="Button" parent="VBoxContainer/MarginContainer/HBoxContainer"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(32, 0)
+layout_mode = 2
+size_flags_horizontal = 0
+tooltip_text = "Collapse"
+icon = SubResource("ImageTexture_uapko")
+
+[node name="ExpandButton" type="Button" parent="VBoxContainer/MarginContainer/HBoxContainer"]
+unique_name_in_owner = true
+visible = false
+custom_minimum_size = Vector2(48, 0)
+layout_mode = 2
+size_flags_horizontal = 0
+tooltip_text = "Expand"
+icon = SubResource("ImageTexture_uapko")
+
+[node name="AddButton" type="Button" parent="VBoxContainer/MarginContainer/HBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 0
+icon = SubResource("ImageTexture_uapko")
+
+[node name="ClearButton" type="Button" parent="VBoxContainer/MarginContainer/HBoxContainer"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+size_flags_horizontal = 0
+icon = SubResource("ImageTexture_uapko")
+
+[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer/MarginContainer/HBoxContainer"]
+layout_mode = 2
+
+[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/MarginContainer/HBoxContainer/MarginContainer"]
+layout_mode = 2
+
+[node name="TitleLabel" type="Label" parent="VBoxContainer/MarginContainer/HBoxContainer/MarginContainer/HBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="CountLabel" type="Label" parent="VBoxContainer/MarginContainer/HBoxContainer/MarginContainer/HBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "(0)"
+
+[node name="Contents" type="VBoxContainer" parent="VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_override_constants/separation = 10
diff --git a/addons/guide/editor/array_edit/array_edit_item.gd b/addons/guide/editor/array_edit/array_edit_item.gd
new file mode 100644
index 0000000..13c8a03
--- /dev/null
+++ b/addons/guide/editor/array_edit/array_edit_item.gd
@@ -0,0 +1,84 @@
+@tool
+extends Container
+const Utils = preload("../utils.gd")
+const Dragger = preload("dragger.gd")
+
+signal move_requested(from:int, to:int)
+signal delete_requested(index:int)
+signal duplicate_requested(index:int)
+
+@onready var _dragger:Dragger = %Dragger
+@onready var _content:Container = %Content
+@onready var _before_indicator:ColorRect = %BeforeIndicator
+@onready var _after_indicator:ColorRect = %AfterIndicator
+@onready var _popup_menu:PopupMenu = %PopupMenu
+
+
+const ID_DELETE = 2
+const ID_DUPLICATE = 3
+
+func _ready():
+ _dragger.icon = get_theme_icon("GuiSpinboxUpdown", "EditorIcons")
+ _before_indicator.color = get_theme_color("box_selection_stroke_color", "Editor")
+ _after_indicator.color = get_theme_color("box_selection_stroke_color", "Editor")
+ _before_indicator.visible = false
+ _after_indicator.visible = false
+ _dragger._parent_array = get_parent()
+ _dragger._index = get_index()
+ _dragger.pressed.connect(_show_popup_menu)
+
+ _popup_menu.clear()
+ _popup_menu.add_icon_item(get_theme_icon("Duplicate", "EditorIcons"), "Duplicate", ID_DUPLICATE)
+ _popup_menu.add_icon_item(get_theme_icon("Remove", "EditorIcons"), "Delete", ID_DELETE)
+ _popup_menu.id_pressed.connect(_on_popup_menu_id_pressed)
+
+func initialize(content:Control):
+ Utils.clear(_content)
+ _content.add_child(content)
+
+
+func _can_drop_data(at_position:Vector2, data) -> bool:
+ if data is Dictionary and data.has("parent_array") and data.parent_array == get_parent() and data.index != get_index():
+ var height = size.y
+
+ var is_before = not _is_last_child() or (at_position.y < height/2.0)
+ if is_before and data.index == get_index() - 1:
+ # don't allow the previous child to be inserted at its
+ # own position
+ return false
+
+ _before_indicator.visible = is_before
+ _after_indicator.visible = not is_before
+ return true
+
+ return false
+
+
+func _drop_data(at_position, data):
+ var height = size.y
+ var is_before = not _is_last_child() or (at_position.y < height/2.0)
+ var from = data.index
+ var to = get_index() if is_before else get_index() + 1
+ move_requested.emit(data.index, to)
+ _before_indicator.visible = false
+ _after_indicator.visible = false
+
+func _is_last_child() -> bool:
+ return get_index() == get_parent().get_child_count() - 1
+
+
+func _on_mouse_exited():
+ _before_indicator.visible = false
+ _after_indicator.visible = false
+
+
+func _show_popup_menu():
+ _popup_menu.popup(Rect2(get_global_mouse_position(), Vector2.ZERO))
+
+
+func _on_popup_menu_id_pressed(id:int):
+ match id:
+ ID_DELETE:
+ delete_requested.emit(get_index())
+ ID_DUPLICATE:
+ duplicate_requested.emit(get_index())
diff --git a/addons/guide/editor/array_edit/array_edit_item.tscn b/addons/guide/editor/array_edit/array_edit_item.tscn
new file mode 100644
index 0000000..be3b43b
--- /dev/null
+++ b/addons/guide/editor/array_edit/array_edit_item.tscn
@@ -0,0 +1,83 @@
+[gd_scene load_steps=5 format=3 uid="uid://cjabwsa4gmlpp"]
+
+[ext_resource type="Script" path="res://addons/guide/editor/array_edit/array_edit_item.gd" id="1_ujx05"]
+[ext_resource type="Script" path="res://addons/guide/editor/array_edit/dragger.gd" id="2_53e2r"]
+
+[sub_resource type="Image" id="Image_efj5n"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_uapko"]
+image = SubResource("Image_efj5n")
+
+[node name="ArrayEditItem" type="MarginContainer"]
+anchors_preset = 10
+anchor_right = 1.0
+offset_bottom = 8.0
+grow_horizontal = 2
+script = ExtResource("1_ujx05")
+
+[node name="MarginContainer" type="MarginContainer" parent="."]
+layout_mode = 2
+theme_override_constants/margin_top = 2
+theme_override_constants/margin_bottom = 2
+
+[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer"]
+layout_mode = 2
+
+[node name="Dragger" type="Button" parent="MarginContainer/HBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 0
+tooltip_text = "Drag to reorder, click for options."
+focus_mode = 0
+mouse_filter = 1
+icon = SubResource("ImageTexture_uapko")
+script = ExtResource("2_53e2r")
+
+[node name="Content" type="MarginContainer" parent="MarginContainer/HBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="VBoxContainer" type="VBoxContainer" parent="."]
+layout_mode = 2
+mouse_filter = 2
+
+[node name="BeforeIndicator" type="ColorRect" parent="VBoxContainer"]
+unique_name_in_owner = true
+visible = false
+custom_minimum_size = Vector2(0, 2)
+layout_mode = 2
+mouse_filter = 2
+color = Color(0, 0, 0, 1)
+
+[node name="Control" type="Control" parent="VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+mouse_filter = 2
+
+[node name="AfterIndicator" type="ColorRect" parent="VBoxContainer"]
+unique_name_in_owner = true
+visible = false
+custom_minimum_size = Vector2(0, 2)
+layout_mode = 2
+mouse_filter = 2
+color = Color(0, 0, 0, 1)
+
+[node name="PopupMenu" type="PopupMenu" parent="."]
+unique_name_in_owner = true
+item_count = 2
+item_0/text = "Duplicate"
+item_0/icon = SubResource("ImageTexture_uapko")
+item_0/id = 3
+item_1/text = "Delete"
+item_1/icon = SubResource("ImageTexture_uapko")
+item_1/id = 2
+
+[connection signal="mouse_exited" from="." to="." method="_on_mouse_exited"]
diff --git a/addons/guide/editor/array_edit/dragger.gd b/addons/guide/editor/array_edit/dragger.gd
new file mode 100644
index 0000000..629283e
--- /dev/null
+++ b/addons/guide/editor/array_edit/dragger.gd
@@ -0,0 +1,8 @@
+@tool
+extends Button
+
+var _parent_array:Variant
+var _index:int
+
+func _get_drag_data(at_position):
+ return { "parent_array" : _parent_array, "index" : _index }
diff --git a/addons/guide/editor/binding_dialog/binding_dialog.gd b/addons/guide/editor/binding_dialog/binding_dialog.gd
new file mode 100644
index 0000000..96a1b02
--- /dev/null
+++ b/addons/guide/editor/binding_dialog/binding_dialog.gd
@@ -0,0 +1,148 @@
+@tool
+extends Window
+
+const ClassScanner = preload("../class_scanner.gd")
+const Utils = preload("../utils.gd")
+
+signal input_selected(input:GUIDEInput)
+
+@onready var _input_display = %InputDisplay
+@onready var _available_types:Container = %AvailableTypes
+@onready var _none_available:Control = %NoneAvailable
+@onready var _some_available:Control = %SomeAvailable
+@onready var _select_bool_button:Button = %SelectBoolButton
+@onready var _select_1d_button:Button = %Select1DButton
+@onready var _select_2d_button:Button = %Select2DButton
+@onready var _select_3d_button:Button = %Select3DButton
+@onready var _instructions_label:Label = %InstructionsLabel
+@onready var _accept_detection_button:Button = %AcceptDetectionButton
+@onready var _input_detector:GUIDEInputDetector = %InputDetector
+@onready var _detect_bool_button:Button = %DetectBoolButton
+@onready var _detect_1d_button:Button = %Detect1DButton
+@onready var _detect_2d_button:Button = %Detect2DButton
+@onready var _detect_3d_button:Button = %Detect3DButton
+
+var _scanner:ClassScanner
+var _last_detected_input:GUIDEInput
+
+
+func initialize(scanner:ClassScanner):
+ _scanner = scanner
+ _setup_dialog()
+
+func _setup_dialog():
+ # we need to bind this here. if we bind it in the editor, the editor
+ # will crash when opening the scene because it will delete the node it
+ # just tries to edit.
+ focus_exited.connect(_on_close_requested)
+
+ _show_inputs_of_value_type(GUIDEAction.GUIDEActionValueType.BOOL)
+ _instructions_label.text = tr("Press one of the buttons above to detect an input.")
+ _accept_detection_button.visible = false
+
+
+func _on_close_requested():
+ hide()
+ queue_free()
+
+
+func _show_inputs_of_value_type(type:GUIDEAction.GUIDEActionValueType) -> void:
+ var items:Array[GUIDEInput] = []
+
+ _select_bool_button.set_pressed_no_signal(type == GUIDEAction.GUIDEActionValueType.BOOL)
+ _select_1d_button.set_pressed_no_signal(type == GUIDEAction.GUIDEActionValueType.AXIS_1D)
+ _select_2d_button.set_pressed_no_signal(type == GUIDEAction.GUIDEActionValueType.AXIS_2D)
+ _select_3d_button.set_pressed_no_signal(type == GUIDEAction.GUIDEActionValueType.AXIS_3D)
+
+ var all_inputs = _scanner.find_inheritors("GUIDEInput")
+ for script in all_inputs.values():
+ var dummy:GUIDEInput = script.new()
+ if dummy._native_value_type() == type:
+ items.append(dummy)
+
+ _some_available.visible = not items.is_empty()
+ _none_available.visible = items.is_empty()
+
+ if items.is_empty():
+ return
+
+ items.sort_custom(func(a,b): return a._editor_name().nocasecmp_to(b._editor_name()) < 0)
+ Utils.clear(_available_types)
+
+ for item in items:
+ var button = Button.new()
+ button.text = item._editor_name()
+ button.tooltip_text = item._editor_description()
+ button.pressed.connect(_deliver.bind(item))
+ button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+
+ _available_types.add_child(button)
+
+
+func _deliver(input:GUIDEInput):
+ input_selected.emit(input)
+ hide()
+ queue_free()
+
+
+func _on_select_bool_button_pressed():
+ _show_inputs_of_value_type(GUIDEAction.GUIDEActionValueType.BOOL)
+
+
+func _on_select_1d_button_pressed():
+ _show_inputs_of_value_type(GUIDEAction.GUIDEActionValueType.AXIS_1D)
+
+
+func _on_select_2d_button_pressed():
+ _show_inputs_of_value_type(GUIDEAction.GUIDEActionValueType.AXIS_2D)
+
+
+func _on_select_3d_button_pressed():
+ _show_inputs_of_value_type(GUIDEAction.GUIDEActionValueType.AXIS_3D)
+
+
+func _on_input_detector_detection_started():
+ _instructions_label.text = tr("Actuate the input now...")
+
+
+func _on_input_detector_input_detected(input:GUIDEInput):
+ _instructions_label.visible = false
+ _input_display.visible = true
+ _input_display.input = input
+ _accept_detection_button.visible = true
+ _last_detected_input = input
+
+
+func _begin_detect_input(type:GUIDEAction.GUIDEActionValueType):
+ _last_detected_input = null
+ _instructions_label.visible = true
+ _instructions_label.text = tr("Get ready...")
+ _accept_detection_button.visible = false
+ _input_display.visible = false
+ _input_detector.detect(type)
+
+
+func _on_detect_bool_button_pressed():
+ _detect_bool_button.release_focus()
+ _begin_detect_input(GUIDEAction.GUIDEActionValueType.BOOL)
+
+
+func _on_detect_1d_button_pressed():
+ _detect_1d_button.release_focus()
+ _begin_detect_input(GUIDEAction.GUIDEActionValueType.AXIS_1D)
+
+
+func _on_detect_2d_button_pressed():
+ _detect_2d_button.release_focus()
+ _begin_detect_input(GUIDEAction.GUIDEActionValueType.AXIS_2D)
+
+
+func _on_detect_3d_button_pressed():
+ _detect_3d_button.release_focus()
+ _begin_detect_input(GUIDEAction.GUIDEActionValueType.AXIS_3D)
+
+
+func _on_accept_detection_button_pressed():
+ input_selected.emit(_last_detected_input)
+ hide()
+ queue_free
diff --git a/addons/guide/editor/binding_dialog/binding_dialog.tscn b/addons/guide/editor/binding_dialog/binding_dialog.tscn
new file mode 100644
index 0000000..056fdac
--- /dev/null
+++ b/addons/guide/editor/binding_dialog/binding_dialog.tscn
@@ -0,0 +1,216 @@
+[gd_scene load_steps=5 format=3 uid="uid://dic27bm4pfw3q"]
+
+[ext_resource type="Script" path="res://addons/guide/editor/binding_dialog/binding_dialog.gd" id="1_tknjd"]
+[ext_resource type="PackedScene" uid="uid://dsv7s6tfmnsrs" path="res://addons/guide/editor/input_display/input_display.tscn" id="2_83ieu"]
+[ext_resource type="Script" path="res://addons/guide/remapping/guide_input_detector.gd" id="3_c6q6r"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_3e874"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(1, 0.365, 0.365, 1)
+draw_center = false
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+corner_detail = 1
+
+[node name="BindingDialog" type="Window"]
+title = "Input Configuration"
+initial_position = 4
+size = Vector2i(1200, 600)
+popup_window = true
+min_size = Vector2i(1200, 600)
+script = ExtResource("1_tknjd")
+
+[node name="MarginContainer" type="MarginContainer" parent="."]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_constants/margin_bottom = 5
+
+[node name="BGPanel" type="Panel" parent="MarginContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_override_styles/panel = SubResource("StyleBoxFlat_3e874")
+
+[node name="MarginContainer" type="MarginContainer" parent="MarginContainer"]
+layout_mode = 2
+theme_override_constants/margin_left = 5
+theme_override_constants/margin_top = 5
+theme_override_constants/margin_right = 5
+theme_override_constants/margin_bottom = 5
+
+[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/MarginContainer"]
+layout_mode = 2
+
+[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/MarginContainer/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="LeftPanel" type="Panel" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer"]
+layout_mode = 2
+theme_override_constants/margin_left = 5
+theme_override_constants/margin_top = 5
+theme_override_constants/margin_right = 5
+theme_override_constants/margin_bottom = 5
+
+[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer/MarginContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_constants/separation = 10
+
+[node name="Label" type="Label" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer/MarginContainer/VBoxContainer"]
+layout_mode = 2
+text = "Detect Input"
+horizontal_alignment = 1
+
+[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer/MarginContainer/VBoxContainer"]
+layout_mode = 2
+
+[node name="DetectBoolButton" type="Button" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer/MarginContainer/VBoxContainer/HBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Boolean"
+
+[node name="Detect1DButton" type="Button" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer/MarginContainer/VBoxContainer/HBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+text = "1D"
+
+[node name="Detect2DButton" type="Button" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer/MarginContainer/VBoxContainer/HBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+text = "2D"
+
+[node name="Detect3DButton" type="Button" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer/MarginContainer/VBoxContainer/HBoxContainer"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(80, 0)
+layout_mode = 2
+size_flags_horizontal = 3
+text = "3D"
+
+[node name="InstructionsLabel" type="Label" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer/MarginContainer/VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 6
+text = "3..2..1.."
+horizontal_alignment = 1
+vertical_alignment = 1
+autowrap_mode = 2
+
+[node name="InputDisplay" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer/MarginContainer/VBoxContainer" instance=ExtResource("2_83ieu")]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 6
+
+[node name="AcceptDetectionButton" type="Button" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer/MarginContainer/VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 4
+text = "Accept"
+
+[node name="MarginContainer2" type="MarginContainer" parent="MarginContainer/MarginContainer/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="RightPanel" type="Panel" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer2"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer2"]
+layout_mode = 2
+theme_override_constants/margin_left = 5
+theme_override_constants/margin_top = 5
+theme_override_constants/margin_right = 5
+theme_override_constants/margin_bottom = 5
+
+[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer2/MarginContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_constants/separation = 10
+
+[node name="Label" type="Label" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer2/MarginContainer/VBoxContainer"]
+layout_mode = 2
+text = "Select Input"
+horizontal_alignment = 1
+
+[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer2/MarginContainer/VBoxContainer"]
+layout_mode = 2
+
+[node name="SelectBoolButton" type="Button" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer2/MarginContainer/VBoxContainer/HBoxContainer"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(80, 0)
+layout_mode = 2
+size_flags_horizontal = 3
+toggle_mode = true
+text = "Boolean"
+
+[node name="Select1DButton" type="Button" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer2/MarginContainer/VBoxContainer/HBoxContainer"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(80, 0)
+layout_mode = 2
+size_flags_horizontal = 3
+toggle_mode = true
+text = "1D"
+
+[node name="Select2DButton" type="Button" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer2/MarginContainer/VBoxContainer/HBoxContainer"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(80, 0)
+layout_mode = 2
+size_flags_horizontal = 3
+toggle_mode = true
+text = "2D"
+
+[node name="Select3DButton" type="Button" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer2/MarginContainer/VBoxContainer/HBoxContainer"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(80, 0)
+layout_mode = 2
+size_flags_horizontal = 3
+toggle_mode = true
+text = "3D"
+
+[node name="NoneAvailable" type="Label" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer2/MarginContainer/VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 6
+size_flags_vertical = 6
+text = "No matching inputs available."
+
+[node name="SomeAvailable" type="ScrollContainer" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer2/MarginContainer/VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="AvailableTypes" type="VBoxContainer" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer2/MarginContainer/VBoxContainer/SomeAvailable"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="InputDetector" type="Node" parent="."]
+unique_name_in_owner = true
+script = ExtResource("3_c6q6r")
+
+[connection signal="close_requested" from="." to="." method="_on_close_requested"]
+[connection signal="pressed" from="MarginContainer/MarginContainer/HBoxContainer/MarginContainer/MarginContainer/VBoxContainer/HBoxContainer/DetectBoolButton" to="." method="_on_detect_bool_button_pressed"]
+[connection signal="pressed" from="MarginContainer/MarginContainer/HBoxContainer/MarginContainer/MarginContainer/VBoxContainer/HBoxContainer/Detect1DButton" to="." method="_on_detect_1d_button_pressed"]
+[connection signal="pressed" from="MarginContainer/MarginContainer/HBoxContainer/MarginContainer/MarginContainer/VBoxContainer/HBoxContainer/Detect2DButton" to="." method="_on_detect_2d_button_pressed"]
+[connection signal="pressed" from="MarginContainer/MarginContainer/HBoxContainer/MarginContainer/MarginContainer/VBoxContainer/HBoxContainer/Detect3DButton" to="." method="_on_detect_3d_button_pressed"]
+[connection signal="pressed" from="MarginContainer/MarginContainer/HBoxContainer/MarginContainer/MarginContainer/VBoxContainer/AcceptDetectionButton" to="." method="_on_accept_detection_button_pressed"]
+[connection signal="pressed" from="MarginContainer/MarginContainer/HBoxContainer/MarginContainer2/MarginContainer/VBoxContainer/HBoxContainer/SelectBoolButton" to="." method="_on_select_bool_button_pressed"]
+[connection signal="pressed" from="MarginContainer/MarginContainer/HBoxContainer/MarginContainer2/MarginContainer/VBoxContainer/HBoxContainer/Select1DButton" to="." method="_on_select_1d_button_pressed"]
+[connection signal="pressed" from="MarginContainer/MarginContainer/HBoxContainer/MarginContainer2/MarginContainer/VBoxContainer/HBoxContainer/Select2DButton" to="." method="_on_select_2d_button_pressed"]
+[connection signal="pressed" from="MarginContainer/MarginContainer/HBoxContainer/MarginContainer2/MarginContainer/VBoxContainer/HBoxContainer/Select3DButton" to="." method="_on_select_3d_button_pressed"]
+[connection signal="detection_started" from="InputDetector" to="." method="_on_input_detector_detection_started"]
+[connection signal="input_detected" from="InputDetector" to="." method="_on_input_detector_input_detected"]
diff --git a/addons/guide/editor/class_scanner.gd b/addons/guide/editor/class_scanner.gd
new file mode 100644
index 0000000..d37e3da
--- /dev/null
+++ b/addons/guide/editor/class_scanner.gd
@@ -0,0 +1,91 @@
+## Scanner to find inheriting classes. Used to detect inheritors of
+## modifiers and triggers. Ideally this would be built into the editor
+## but sometimes one has to hack their way around the limitations.
+## This only scans to the extent needed to drive the UI, it's not a general
+## purpose implementation.
+@tool
+
+const GUIDESet = preload("../guide_set.gd")
+
+var _dirty:bool = true
+
+# looks like we only get very limited access to the script's inheritance tree,
+# so we need to do a little caching ourselves
+var _script_lut:Dictionary = {}
+
+func _init():
+ EditorInterface.get_resource_filesystem().script_classes_updated.connect(_mark_dirty)
+
+
+func _mark_dirty():
+ _dirty = true
+
+## Returns all classes that directly or indirectly inherit from the
+## given class. Only works for scripts in the project, e.g. doesn't
+## scan the whole class_db. Key is class name, value is the Script instance
+func find_inheritors(clazz_name:StringName) -> Dictionary:
+ var result:Dictionary = {}
+
+ var root := EditorInterface.get_resource_filesystem().get_filesystem()
+
+ # rebuild the LUT when needed
+ if _dirty:
+ _script_lut.clear()
+ _scan(root)
+ _dirty = false
+
+
+ var open_set:GUIDESet = GUIDESet.new()
+ # a closed set just to avoid infinite loops, we'll never
+ # look at the same class more than once.
+ var closed_set:GUIDESet = GUIDESet.new()
+
+ open_set.add(clazz_name)
+
+ while not open_set.is_empty():
+ var next = open_set.pull()
+ closed_set.add(next)
+ if not _script_lut.has(next):
+ # we don't know this script, ignore, move on
+ continue
+
+ # now find all scripts that extend the one we
+ # are looking at
+ for item:ScriptInfo in _script_lut.values():
+ if item.extendz == next:
+ # put them into the result
+ result[item.clazz_name] = item.clazz_script
+ # and put their class in the open set
+ # unless we already looked at it.
+ if not closed_set.has(item.clazz_name):
+ open_set.add(item.clazz_name)
+
+ return result
+
+
+func _scan(folder:EditorFileSystemDirectory):
+ for i in folder.get_file_count():
+ var script_clazz = folder.get_file_script_class_name(i)
+ if script_clazz != "":
+ var info := _script_lut.get(script_clazz)
+ if info == null:
+ info = ScriptInfo.new()
+ info.clazz_name = script_clazz
+ info.clazz_script = ResourceLoader.load(folder.get_file_path(i))
+ _script_lut[script_clazz] = info
+
+ var script_extendz = folder.get_file_script_class_extends(i)
+ info.extendz = script_extendz
+
+ for i in folder.get_subdir_count():
+ _scan(folder.get_subdir(i))
+
+
+class ScriptInfo:
+ var clazz_name:StringName
+ var extendz:StringName
+ var clazz_script:Script
+
+ func _to_string() -> String:
+ return clazz_name + ":" + extendz
+
diff --git a/addons/guide/editor/input_display/input_display.gd b/addons/guide/editor/input_display/input_display.gd
new file mode 100644
index 0000000..eb21aef
--- /dev/null
+++ b/addons/guide/editor/input_display/input_display.gd
@@ -0,0 +1,39 @@
+@tool
+extends RichTextLabel
+signal clicked()
+
+var _formatter:GUIDEInputFormatter = GUIDEInputFormatter.new(64)
+
+var input:GUIDEInput:
+ set(value):
+ if value == input:
+ return
+
+ if is_instance_valid(input):
+ input.changed.disconnect(_refresh)
+
+ input = value
+
+ if is_instance_valid(input):
+ input.changed.connect(_refresh)
+
+ _refresh()
+
+func _refresh():
+ if not is_instance_valid(input):
+ parse_bbcode("[center][i][/i][/center]")
+ tooltip_text = ""
+ return
+
+ var text := await _formatter.input_as_richtext_async(input, false)
+ parse_bbcode("[center]" + text + "[/center]")
+ tooltip_text = _formatter.input_as_text(input)
+
+
+func _gui_input(event):
+ if event is InputEventMouseButton:
+ if event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
+ clicked.emit()
+
+
+
diff --git a/addons/guide/editor/input_display/input_display.tscn b/addons/guide/editor/input_display/input_display.tscn
new file mode 100644
index 0000000..4cd79a3
--- /dev/null
+++ b/addons/guide/editor/input_display/input_display.tscn
@@ -0,0 +1,18 @@
+[gd_scene load_steps=3 format=3 uid="uid://dsv7s6tfmnsrs"]
+
+[ext_resource type="Script" path="res://addons/guide/editor/input_display/input_display.gd" id="1_ne6sd"]
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_0bp65"]
+
+[node name="InputDisplay" type="RichTextLabel"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme_override_styles/normal = SubResource("StyleBoxEmpty_0bp65")
+bbcode_enabled = true
+fit_content = true
+script = ExtResource("1_ne6sd")
diff --git a/addons/guide/editor/input_mapping_editor/input_mapping_editor.gd b/addons/guide/editor/input_mapping_editor/input_mapping_editor.gd
new file mode 100644
index 0000000..e4c428c
--- /dev/null
+++ b/addons/guide/editor/input_mapping_editor/input_mapping_editor.gd
@@ -0,0 +1,299 @@
+@tool
+extends MarginContainer
+
+const ArrayEdit = preload("../array_edit/array_edit.gd")
+const ClassScanner = preload("../class_scanner.gd")
+const Utils = preload("../utils.gd")
+
+@export var modifier_slot_scene:PackedScene
+@export var trigger_slot_scene:PackedScene
+@export var binding_dialog_scene:PackedScene
+
+@onready var _edit_input_mapping_button:Button = %EditInputMappingButton
+@onready var _input_display = %InputDisplay
+@onready var _edit_input_button:Button = %EditInputButton
+@onready var _clear_input_button:Button = %ClearInputButton
+
+@onready var _modifiers:ArrayEdit = %Modifiers
+@onready var _add_modifier_popup:PopupMenu = %AddModifierPopup
+
+@onready var _triggers:ArrayEdit = %Triggers
+@onready var _add_trigger_popup:PopupMenu = %AddTriggerPopup
+
+var _plugin:EditorPlugin
+var _scanner:ClassScanner
+var _undo_redo:EditorUndoRedoManager
+
+var _mapping:GUIDEInputMapping
+
+func _ready():
+ _edit_input_button.icon = get_theme_icon("Edit", "EditorIcons")
+ _clear_input_button.icon = get_theme_icon("Remove", "EditorIcons")
+ _edit_input_mapping_button.icon = get_theme_icon("Tools", "EditorIcons")
+
+ _modifiers.add_requested.connect(_on_modifiers_add_requested)
+ _modifiers.delete_requested.connect(_on_modifier_delete_requested)
+ _modifiers.duplicate_requested.connect(_on_modifier_duplicate_requested)
+ _modifiers.move_requested.connect(_on_modifier_move_requested)
+ _modifiers.clear_requested.connect(_on_modifiers_clear_requested)
+ _modifiers.collapse_state_changed.connect(_on_modifiers_collapse_state_changed)
+
+ _triggers.add_requested.connect(_on_triggers_add_requested)
+ _triggers.delete_requested.connect(_on_trigger_delete_requested)
+ _triggers.duplicate_requested.connect(_on_trigger_duplicate_requested)
+ _triggers.move_requested.connect(_on_trigger_move_requested)
+ _triggers.clear_requested.connect(_on_triggers_clear_requested)
+ _triggers.collapse_state_changed.connect(_on_triggers_collapse_state_changed)
+
+
+func initialize(plugin:EditorPlugin, scanner:ClassScanner) -> void:
+ _plugin = plugin
+ _scanner = scanner
+ _undo_redo = plugin.get_undo_redo()
+ _input_display.clicked.connect(_on_input_display_clicked)
+
+
+func edit(mapping:GUIDEInputMapping) -> void:
+ assert(_mapping == null)
+ _mapping = mapping
+ _mapping.changed.connect(_update)
+ _update()
+
+
+func _update():
+ _modifiers.clear()
+ _triggers.clear()
+
+ _input_display.input = _mapping.input
+ for i in _mapping.modifiers.size():
+ var modifier_slot = modifier_slot_scene.instantiate()
+ _modifiers.add_item(modifier_slot)
+
+ modifier_slot.modifier = _mapping.modifiers[i]
+ modifier_slot.changed.connect(_on_modifier_changed.bind(i, modifier_slot))
+
+ for i in _mapping.triggers.size():
+ var trigger_slot = trigger_slot_scene.instantiate()
+ _triggers.add_item(trigger_slot)
+
+ trigger_slot.trigger = _mapping.triggers[i]
+ trigger_slot.changed.connect(_on_trigger_changed.bind(i, trigger_slot))
+
+ _modifiers.collapsed = _mapping.get_meta("_guide_modifiers_collapsed", false)
+ _triggers.collapsed = _mapping.get_meta("_guide_triggers_collapsed", false)
+
+
+func _on_modifiers_add_requested():
+ _fill_popup(_add_modifier_popup, "GUIDEModifier")
+ _add_modifier_popup.popup(Rect2(get_global_mouse_position(), Vector2.ZERO))
+
+
+func _on_triggers_add_requested():
+ _fill_popup(_add_trigger_popup, "GUIDETrigger")
+ _add_trigger_popup.popup(Rect2(get_global_mouse_position(), Vector2.ZERO))
+
+
+func _fill_popup(popup:PopupMenu, base_clazz:StringName):
+ popup.clear(true)
+
+ var inheritors := _scanner.find_inheritors(base_clazz)
+ for type in inheritors.keys():
+ var class_script:Script = inheritors[type]
+ var dummy = class_script.new()
+ popup.add_item(dummy._editor_name())
+ popup.set_item_tooltip(popup.item_count -1, dummy._editor_description())
+ popup.set_item_metadata(popup.item_count - 1, class_script)
+
+func _on_input_display_clicked():
+ if is_instance_valid(_mapping.input):
+ EditorInterface.edit_resource(_mapping.input)
+
+
+func _on_input_changed(input:GUIDEInput):
+ _undo_redo.create_action("Change input")
+
+ _undo_redo.add_do_property(_mapping, "input", input)
+ _undo_redo.add_undo_property(_mapping, "input", _mapping.input)
+
+ _undo_redo.commit_action()
+
+ if is_instance_valid(input):
+ EditorInterface.edit_resource(input)
+
+
+func _on_edit_input_button_pressed():
+ var dialog:Window = binding_dialog_scene.instantiate()
+ EditorInterface.popup_dialog_centered(dialog)
+ dialog.initialize(_scanner)
+ dialog.input_selected.connect(_on_input_changed)
+
+
+func _on_clear_input_button_pressed():
+ _undo_redo.create_action("Delete bound input")
+
+ _undo_redo.add_do_property(_mapping, "input", null)
+ _undo_redo.add_undo_property(_mapping, "triggers", _mapping.input)
+
+ _undo_redo.commit_action()
+
+
+func _on_add_modifier_popup_index_pressed(index:int) -> void:
+ var script = _add_modifier_popup.get_item_metadata(index)
+ var new_modifier = script.new()
+
+ _undo_redo.create_action("Add " + new_modifier._editor_name() + " modifier")
+ var modifiers = _mapping.modifiers.duplicate()
+ modifiers.append(new_modifier)
+
+ _undo_redo.add_do_property(_mapping, "modifiers", modifiers)
+ _undo_redo.add_undo_property(_mapping, "modifiers", _mapping.modifiers)
+
+ _undo_redo.commit_action()
+
+
+func _on_add_trigger_popup_index_pressed(index):
+ var script = _add_trigger_popup.get_item_metadata(index)
+ var new_trigger = script.new()
+
+ _undo_redo.create_action("Add " + new_trigger._editor_name() + " trigger")
+ var triggers = _mapping.triggers.duplicate()
+ triggers.append(new_trigger)
+
+ _undo_redo.add_do_property(_mapping, "triggers", triggers)
+ _undo_redo.add_undo_property(_mapping, "triggers", _mapping.triggers)
+
+ _undo_redo.commit_action()
+
+
+func _on_modifier_changed(index:int, slot) -> void:
+ var new_modifier = slot.modifier
+
+ _undo_redo.create_action("Replace modifier")
+ var modifiers = _mapping.modifiers.duplicate()
+ modifiers[index] = new_modifier
+
+ _undo_redo.add_do_property(_mapping, "modifiers", modifiers)
+ _undo_redo.add_undo_property(_mapping, "modifiers", _mapping.modifiers)
+
+ _undo_redo.commit_action()
+
+
+func _on_trigger_changed(index:int, slot) -> void:
+ var new_trigger = slot.trigger
+
+ _undo_redo.create_action("Replace trigger")
+ var triggers = _mapping.triggers.duplicate()
+ triggers[index] = new_trigger
+
+ _undo_redo.add_do_property(_mapping, "triggers", triggers)
+ _undo_redo.add_undo_property(_mapping, "triggers", _mapping.triggers)
+
+ _undo_redo.commit_action()
+
+
+func _on_modifier_move_requested(from:int, to:int) -> void:
+ _undo_redo.create_action("Move modifier")
+ var modifiers = _mapping.modifiers.duplicate()
+ var modifier = modifiers[from]
+ modifiers.remove_at(from)
+ if from < to:
+ to -= 1
+ modifiers.insert(to, modifier)
+
+ _undo_redo.add_do_property(_mapping, "modifiers", modifiers)
+ _undo_redo.add_undo_property(_mapping, "modifiers", _mapping.modifiers)
+
+ _undo_redo.commit_action()
+
+
+func _on_trigger_move_requested(from:int, to:int) -> void:
+ _undo_redo.create_action("Move trigger")
+ var triggers = _mapping.triggers.duplicate()
+ var trigger = triggers[from]
+ triggers.remove_at(from)
+ if from < to:
+ to -= 1
+ triggers.insert(to, trigger)
+
+ _undo_redo.add_do_property(_mapping, "triggers", triggers)
+ _undo_redo.add_undo_property(_mapping, "triggers", _mapping.triggers)
+
+ _undo_redo.commit_action()
+
+func _on_modifier_duplicate_requested(index:int) -> void:
+ _undo_redo.create_action("Duplicate modifier")
+ var modifiers = _mapping.modifiers.duplicate()
+ var copy = Utils.duplicate_if_inline(modifiers[index])
+ modifiers.insert(index+1, copy)
+
+ _undo_redo.add_do_property(_mapping, "modifiers", modifiers)
+ _undo_redo.add_undo_property(_mapping, "modifiers", _mapping.modifiers)
+
+ _undo_redo.commit_action()
+
+func _on_trigger_duplicate_requested(index:int) -> void:
+ _undo_redo.create_action("Duplicate trigger")
+ var triggers = _mapping.triggers.duplicate()
+ var copy = Utils.duplicate_if_inline(triggers[index])
+ triggers.insert(index+1, copy)
+
+ _undo_redo.add_do_property(_mapping, "triggers", triggers)
+ _undo_redo.add_undo_property(_mapping, "triggers", _mapping.triggers)
+
+ _undo_redo.commit_action()
+
+
+
+func _on_modifier_delete_requested(index:int) -> void:
+ _undo_redo.create_action("Delete modifier")
+ var modifiers = _mapping.modifiers.duplicate()
+ modifiers.remove_at(index)
+
+ _undo_redo.add_do_property(_mapping, "modifiers", modifiers)
+ _undo_redo.add_undo_property(_mapping, "modifiers", _mapping.modifiers)
+
+ _undo_redo.commit_action()
+
+
+func _on_trigger_delete_requested(index:int) -> void:
+ _undo_redo.create_action("Delete trigger")
+ var triggers = _mapping.triggers.duplicate()
+ triggers.remove_at(index)
+
+ _undo_redo.add_do_property(_mapping, "triggers", triggers)
+ _undo_redo.add_undo_property(_mapping, "triggers", _mapping.triggers)
+
+ _undo_redo.commit_action()
+
+
+func _on_modifiers_clear_requested() -> void:
+ _undo_redo.create_action("Clear modifiers")
+ # if this is inlined into the do_property, then it doesn't work
+ # so lets keep it a local variable
+ var value:Array[GUIDEModifier] = []
+ _undo_redo.add_do_property(_mapping, "modifiers", value)
+ _undo_redo.add_undo_property(_mapping, "modifiers", _mapping.modifiers)
+
+ _undo_redo.commit_action()
+
+
+func _on_triggers_clear_requested() -> void:
+ _undo_redo.create_action("Clear triggers")
+ # if this is inlined into the do_property, then it doesn't work
+ # so lets keep it a local variable
+ var value:Array[GUIDETrigger] = []
+ _undo_redo.add_do_property(_mapping, "triggers", value)
+ _undo_redo.add_undo_property(_mapping, "triggers", _mapping.triggers)
+
+ _undo_redo.commit_action()
+
+
+func _on_modifiers_collapse_state_changed(new_state:bool):
+ _mapping.set_meta("_guide_modifiers_collapsed", new_state)
+
+func _on_triggers_collapse_state_changed(new_state:bool):
+ _mapping.set_meta("_guide_triggers_collapsed", new_state)
+
+
+func _on_edit_input_mapping_button_pressed():
+ EditorInterface.edit_resource(_mapping)
diff --git a/addons/guide/editor/input_mapping_editor/input_mapping_editor.tscn b/addons/guide/editor/input_mapping_editor/input_mapping_editor.tscn
new file mode 100644
index 0000000..a70f142
--- /dev/null
+++ b/addons/guide/editor/input_mapping_editor/input_mapping_editor.tscn
@@ -0,0 +1,140 @@
+[gd_scene load_steps=9 format=3 uid="uid://c323mdijdhktg"]
+
+[ext_resource type="PackedScene" uid="uid://dsv7s6tfmnsrs" path="res://addons/guide/editor/input_display/input_display.tscn" id="1_pg8n3"]
+[ext_resource type="Script" path="res://addons/guide/editor/input_mapping_editor/input_mapping_editor.gd" id="1_xsluc"]
+[ext_resource type="PackedScene" uid="uid://ck5a30syo6bpo" path="res://addons/guide/editor/modifier_slot/modifier_slot.tscn" id="2_uhbrq"]
+[ext_resource type="PackedScene" uid="uid://tk30wnstb0ku" path="res://addons/guide/editor/trigger_slot/trigger_slot.tscn" id="3_e0jys"]
+[ext_resource type="PackedScene" uid="uid://dic27bm4pfw3q" path="res://addons/guide/editor/binding_dialog/binding_dialog.tscn" id="4_oepf3"]
+[ext_resource type="PackedScene" uid="uid://cly0ff32fvpb2" path="res://addons/guide/editor/array_edit/array_edit.tscn" id="6_jekhk"]
+
+[sub_resource type="Image" id="Image_m1w1j"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_y0eyy"]
+image = SubResource("Image_m1w1j")
+
+[node name="InputMappingEditor" type="MarginContainer"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_vertical = 0
+script = ExtResource("1_xsluc")
+modifier_slot_scene = ExtResource("2_uhbrq")
+trigger_slot_scene = ExtResource("3_e0jys")
+binding_dialog_scene = ExtResource("4_oepf3")
+
+[node name="HBoxContainer" type="HBoxContainer" parent="."]
+layout_mode = 2
+size_flags_vertical = 0
+theme_override_constants/separation = 8
+
+[node name="MarginContainer" type="MarginContainer" parent="HBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 0
+
+[node name="Panel" type="Panel" parent="HBoxContainer/MarginContainer"]
+visible = false
+layout_mode = 2
+
+[node name="EditInputMappingButton" type="Button" parent="HBoxContainer/MarginContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Open input mapping in inspector"
+icon = SubResource("ImageTexture_y0eyy")
+flat = true
+
+[node name="MarginContainer1" type="MarginContainer" parent="HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="Panel" type="Panel" parent="HBoxContainer/MarginContainer1"]
+visible = false
+layout_mode = 2
+
+[node name="HBoxContainer" type="HBoxContainer" parent="HBoxContainer/MarginContainer1"]
+layout_mode = 2
+
+[node name="InputDisplay" parent="HBoxContainer/MarginContainer1/HBoxContainer" instance=ExtResource("1_pg8n3")]
+unique_name_in_owner = true
+layout_mode = 2
+scroll_active = false
+
+[node name="EditInputButton" type="Button" parent="HBoxContainer/MarginContainer1/HBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 0
+tooltip_text = "Edit bound input..."
+icon = SubResource("ImageTexture_y0eyy")
+flat = true
+
+[node name="ClearInputButton" type="Button" parent="HBoxContainer/MarginContainer1/HBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 0
+tooltip_text = "Delete bound input"
+icon = SubResource("ImageTexture_y0eyy")
+flat = true
+
+[node name="MarginContainer2" type="MarginContainer" parent="HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_stretch_ratio = 2.0
+
+[node name="Panel" type="Panel" parent="HBoxContainer/MarginContainer2"]
+visible = false
+layout_mode = 2
+
+[node name="VBoxContainer" type="VBoxContainer" parent="HBoxContainer/MarginContainer2"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 0
+size_flags_stretch_ratio = 2.0
+
+[node name="Modifiers" parent="HBoxContainer/MarginContainer2/VBoxContainer" instance=ExtResource("6_jekhk")]
+unique_name_in_owner = true
+layout_mode = 2
+title = "Modifiers"
+add_tooltip = "Add modifier..."
+clear_tooltip = "Clear modifiers"
+
+[node name="AddModifierPopup" type="PopupMenu" parent="HBoxContainer/MarginContainer2/VBoxContainer"]
+unique_name_in_owner = true
+
+[node name="MarginContainer3" type="MarginContainer" parent="HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_stretch_ratio = 2.0
+
+[node name="Panel" type="Panel" parent="HBoxContainer/MarginContainer3"]
+visible = false
+layout_mode = 2
+
+[node name="VBoxContainer2" type="VBoxContainer" parent="HBoxContainer/MarginContainer3"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 0
+size_flags_stretch_ratio = 2.0
+
+[node name="Triggers" parent="HBoxContainer/MarginContainer3/VBoxContainer2" instance=ExtResource("6_jekhk")]
+unique_name_in_owner = true
+layout_mode = 2
+title = "Triggers"
+add_tooltip = "Add trigger..."
+clear_tooltip = "Clear triggers"
+
+[node name="AddTriggerPopup" type="PopupMenu" parent="HBoxContainer/MarginContainer3/VBoxContainer2"]
+unique_name_in_owner = true
+
+[connection signal="pressed" from="HBoxContainer/MarginContainer/EditInputMappingButton" to="." method="_on_edit_input_mapping_button_pressed"]
+[connection signal="pressed" from="HBoxContainer/MarginContainer1/HBoxContainer/EditInputButton" to="." method="_on_edit_input_button_pressed"]
+[connection signal="pressed" from="HBoxContainer/MarginContainer1/HBoxContainer/ClearInputButton" to="." method="_on_clear_input_button_pressed"]
+[connection signal="index_pressed" from="HBoxContainer/MarginContainer2/VBoxContainer/AddModifierPopup" to="." method="_on_add_modifier_popup_index_pressed"]
+[connection signal="index_pressed" from="HBoxContainer/MarginContainer3/VBoxContainer2/AddTriggerPopup" to="." method="_on_add_trigger_popup_index_pressed"]
diff --git a/addons/guide/editor/logo_editor_small.svg b/addons/guide/editor/logo_editor_small.svg
new file mode 100644
index 0000000..38b6805
--- /dev/null
+++ b/addons/guide/editor/logo_editor_small.svg
@@ -0,0 +1,24 @@
+
+
+
diff --git a/addons/guide/editor/logo_editor_small.svg.import b/addons/guide/editor/logo_editor_small.svg.import
new file mode 100644
index 0000000..50a40ea
--- /dev/null
+++ b/addons/guide/editor/logo_editor_small.svg.import
@@ -0,0 +1,38 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cap7e0f05pj8j"
+path="res://.godot/imported/logo_editor_small.svg-a18f1eaff840dcdf5215ef26c289caf9.ctex"
+metadata={
+"has_editor_variant": true,
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/editor/logo_editor_small.svg"
+dest_files=["res://.godot/imported/logo_editor_small.svg-a18f1eaff840dcdf5215ef26c289caf9.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=0.5
+editor/scale_with_editor_scale=true
+editor/convert_colors_with_editor_theme=false
diff --git a/addons/guide/editor/mapping_context_editor/mapping_context_editor.gd b/addons/guide/editor/mapping_context_editor/mapping_context_editor.gd
new file mode 100644
index 0000000..8f7882c
--- /dev/null
+++ b/addons/guide/editor/mapping_context_editor/mapping_context_editor.gd
@@ -0,0 +1,159 @@
+@tool
+extends MarginContainer
+
+const ClassScanner = preload("../class_scanner.gd")
+const Utils = preload("../utils.gd")
+const ArrayEdit = preload("../array_edit/array_edit.gd")
+
+@export var action_mapping_editor_scene:PackedScene
+
+@onready var _title_label:Label = %TitleLabel
+@onready var _action_mappings:ArrayEdit = %ActionMappings
+@onready var _editing_view:Control = %EditingView
+@onready var _empty_view = %EmptyView
+
+var _plugin:EditorPlugin
+var _current_context:GUIDEMappingContext
+var _undo_redo:EditorUndoRedoManager
+var _scanner:ClassScanner
+
+
+func _ready():
+ _title_label.add_theme_font_override("font", get_theme_font("title", "EditorFonts"))
+ _scanner = ClassScanner.new()
+
+ _editing_view.visible = false
+ _empty_view.visible = true
+
+ _action_mappings.add_requested.connect(_on_action_mappings_add_requested)
+ _action_mappings.move_requested.connect(_on_action_mappings_move_requested)
+ _action_mappings.delete_requested.connect(_on_action_mapping_delete_requested)
+ _action_mappings.clear_requested.connect(_on_action_mappings_clear_requested)
+ _action_mappings.duplicate_requested.connect(_on_action_mapping_duplicate_requested)
+ _action_mappings.collapse_state_changed.connect(_on_action_mappings_collapse_state_changed)
+
+func initialize(plugin:EditorPlugin) -> void:
+ _plugin = plugin
+ _undo_redo = plugin.get_undo_redo()
+
+
+func edit(context:GUIDEMappingContext) -> void:
+ if is_instance_valid(_current_context):
+ _current_context.changed.disconnect(_refresh)
+
+ _current_context = context
+
+ if is_instance_valid(_current_context):
+ _current_context.changed.connect(_refresh)
+
+ _refresh()
+
+
+func _refresh():
+ _editing_view.visible = is_instance_valid(_current_context)
+ _empty_view.visible = not is_instance_valid(_current_context)
+
+ if not is_instance_valid(_current_context):
+ return
+
+ _title_label.text = _current_context._editor_name()
+ _title_label.tooltip_text = _current_context.resource_path
+
+ _action_mappings.clear()
+
+ for i in _current_context.mappings.size():
+ var mapping = _current_context.mappings[i]
+
+ var mapping_editor = action_mapping_editor_scene.instantiate()
+ mapping_editor.initialize(_plugin, _scanner)
+
+ _action_mappings.add_item(mapping_editor)
+
+ mapping_editor.edit(mapping)
+
+ _action_mappings.collapsed = _current_context.get_meta("_guide_action_mappings_collapsed", false)
+
+func _on_action_mappings_add_requested():
+ var mappings = _current_context.mappings.duplicate()
+ var new_mapping := GUIDEActionMapping.new()
+ # don't set an action because they should come from the file system
+ mappings.append(new_mapping)
+
+ _undo_redo.create_action("Add action mapping")
+
+ _undo_redo.add_do_property(_current_context, "mappings", mappings)
+ _undo_redo.add_undo_property(_current_context, "mappings", _current_context.mappings)
+
+ _undo_redo.commit_action()
+
+
+func _on_action_mappings_move_requested(from:int, to:int):
+ var mappings = _current_context.mappings.duplicate()
+ var mapping = mappings[from]
+ mappings.remove_at(from)
+ if from < to:
+ to -= 1
+ mappings.insert(to, mapping)
+
+ _undo_redo.create_action("Move action mapping")
+
+ _undo_redo.add_do_property(_current_context, "mappings", mappings)
+ _undo_redo.add_undo_property(_current_context, "mappings", _current_context.mappings)
+
+ _undo_redo.commit_action()
+
+
+func _on_action_mapping_delete_requested(index:int):
+ var mappings = _current_context.mappings.duplicate()
+ mappings.remove_at(index)
+
+ _undo_redo.create_action("Delete action mapping")
+
+ _undo_redo.add_do_property(_current_context, "mappings", mappings)
+ _undo_redo.add_undo_property(_current_context, "mappings", _current_context.mappings)
+
+ _undo_redo.commit_action()
+
+
+func _on_action_mappings_clear_requested():
+ var mappings:Array[GUIDEActionMapping] = []
+
+ _undo_redo.create_action("Clear action mappings")
+
+ _undo_redo.add_do_property(_current_context, "mappings", mappings)
+ _undo_redo.add_undo_property(_current_context, "mappings", _current_context.mappings)
+
+ _undo_redo.commit_action()
+
+func _on_action_mapping_duplicate_requested(index:int):
+ var mappings = _current_context.mappings.duplicate()
+ var to_duplicate:GUIDEActionMapping = mappings[index]
+
+ var copy = GUIDEActionMapping.new()
+ # don't set the action, because each mapping should have a unique mapping
+ for input_mapping:GUIDEInputMapping in to_duplicate.input_mappings:
+ var copied_input_mapping := GUIDEInputMapping.new()
+ copied_input_mapping.input = Utils.duplicate_if_inline(input_mapping.input)
+ for modifier in input_mapping.modifiers:
+ copied_input_mapping.modifiers.append(Utils.duplicate_if_inline(modifier))
+
+ for trigger in input_mapping.triggers:
+ copied_input_mapping.triggers.append(Utils.duplicate_if_inline(trigger))
+
+ copy.input_mappings.append(copied_input_mapping)
+
+ # insert the copy after the copied mapping
+ mappings.insert(index+1, copy)
+
+
+ _undo_redo.create_action("Duplicate action mapping")
+
+ _undo_redo.add_do_property(_current_context, "mappings", mappings)
+ _undo_redo.add_undo_property(_current_context, "mappings", _current_context.mappings)
+
+ _undo_redo.commit_action()
+
+func _on_action_mappings_collapse_state_changed(new_state:bool):
+ _current_context.set_meta("_guide_action_mappings_collapsed", new_state)
+
+
diff --git a/addons/guide/editor/mapping_context_editor/mapping_context_editor.tscn b/addons/guide/editor/mapping_context_editor/mapping_context_editor.tscn
new file mode 100644
index 0000000..f28a17f
--- /dev/null
+++ b/addons/guide/editor/mapping_context_editor/mapping_context_editor.tscn
@@ -0,0 +1,58 @@
+[gd_scene load_steps=4 format=3 uid="uid://dm3hott3tfvwe"]
+
+[ext_resource type="Script" path="res://addons/guide/editor/mapping_context_editor/mapping_context_editor.gd" id="1_vytdu"]
+[ext_resource type="PackedScene" uid="uid://361aipcef24h" path="res://addons/guide/editor/action_mapping_editor/action_mapping_editor.tscn" id="2_qb3p8"]
+[ext_resource type="PackedScene" uid="uid://cly0ff32fvpb2" path="res://addons/guide/editor/array_edit/array_edit.tscn" id="3_x7h5x"]
+
+[node name="MappingContextEditor" type="MarginContainer"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme_override_constants/margin_left = 5
+theme_override_constants/margin_top = 5
+theme_override_constants/margin_right = 5
+theme_override_constants/margin_bottom = 5
+script = ExtResource("1_vytdu")
+action_mapping_editor_scene = ExtResource("2_qb3p8")
+
+[node name="EditingView" type="VBoxContainer" parent="."]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="HBoxContainer" type="HBoxContainer" parent="EditingView"]
+layout_mode = 2
+
+[node name="TitleLabel" type="Label" parent="EditingView/HBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 6
+text = "narf.tres"
+horizontal_alignment = 1
+
+[node name="MarginContainer" type="MarginContainer" parent="EditingView"]
+layout_mode = 2
+theme_override_constants/margin_bottom = 5
+
+[node name="ScrollContainer" type="ScrollContainer" parent="EditingView"]
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="ActionMappings" parent="EditingView/ScrollContainer" instance=ExtResource("3_x7h5x")]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+title = "Action mappings"
+add_tooltip = "Add action mapping"
+clear_tooltip = "Clear action mappings"
+
+[node name="EmptyView" type="CenterContainer" parent="."]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="Label" type="Label" parent="EmptyView"]
+layout_mode = 2
+text = "Create and open a GUIDEMappingContext to get started."
diff --git a/addons/guide/editor/modifier_slot/modifier_slot.gd b/addons/guide/editor/modifier_slot/modifier_slot.gd
new file mode 100644
index 0000000..30dc53d
--- /dev/null
+++ b/addons/guide/editor/modifier_slot/modifier_slot.gd
@@ -0,0 +1,14 @@
+@tool
+extends "../resource_slot/resource_slot.gd"
+
+var modifier:GUIDEModifier:
+ set(value):
+ _value = value
+ get:
+ return _value
+
+func _accepts_drop_data(data:Resource) -> bool:
+ return data is GUIDEModifier
+
+
+
diff --git a/addons/guide/editor/modifier_slot/modifier_slot.tscn b/addons/guide/editor/modifier_slot/modifier_slot.tscn
new file mode 100644
index 0000000..6b2f91e
--- /dev/null
+++ b/addons/guide/editor/modifier_slot/modifier_slot.tscn
@@ -0,0 +1,18 @@
+[gd_scene load_steps=2 format=3 uid="uid://ck5a30syo6bpo"]
+
+[ext_resource type="Script" path="res://addons/guide/editor/modifier_slot/modifier_slot.gd" id="1_273m5"]
+
+[node name="LineEdit" type="LineEdit"]
+offset_right = 1920.0
+offset_bottom = 31.0
+size_flags_horizontal = 3
+size_flags_vertical = 0
+text = "Name"
+editable = false
+context_menu_enabled = false
+virtual_keyboard_enabled = false
+shortcut_keys_enabled = false
+middle_mouse_paste_enabled = false
+selecting_enabled = false
+drag_and_drop_selection_enabled = false
+script = ExtResource("1_273m5")
diff --git a/addons/guide/editor/resource_slot/resource_slot.gd b/addons/guide/editor/resource_slot/resource_slot.gd
new file mode 100644
index 0000000..d54ec95
--- /dev/null
+++ b/addons/guide/editor/resource_slot/resource_slot.gd
@@ -0,0 +1,106 @@
+@tool
+extends LineEdit
+
+signal changed()
+const Utils = preload("../utils.gd")
+
+func _ready():
+ editable = false
+ context_menu_enabled = false
+ virtual_keyboard_enabled = false
+ shortcut_keys_enabled = false
+ selecting_enabled = false
+ drag_and_drop_selection_enabled = false
+ middle_mouse_paste_enabled = false
+
+## The underlying resource. This is opened for editing when the user clicks on the control. Its also
+## used when dragging from the control.
+var _value:Resource = null:
+ set(value):
+ if _value == value:
+ return
+
+ # stop tracking changes to the old resource (if any)
+ if is_instance_valid(_value):
+ _value.changed.disconnect(_update_from_value)
+
+ _value = value
+
+ # track changes to the resource itself
+ if is_instance_valid(_value):
+ _value.changed.connect(_update_from_value)
+
+ _update_from_value()
+ changed.emit()
+
+func _update_from_value():
+ if not is_instance_valid(_value):
+ text = ""
+ tooltip_text = ""
+ remove_theme_color_override("font_uneditable_color")
+ else:
+ text = _value._editor_name()
+ tooltip_text = _value.resource_path
+ # if the value is shared, we override the font color to indicate that
+ if not Utils.is_inline(_value):
+ add_theme_color_override("font_uneditable_color", get_theme_color("accent_color", "Editor"))
+ queue_redraw()
+ else:
+ remove_theme_color_override("font_uneditable_color")
+
+## Can be overridden to handle the drop data. This method is called when the user drops something on the control.
+## If the value should be updated ,this method should set the _value property.
+func _do_drop_data(data:Resource):
+ _value = data
+
+
+## Whether this control can accept drop data. This method is called when the user drags something over the control.
+func _accepts_drop_data(data:Resource) -> bool:
+ return false
+
+func _can_drop_data(at_position, data) -> bool:
+ if data is Resource:
+ return _accepts_drop_data(data)
+
+ if not data is Dictionary:
+ return false
+
+ if data.has("files"):
+ for file in data["files"]:
+ if _accepts_drop_data(ResourceLoader.load(file)):
+ return true
+
+ return false
+
+
+func _drop_data(at_position, data) -> void:
+ if data is Resource:
+ _do_drop_data(data)
+ return
+
+ for file in data["files"]:
+ var item := ResourceLoader.load(file)
+ _do_drop_data(item)
+
+
+func _get_drag_data(at_position: Vector2) -> Variant:
+ if is_instance_valid(_value):
+ var _preview := TextureRect.new()
+ _preview.texture = get_theme_icon("File", "EditorIcons")
+ set_drag_preview(_preview)
+ # if the value is shared, we just hand out the resource path
+ if not Utils.is_inline(_value):
+ return {"files": [_value.resource_path]}
+ else:
+ # otherwise we hand out a shallow copy
+ return _value.duplicate()
+ else:
+ return null
+
+func _gui_input(event):
+ if event is InputEventMouseButton:
+ if event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
+ if is_instance_valid(_value):
+ EditorInterface.edit_resource(_value)
+
+
diff --git a/addons/guide/editor/trigger_slot/trigger_slot.gd b/addons/guide/editor/trigger_slot/trigger_slot.gd
new file mode 100644
index 0000000..70cb053
--- /dev/null
+++ b/addons/guide/editor/trigger_slot/trigger_slot.gd
@@ -0,0 +1,14 @@
+@tool
+extends "../resource_slot/resource_slot.gd"
+
+var trigger:GUIDETrigger:
+ set(value):
+ _value = value
+ get:
+ return _value
+
+func _accepts_drop_data(data:Resource) -> bool:
+ return data is GUIDETrigger
+
+
+
diff --git a/addons/guide/editor/trigger_slot/trigger_slot.tscn b/addons/guide/editor/trigger_slot/trigger_slot.tscn
new file mode 100644
index 0000000..3d5e6d8
--- /dev/null
+++ b/addons/guide/editor/trigger_slot/trigger_slot.tscn
@@ -0,0 +1,20 @@
+[gd_scene load_steps=2 format=3 uid="uid://tk30wnstb0ku"]
+
+[ext_resource type="Script" path="res://addons/guide/editor/trigger_slot/trigger_slot.gd" id="1_wxafc"]
+
+[node name="LineEdit" type="LineEdit"]
+unique_name_in_owner = true
+offset_right = 1920.0
+offset_bottom = 31.0
+size_flags_horizontal = 3
+size_flags_vertical = 0
+tooltip_text = "Delete trigger"
+text = "Name"
+editable = false
+context_menu_enabled = false
+virtual_keyboard_enabled = false
+shortcut_keys_enabled = false
+middle_mouse_paste_enabled = false
+selecting_enabled = false
+drag_and_drop_selection_enabled = false
+script = ExtResource("1_wxafc")
diff --git a/addons/guide/editor/utils.gd b/addons/guide/editor/utils.gd
new file mode 100644
index 0000000..f3549af
--- /dev/null
+++ b/addons/guide/editor/utils.gd
@@ -0,0 +1,22 @@
+## Removes and frees all children of a node.
+static func clear(node:Node):
+ if not is_instance_valid(node):
+ return
+ for child in node.get_children():
+ node.remove_child(child)
+ child.queue_free()
+
+
+## Checks if the given resource is an inline resource. If so, returns a shallow copy,
+## otherwise returns the resource. If the resource is null, returns null.
+static func duplicate_if_inline(resource:Resource) -> Resource:
+ if is_inline(resource):
+ return resource.duplicate()
+ return resource
+
+
+## Checks if the given resource is an inline resource.
+static func is_inline(resource:Resource) -> bool:
+ if resource == null:
+ return false
+ return resource.resource_path.contains("::") or resource.resource_path == ""
\ No newline at end of file
diff --git a/addons/guide/guide.gd b/addons/guide/guide.gd
new file mode 100644
index 0000000..1f8f19f
--- /dev/null
+++ b/addons/guide/guide.gd
@@ -0,0 +1,365 @@
+extends Node
+
+const GUIDESet = preload("guide_set.gd")
+const GUIDEReset = preload("guide_reset.gd")
+const GUIDEInputTracker = preload("guide_input_tracker.gd")
+
+## This is emitted whenever input mappings change (either due to mapping
+## contexts being enabled/disabled or remapping configs being re-applied or
+## joystick devices being connected/disconnected).
+## This is useful for updating UI prompts.
+signal input_mappings_changed()
+
+## The currently active contexts. Key is the context, value is the priority
+var _active_contexts:Dictionary = {}
+## The currently active action mappings.
+var _active_action_mappings:Array[GUIDEActionMapping] = []
+
+## The currently active remapping config.
+var _active_remapping_config:GUIDERemappingConfig
+
+## All currently active inputs as collected from the active input mappings
+var _active_inputs:Array[GUIDEInput] = []
+
+## A dictionary of actions sharing input. Key is the action, value
+## is an array of lower-priority actions that share input with the
+## key action.
+var _actions_sharing_input:Dictionary = {}
+
+## A reference to the reset node which resets inputs that need a reset per frame
+## This is an extra node because the reset should run at the end of the frame
+## before new input is processed at the beginning of the frame.
+var _reset_node:GUIDEReset
+
+
+func _ready():
+ process_mode = Node.PROCESS_MODE_ALWAYS
+ _reset_node = GUIDEReset.new()
+ add_child(_reset_node)
+ # attach to the current viewport to get input events
+ GUIDEInputTracker._instrument.call_deferred(get_viewport())
+
+ get_tree().node_added.connect(_on_node_added)
+
+ # Emit a change of input mappings whenever a joystick was connected
+ # or disconnected.
+ Input.joy_connection_changed.connect(func(ig, ig2): input_mappings_changed.emit())
+
+
+## Called when a node is added to the tree. If the node is a window
+## GUIDE will instrument it to get events when the window is focused.
+func _on_node_added(node:Node) -> void:
+ if not node is Window:
+ return
+
+ GUIDEInputTracker._instrument(node)
+
+
+## Injects input into GUIDE. GUIDE will call this automatically but
+## can also be used to manually inject input for GUIDE to handle
+func inject_input(event:InputEvent) -> void:
+ if event is InputEventAction:
+ return # we don't react to Godot's built-in events
+
+ for input:GUIDEInput in _active_inputs:
+ input._input(event)
+
+
+## Applies an input remapping config. This will override all input bindings in the
+## currently loaded mapping contexts with the bindings from the configuration.
+## Note that GUIDE will not track changes to the remapping config. If your remapping
+## config changes, you will need to call this method again.
+func set_remapping_config(config:GUIDERemappingConfig) -> void:
+ _active_remapping_config = config
+ _update_caches()
+
+
+## Enables the given context with the given priority. Lower numbers have higher priority. If
+## disable_others is set to true, all other currently enabled mapping contexts will be disabled.
+func enable_mapping_context(context:GUIDEMappingContext, disable_others:bool = false, priority:int = 0):
+ if not is_instance_valid(context):
+ push_error("Null context given. Ignoring.")
+ return
+
+ if disable_others:
+ _active_contexts.clear()
+
+ _active_contexts[context] = priority
+ _update_caches()
+
+
+## Disables the given mapping context.
+func disable_mapping_context(context:GUIDEMappingContext):
+ if not is_instance_valid(context):
+ push_error("Null context given. Ignoring.")
+ return
+
+ _active_contexts.erase(context)
+ _update_caches()
+
+
+## Checks whether the given mapping context is currently enabled.
+func is_mapping_context_enabled(context:GUIDEMappingContext) -> bool:
+ return _active_contexts.has(context)
+
+
+## Returns the currently enabled mapping contexts
+func get_enabled_mapping_contexts() -> Array[GUIDEMappingContext]:
+ var result:Array[GUIDEMappingContext] = []
+ for key in _active_contexts.keys():
+ result.append(key)
+ return result
+
+
+## Processes all currently active actions
+func _process(delta:float) -> void:
+ var blocked_actions:GUIDESet = GUIDESet.new()
+
+ for action_mapping:GUIDEActionMapping in _active_action_mappings:
+
+ var action:GUIDEAction = action_mapping.action
+
+ # Walk over all input mappings for this action and consolidate state
+ # and result value.
+ var consolidated_value:Vector3 = Vector3.ZERO
+ var consolidated_trigger_state:GUIDETrigger.GUIDETriggerState
+
+ for input_mapping:GUIDEInputMapping in action_mapping.input_mappings:
+ input_mapping._update_state(delta, action.action_value_type)
+ consolidated_value += input_mapping._value
+ consolidated_trigger_state = max(consolidated_trigger_state, input_mapping._state)
+
+ # we do the blocking check only here because triggers may need to run anyways
+ # (e.g. to collect hold times).
+ if blocked_actions.has(action):
+ consolidated_trigger_state = GUIDETrigger.GUIDETriggerState.NONE
+
+ if action.block_lower_priority_actions and \
+ consolidated_trigger_state == GUIDETrigger.GUIDETriggerState.TRIGGERED and \
+ _actions_sharing_input.has(action):
+ for blocked_action in _actions_sharing_input[action]:
+ blocked_actions.add(blocked_action)
+
+
+ # Now state change events.
+ match(action._last_state):
+ GUIDEAction.GUIDEActionState.TRIGGERED:
+ match(consolidated_trigger_state):
+ GUIDETrigger.GUIDETriggerState.NONE:
+ action._completed(consolidated_value)
+ GUIDETrigger.GUIDETriggerState.ONGOING:
+ action._ongoing(consolidated_value, delta)
+ GUIDETrigger.GUIDETriggerState.TRIGGERED:
+ action._triggered(consolidated_value, delta)
+
+ GUIDEAction.GUIDEActionState.ONGOING:
+ match(consolidated_trigger_state):
+ GUIDETrigger.GUIDETriggerState.NONE:
+ action._cancelled(consolidated_value)
+ GUIDETrigger.GUIDETriggerState.ONGOING:
+ action._ongoing(consolidated_value, delta)
+ GUIDETrigger.GUIDETriggerState.TRIGGERED:
+ action._triggered(consolidated_value, delta)
+
+ GUIDEAction.GUIDEActionState.COMPLETED:
+ match(consolidated_trigger_state):
+ GUIDETrigger.GUIDETriggerState.NONE:
+ # make sure the value updated but don't emit any other events
+ action._update_value(consolidated_value)
+ GUIDETrigger.GUIDETriggerState.ONGOING:
+ action._started(consolidated_value)
+ GUIDETrigger.GUIDETriggerState.TRIGGERED:
+ action._triggered(consolidated_value, delta)
+
+func _update_caches():
+ # Notify existing inputs that they aren no longer required
+ for input:GUIDEInput in _active_inputs:
+ input._reset()
+ input._end_usage()
+
+ # Cancel all actions, so they don't remain in weird states.
+ for mapping:GUIDEActionMapping in _active_action_mappings:
+ match mapping.action._last_state:
+ GUIDEAction.GUIDEActionState.ONGOING:
+ mapping.action._cancelled(Vector3.ZERO)
+ GUIDEAction.GUIDEActionState.TRIGGERED:
+ mapping.action._completed(Vector3.ZERO)
+ # notify all modifiers they are no longer in use
+ for input_mapping in mapping.input_mappings:
+ for modifier in input_mapping.modifiers:
+ modifier._end_usage()
+
+ _active_inputs.clear()
+ _active_action_mappings.clear()
+ _actions_sharing_input.clear()
+
+ var sorted_contexts:Array[Dictionary] = []
+
+ for context:GUIDEMappingContext in _active_contexts.keys():
+ sorted_contexts.append({"context": context, "priority": _active_contexts[context]})
+
+ sorted_contexts.sort_custom( func(a,b): return a.priority < b.priority )
+
+ # The actions we already have processed. Same action may appear in different
+ # contexts, so if we find the same action twice, only the first instance wins.
+ var processed_actions:GUIDESet = GUIDESet.new()
+ var consolidated_inputs:GUIDESet = GUIDESet.new()
+
+ for entry:Dictionary in sorted_contexts:
+ var context:GUIDEMappingContext = entry.context
+ for action_mapping:GUIDEActionMapping in context.mappings:
+ var action := action_mapping.action
+ # If the action was already configured in a higher priority context,
+ # we'll skip it.
+ if processed_actions.has(action):
+ # skip
+ continue
+
+ processed_actions.add(action)
+
+ # We consolidate the inputs here, so we'll internally build a new
+ # action mapping that uses consolidated inputs rather than the
+ # original ones. This achieves multiple things:
+ # - if two actions check for the same input, we only need to
+ # process the input once instead of twice.
+ # - it allows us to prioritize input, if two actions check for
+ # the same input. This way the first action can consume the
+ # input and not have it affect further actions.
+ # - we make sure nobody shares triggers as they are stateful and
+ # should not be shared.
+
+ var effective_mapping = GUIDEActionMapping.new()
+ effective_mapping.action = action
+
+ # now update the input mappings
+ for index in action_mapping.input_mappings.size():
+ var bound_input:GUIDEInput = action_mapping.input_mappings[index].input
+
+ # if the mapping has an override for the input, apply it.
+ if _active_remapping_config != null and \
+ _active_remapping_config._has(context, action, index):
+ bound_input = _active_remapping_config._get_bound_input_or_null(context, action, index)
+
+ # make a new input mapping
+ var new_input_mapping := GUIDEInputMapping.new()
+
+ # can be null for combo mappings, so check that
+ if bound_input != null:
+ # check if we already have this kind of input
+ var existing = consolidated_inputs.first_match(func(it:GUIDEInput): return it.is_same_as(bound_input))
+ if existing != null:
+ # if we have this already, use the instance we have
+ bound_input = existing
+ else:
+ # otherwise register this input into the consolidated input
+ consolidated_inputs.add(bound_input)
+
+ new_input_mapping.input = bound_input
+ # modifiers cannot be re-bound so we can just use the one
+ # from the original configuration. this is also needed for shared
+ # modifiers to work.
+ new_input_mapping.modifiers = action_mapping.input_mappings[index].modifiers
+ # triggers also cannot be re-bound but we still make a copy
+ # to ensure that no shared triggers exist.
+ new_input_mapping.triggers = []
+
+ for trigger in action_mapping.input_mappings[index].triggers:
+ new_input_mapping.triggers.append(trigger.duplicate())
+
+ new_input_mapping._initialize()
+
+ # and add it to the new mapping
+ effective_mapping.input_mappings.append(new_input_mapping)
+
+
+ # if any binding remains, add the mapping to the list of active
+ # action mappings
+ if not effective_mapping.input_mappings.is_empty():
+ _active_action_mappings.append(effective_mapping)
+
+ # now we have a new set of active inputs
+ for input:GUIDEInput in consolidated_inputs.values():
+ _active_inputs.append(input)
+
+ # prepare the action input share lookup table
+ for i:int in _active_action_mappings.size():
+
+ var mapping = _active_action_mappings[i]
+
+ if mapping.action.block_lower_priority_actions:
+ # first find out if the action uses any chorded actions and
+ # collect all inputs that this action uses
+ var chorded_actions:GUIDESet = GUIDESet.new()
+ var inputs:GUIDESet = GUIDESet.new()
+ var blocked_actions:GUIDESet = GUIDESet.new()
+ for input_mapping:GUIDEInputMapping in mapping.input_mappings:
+ if input_mapping.input != null:
+ inputs.add(input_mapping.input)
+
+ for trigger:GUIDETrigger in input_mapping.triggers:
+ if trigger is GUIDETriggerChordedAction and trigger.action != null:
+ chorded_actions.add(trigger.action)
+
+ # Now the action that has a chorded action (A) needs to make sure that
+ # the chorded action it depends upon (B) is not blocked (otherwise A would
+ # never trigger) and if that chorded action (B) in turn depends on chorded actions. So
+ # if chorded actions build a chain, we need to keep the full
+ # chain unblocked. In addition we need to add the inputs of all
+ # these chorded actions to the list of blocked inputs.
+ for j:int in range(i+1, _active_action_mappings.size()):
+ var inner_mapping = _active_action_mappings[j]
+ # this is a chorded action that is used by one other action
+ # in the chain.
+ if chorded_actions.has(inner_mapping.action):
+ for input_mapping:GUIDEInputMapping in inner_mapping.input_mappings:
+ # put all of its inputs into the list of blocked inputs
+ if input_mapping.input != null:
+ inputs.add(input_mapping.input)
+
+ # also if this mapping in turn again depends on a chorded
+ # action, ad this one to the list of chorded actions
+ for trigger:GUIDETrigger in input_mapping.triggers:
+ if trigger is GUIDETriggerChordedAction and trigger.action != null:
+ chorded_actions.add(trigger.action)
+
+ # now find lower priority actions that share input
+ for j:int in range(i+1, _active_action_mappings.size()):
+ var inner_mapping = _active_action_mappings[j]
+ if chorded_actions.has(inner_mapping.action):
+ continue
+
+ for input_mapping:GUIDEInputMapping in inner_mapping.input_mappings:
+ if input_mapping.input == null:
+ continue
+
+ # because we consolidated input, we can now do an == comparison
+ # to find equal input.
+ if inputs.has(input_mapping.input):
+ blocked_actions.add(inner_mapping.action)
+ # we can continue to the next action
+ break
+
+ if not blocked_actions.is_empty():
+ _actions_sharing_input[mapping.action] = blocked_actions.values()
+
+ # finally collect which inputs we need to reset per frame
+ _reset_node._inputs_to_reset.clear()
+ for input:GUIDEInput in _active_inputs:
+ if input._needs_reset():
+ _reset_node._inputs_to_reset.append(input)
+ # Notify inputs that GUIDE is about to use them
+ input._begin_usage()
+
+ for mapping in _active_action_mappings:
+ for input_mapping in mapping.input_mappings:
+ # notify modifiers they will be used.
+ for modifier in input_mapping.modifiers:
+ modifier._begin_usage()
+
+ # and copy over the hold time threshold from the mapping
+ mapping.action._trigger_hold_threshold = input_mapping._trigger_hold_threshold
+
+ # and notify interested parties that the input mappings have changed
+ input_mappings_changed.emit()
+
+
diff --git a/addons/guide/guide_action.gd b/addons/guide/guide_action.gd
new file mode 100644
index 0000000..a92ef64
--- /dev/null
+++ b/addons/guide/guide_action.gd
@@ -0,0 +1,254 @@
+@tool
+@icon("res://addons/guide/guide_action.svg")
+class_name GUIDEAction
+extends Resource
+
+enum GUIDEActionValueType {
+ BOOL = 0,
+ AXIS_1D = 1,
+ AXIS_2D = 2,
+ AXIS_3D = 3
+}
+
+enum GUIDEActionState {
+ TRIGGERED,
+ ONGOING,
+ COMPLETED
+}
+
+## The name of this action. Required when this action should be used as
+## Godot action. Also displayed in the debugger.
+@export var name:StringName:
+ set(value):
+ if name == value:
+ return
+ name = value
+ emit_changed()
+
+
+## The action value type.
+@export var action_value_type: GUIDEActionValueType = GUIDEActionValueType.BOOL:
+ set(value):
+ if action_value_type == value:
+ return
+ action_value_type = value
+ emit_changed()
+
+## If this action triggers, lower-priority actions cannot trigger
+## if they share input with this action unless these actions are
+## chorded with this action.
+@export var block_lower_priority_actions:bool = true:
+ set(value):
+ if block_lower_priority_actions == value:
+ return
+ block_lower_priority_actions = value
+ emit_changed()
+
+
+@export_category("Godot Actions")
+## If true, then this action will be emitted into Godot's
+## built-in action system. This can be helpful to interact with
+## code using this system, like Godot's UI system. Actions
+## will be emitted on trigger and completion (e.g. button down
+## and button up).
+@export var emit_as_godot_actions:bool = false:
+ set(value):
+ if emit_as_godot_actions == value:
+ return
+ emit_as_godot_actions = value
+ emit_changed()
+
+
+@export_category("Action Remapping")
+
+## If true, players can remap this action. To be remappable, make sure
+## that a name and the action type are properly set.
+@export var is_remappable:bool:
+ set(value):
+ if is_remappable == value:
+ return
+ is_remappable = value
+ emit_changed()
+
+## The display name of the action shown to the player.
+@export var display_name:String:
+ set(value):
+ if display_name == value:
+ return
+ display_name = value
+ emit_changed()
+
+## The display category of the action shown to the player.
+@export var display_category:String:
+ set(value):
+ if display_category == value:
+ return
+ display_category = value
+ emit_changed()
+
+## Emitted every frame while the action is triggered.
+signal triggered()
+
+## Emitted when the action started evaluating.
+signal started()
+
+## Emitted every frame while the action is still evaluating.
+signal ongoing()
+
+## Emitted when the action finished evaluating.
+signal completed()
+
+## Emitted when the action was cancelled.
+signal cancelled()
+
+var _last_state:GUIDEActionState = GUIDEActionState.COMPLETED
+
+var _value_bool:bool
+## Returns the value of this action as bool.
+var value_bool:bool:
+ get: return _value_bool
+
+## Returns the value of this action as float.
+var value_axis_1d:float:
+ get: return _value.x
+
+var _value_axis_2d:Vector2 = Vector2.ZERO
+## Returns the value of this action as Vector2.
+var value_axis_2d:Vector2:
+ get: return _value_axis_2d
+
+var _value:Vector3 = Vector3.ZERO
+## Returns the value of this action as Vector3.
+var value_axis_3d:Vector3:
+ get: return _value
+
+
+var _elapsed_seconds:float
+## The amount of seconds elapsed since the action started evaluating.
+var elapsed_seconds:float:
+ get: return _elapsed_seconds
+
+var _elapsed_ratio:float
+## The ratio of the elapsed time to the hold time. This is a percentage
+## of the hold time that has passed. If the action has no hold time, this will
+## be 0 when the action is not triggered and 1 when the action is triggered.
+## Otherwise, this will be a value between 0 and 1.
+var elapsed_ratio:float:
+ get: return _elapsed_ratio
+
+var _triggered_seconds:float
+## The amount of seconds elapsed since the action triggered.
+var triggered_seconds:float:
+ get: return _triggered_seconds
+
+
+## This is a hint for how long the input must remain actuated (in seconds) before the action triggers.
+## It depends on the mapping in which this action is used. If the mapping has no hold trigger it will be -1.
+## In general, you should not access this variable directly, but rather the `elapsed_ratio` property of the action
+## which is a percentage of the hold time that has passed.
+var _trigger_hold_threshold:float = -1.0
+
+func _triggered(value:Vector3, delta:float) -> void:
+ _triggered_seconds += delta
+ _elapsed_ratio = 1.0
+ _update_value(value)
+ _last_state = GUIDEActionState.TRIGGERED
+ triggered.emit()
+ _emit_godot_action_maybe(true)
+
+func _started(value:Vector3) -> void:
+ _elapsed_ratio = 0.0
+ _update_value(value)
+ _last_state = GUIDEActionState.ONGOING
+ started.emit()
+ ongoing.emit()
+
+func _ongoing(value:Vector3, delta:float) -> void:
+ _elapsed_seconds += delta
+ if _trigger_hold_threshold > 0:
+ _elapsed_ratio = _elapsed_seconds / _trigger_hold_threshold
+ _update_value(value)
+ var was_triggered:bool = _last_state == GUIDEActionState.TRIGGERED
+ _last_state = GUIDEActionState.ONGOING
+ ongoing.emit()
+ # if the action reverts from triggered to ongoing, this counts as
+ # releasing the action for the godot action system.
+ if was_triggered:
+ _emit_godot_action_maybe(false)
+
+
+func _cancelled(value:Vector3) -> void:
+ _elapsed_seconds = 0
+ _elapsed_ratio = 0
+ _update_value(value)
+ _last_state = GUIDEActionState.COMPLETED
+ cancelled.emit()
+ completed.emit()
+
+func _completed(value:Vector3) -> void:
+ _elapsed_seconds = 0
+ _elapsed_ratio = 0
+ _triggered_seconds = 0
+ _update_value(value)
+ _last_state = GUIDEActionState.COMPLETED
+ completed.emit()
+ _emit_godot_action_maybe(false)
+
+func _emit_godot_action_maybe(pressed:bool) -> void:
+ if not emit_as_godot_actions:
+ return
+
+ if name.is_empty():
+ push_error("Cannot emit action into Godot's system because name is empty.")
+ return
+
+ var godot_action = InputEventAction.new()
+ godot_action.action = name
+ godot_action.strength = _value.x
+ godot_action.pressed = pressed
+ Input.parse_input_event(godot_action)
+
+func _update_value(value:Vector3):
+ match action_value_type:
+ GUIDEActionValueType.BOOL, GUIDEActionValueType.AXIS_1D:
+ _value_bool = abs(value.x) > 0
+ _value_axis_2d = Vector2(abs(value.x), 0)
+ _value = Vector3(value.x, 0, 0)
+ GUIDEActionValueType.AXIS_2D:
+ _value_bool = abs(value.x) > 0
+ _value_axis_2d = Vector2(value.x, value.y)
+ _value = Vector3(value.x, value.y, 0)
+ GUIDEActionValueType.AXIS_3D:
+ _value_bool = abs(value.x) > 0
+ _value_axis_2d = Vector2(value.x, value.y)
+ _value = value
+
+## Returns whether the action is currently triggered. Can be used for a
+## polling style input.
+func is_triggered() -> bool:
+ return _last_state == GUIDEActionState.TRIGGERED
+
+
+## Returns whether the action is currently completed. Can be used for a
+## polling style input.
+func is_completed() -> bool:
+ return _last_state == GUIDEActionState.COMPLETED
+
+
+## Returns whether the action is currently completed. Can be used for a
+## polling style input.
+func is_ongoing() -> bool:
+ return _last_state == GUIDEActionState.ONGOING
+
+
+func _editor_name() -> String:
+ # Try to give the most user friendly name
+ if display_name != "":
+ return display_name
+
+ if name != "":
+ return name
+
+ return resource_path.get_file().replace(".tres", "")
+
+
diff --git a/addons/guide/guide_action.svg b/addons/guide/guide_action.svg
new file mode 100644
index 0000000..b39120c
--- /dev/null
+++ b/addons/guide/guide_action.svg
@@ -0,0 +1,7 @@
+
+
+
diff --git a/addons/godot-rapier2d/Radial2D.svg.import b/addons/guide/guide_action.svg.import
similarity index 70%
rename from addons/godot-rapier2d/Radial2D.svg.import
rename to addons/guide/guide_action.svg.import
index 317d9d1..a55db27 100644
--- a/addons/godot-rapier2d/Radial2D.svg.import
+++ b/addons/guide/guide_action.svg.import
@@ -2,8 +2,8 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://cho3shol3rky2"
-path="res://.godot/imported/Radial2D.svg-4380d9841bddc115105cff2016be7c6a.ctex"
+uid="uid://bei7cw115tks0"
+path="res://.godot/imported/guide_action.svg-4d1dfb47183d95c4796078798ce2d0ab.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
@@ -11,8 +11,8 @@ metadata={
[deps]
-source_file="res://addons/godot-rapier2d/Radial2D.svg"
-dest_files=["res://.godot/imported/Radial2D.svg-4380d9841bddc115105cff2016be7c6a.ctex"]
+source_file="res://addons/guide/guide_action.svg"
+dest_files=["res://.godot/imported/guide_action.svg-4d1dfb47183d95c4796078798ce2d0ab.ctex"]
[params]
@@ -33,6 +33,6 @@ process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
-svg/scale=1.0
+svg/scale=0.5
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false
diff --git a/addons/guide/guide_action_mapping.gd b/addons/guide/guide_action_mapping.gd
new file mode 100644
index 0000000..f965b9f
--- /dev/null
+++ b/addons/guide/guide_action_mapping.gd
@@ -0,0 +1,21 @@
+@icon("res://addons/guide/guide_internal.svg")
+@tool
+## An action to input mapping
+class_name GUIDEActionMapping
+extends Resource
+
+## The action to be mapped
+@export var action:GUIDEAction:
+ set(value):
+ if value == action:
+ return
+ action = value
+ emit_changed()
+
+## A set of input mappings that can trigger the action
+@export var input_mappings:Array[GUIDEInputMapping] = []:
+ set(value):
+ if value == input_mappings:
+ return
+ input_mappings = value
+ emit_changed()
diff --git a/addons/guide/guide_input_mapping.gd b/addons/guide/guide_input_mapping.gd
new file mode 100644
index 0000000..62d4dd9
--- /dev/null
+++ b/addons/guide/guide_input_mapping.gd
@@ -0,0 +1,177 @@
+@icon("res://addons/guide/guide_internal.svg")
+@tool
+## A mapping from actuated input to a trigger result
+class_name GUIDEInputMapping
+extends Resource
+
+## Whether the remapping configuration in this input mapping
+## should override the configuration of the bound action. Enable
+## this, to give a key a custom name or category for remapping.
+@export var override_action_settings:bool = false:
+ set(value):
+ if override_action_settings == value:
+ return
+ override_action_settings = value
+ emit_changed()
+
+## If true, players can remap this input mapping. Note that the
+## action to which this input is bound also needs to be remappable
+## for this setting to have an effect.
+@export var is_remappable:bool = false:
+ set(value):
+ if is_remappable == value:
+ return
+ is_remappable = value
+ emit_changed()
+
+## The display name of the input mapping shown to the player. If empty,
+## the display name of the action is used.
+@export var display_name:String = "":
+ set(value):
+ if display_name == value:
+ return
+ display_name = value
+ emit_changed()
+
+## The display category of the input mapping. If empty, the display name of the
+## action is used.
+@export var display_category:String = "":
+ set(value):
+ if display_category == value:
+ return
+ display_category = value
+ emit_changed()
+
+
+@export_group("Mappings")
+## The input to be actuated
+@export var input:GUIDEInput:
+ set(value):
+ if value == input:
+ return
+ input = value
+ emit_changed()
+
+
+## A list of modifiers that preprocess the actuated input before
+## it is fed to the triggers.
+@export var modifiers:Array[GUIDEModifier] = []:
+ set(value):
+ if value == modifiers:
+ return
+ modifiers = value
+ emit_changed()
+
+
+## A list of triggers that could trigger the mapped action.
+@export var triggers:Array[GUIDETrigger] = []:
+ set(value):
+ if value == triggers:
+ return
+ triggers = value
+ emit_changed()
+
+## Hint for how long the input must remain actuated (in seconds) before the mapping triggers.
+## If the mapping has no hold trigger it will be -1. If it has multiple hold triggers
+## the shortest hold time will be used.
+var _trigger_hold_threshold:float = -1.0
+
+var _state:GUIDETrigger.GUIDETriggerState = GUIDETrigger.GUIDETriggerState.NONE
+var _value:Vector3 = Vector3.ZERO
+
+var _trigger_list:Array[GUIDETrigger] = []
+var _implicit_count:int = 0
+var _explicit_count:int = 0
+
+## Called when the mapping is started to be used by GUIDE. Calculates
+## the number of implicit and explicit triggers so we don't need to do this
+## per frame. Also creates a default trigger when none is set.
+func _initialize() -> void :
+ _trigger_list.clear()
+
+ _implicit_count = 0
+ _explicit_count = 0
+ _trigger_hold_threshold = -1.0
+
+ if triggers.is_empty():
+ # make a default trigger and use that
+ var default_trigger = GUIDETriggerDown.new()
+ default_trigger.actuation_threshold = 0
+ _explicit_count = 1
+ _trigger_list.append(default_trigger)
+ return
+
+ for trigger in triggers:
+ match trigger._get_trigger_type():
+ GUIDETrigger.GUIDETriggerType.EXPLICIT:
+ _explicit_count += 1
+ GUIDETrigger.GUIDETriggerType.IMPLICIT:
+ _implicit_count += 1
+ _trigger_list.append(trigger)
+
+ # collect the hold threshold for hinting the UI about how long
+ # the input must be held down. This is only relevant for hold triggers
+ if trigger is GUIDETriggerHold:
+ if _trigger_hold_threshold == -1:
+ _trigger_hold_threshold = trigger.hold_treshold
+ else:
+ _trigger_hold_threshold = min(_trigger_hold_threshold, trigger.hold_treshold)
+
+
+
+func _update_state(delta:float, value_type:GUIDEAction.GUIDEActionValueType):
+ # Collect the current input value
+ var input_value:Vector3 = input._value if input != null else Vector3.ZERO
+
+ # Run it through all modifiers
+ for modifier:GUIDEModifier in modifiers:
+ input_value = modifier._modify_input(input_value, delta, value_type)
+
+ _value = input_value
+
+ var triggered_implicits:int = 0
+ var triggered_explicits:int = 0
+ var triggered_blocked:int = 0
+
+ # Run over all triggers
+ var result:int = GUIDETrigger.GUIDETriggerState.NONE
+ for trigger:GUIDETrigger in _trigger_list:
+ var trigger_result:GUIDETrigger.GUIDETriggerState = trigger._update_state(_value, delta, value_type)
+ trigger._last_value = _value
+
+ var trigger_type = trigger._get_trigger_type()
+ if trigger_result == GUIDETrigger.GUIDETriggerState.TRIGGERED:
+ match trigger_type:
+ GUIDETrigger.GUIDETriggerType.EXPLICIT:
+ triggered_explicits += 1
+ GUIDETrigger.GUIDETriggerType.IMPLICIT:
+ triggered_implicits += 1
+ GUIDETrigger.GUIDETriggerType.BLOCKING:
+ triggered_blocked += 1
+
+ # we only care about the nuances of explicit triggers. implicits and blocking
+ # can only really return yes or no, so they have no nuance
+ if trigger_type == GUIDETrigger.GUIDETriggerType.EXPLICIT:
+ # Higher value results take precedence over lower value results
+ result = max(result, trigger_result)
+
+ # final collection
+ if triggered_blocked > 0:
+ # some blocker triggered which means that this cannot succeed
+ _state = GUIDETrigger.GUIDETriggerState.NONE
+ return
+
+ if triggered_implicits < _implicit_count:
+ # not all implicits triggered, which also fails this binding
+ _state = GUIDETrigger.GUIDETriggerState.NONE
+ return
+
+ if _explicit_count == 0 and _implicit_count > 0:
+ # if no explicits exist, its enough when all implicits trigger
+ _state = GUIDETrigger.GUIDETriggerState.TRIGGERED
+ return
+
+ # return the best result
+ _state = result
+
+
diff --git a/addons/guide/guide_input_tracker.gd b/addons/guide/guide_input_tracker.gd
new file mode 100644
index 0000000..6dd7ef1
--- /dev/null
+++ b/addons/guide/guide_input_tracker.gd
@@ -0,0 +1,26 @@
+## Tracker that tracks input for a window and injects it into GUIDE.
+## Will automatically keep track of sub-windows.
+extends Node
+
+## Instruments a sub-window so it forwards input events to GUIDE.
+static func _instrument(viewport:Viewport):
+ if viewport.has_meta("x-guide-instrumented"):
+ return
+
+ var tracker = preload("guide_input_tracker.gd").new()
+ tracker.process_mode = Node.PROCESS_MODE_ALWAYS
+ viewport.add_child(tracker, false, Node.INTERNAL_MODE_BACK)
+ viewport.gui_focus_changed.connect(tracker._control_focused)
+
+## Catches unhandled input and forwards it to GUIDE
+func _unhandled_input(event:InputEvent):
+ GUIDE.inject_input(event)
+
+## Some ... creative code ... to catch events from popup windows
+## that are spawned by Godot's control nodes.
+func _control_focused(control:Control):
+ if control is OptionButton or control is ColorPickerButton \
+ or control is MenuButton or control is TabContainer:
+ _instrument(control.get_popup())
+
+
diff --git a/addons/guide/guide_internal.svg b/addons/guide/guide_internal.svg
new file mode 100644
index 0000000..78d42de
--- /dev/null
+++ b/addons/guide/guide_internal.svg
@@ -0,0 +1,12 @@
+
+
+
diff --git a/addons/guide/guide_internal.svg.import b/addons/guide/guide_internal.svg.import
new file mode 100644
index 0000000..f235301
--- /dev/null
+++ b/addons/guide/guide_internal.svg.import
@@ -0,0 +1,38 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://ddkj7kntb4fit"
+path="res://.godot/imported/guide_internal.svg-560a143a1e289215e72d8844f5173844.ctex"
+metadata={
+"has_editor_variant": true,
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/guide_internal.svg"
+dest_files=["res://.godot/imported/guide_internal.svg-560a143a1e289215e72d8844f5173844.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=0.5
+editor/scale_with_editor_scale=true
+editor/convert_colors_with_editor_theme=false
diff --git a/addons/guide/guide_mapping_context.gd b/addons/guide/guide_mapping_context.gd
new file mode 100644
index 0000000..097205b
--- /dev/null
+++ b/addons/guide/guide_mapping_context.gd
@@ -0,0 +1,30 @@
+@tool
+@icon("res://addons/guide/guide_mapping_context.svg")
+class_name GUIDEMappingContext
+extends Resource
+
+const GUIDESet = preload("guide_set.gd")
+
+## The display name for this mapping context during action remapping
+@export var display_name:String:
+ set(value):
+ if value == display_name:
+ return
+ display_name = value
+ emit_changed()
+
+## The mappings. Do yourself a favour and use the G.U.I.D.E panel
+## to edit these.
+@export var mappings:Array[GUIDEActionMapping] = []:
+ set(value):
+ if value == mappings:
+ return
+ mappings = value
+ emit_changed()
+
+
+func _editor_name() -> String:
+ if display_name.is_empty():
+ return resource_path.get_file()
+ else:
+ return display_name
diff --git a/addons/guide/guide_mapping_context.svg b/addons/guide/guide_mapping_context.svg
new file mode 100644
index 0000000..eaa1abf
--- /dev/null
+++ b/addons/guide/guide_mapping_context.svg
@@ -0,0 +1,9 @@
+
+
+
diff --git a/addons/guide/guide_mapping_context.svg.import b/addons/guide/guide_mapping_context.svg.import
new file mode 100644
index 0000000..6f740b7
--- /dev/null
+++ b/addons/guide/guide_mapping_context.svg.import
@@ -0,0 +1,38 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bcwpqc8016n7b"
+path="res://.godot/imported/guide_mapping_context.svg-025f10fbbdb2bb11a96754ab9b725bea.ctex"
+metadata={
+"has_editor_variant": true,
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/guide_mapping_context.svg"
+dest_files=["res://.godot/imported/guide_mapping_context.svg-025f10fbbdb2bb11a96754ab9b725bea.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=0.5
+editor/scale_with_editor_scale=true
+editor/convert_colors_with_editor_theme=false
diff --git a/addons/guide/guide_reset.gd b/addons/guide/guide_reset.gd
new file mode 100644
index 0000000..b77817d
--- /dev/null
+++ b/addons/guide/guide_reset.gd
@@ -0,0 +1,13 @@
+extends Node
+
+
+var _inputs_to_reset:Array[GUIDEInput] = []
+
+func _enter_tree() -> void:
+ # this should run at the end of the frame, so we put in a low priority (= high number)
+ process_priority = 10000000
+
+# Called every frame. 'delta' is the elapsed time since the previous frame.
+func _process(delta: float) -> void:
+ for input:GUIDEInput in _inputs_to_reset:
+ input._reset()
diff --git a/addons/guide/guide_set.gd b/addons/guide/guide_set.gd
new file mode 100644
index 0000000..d439c91
--- /dev/null
+++ b/addons/guide/guide_set.gd
@@ -0,0 +1,40 @@
+## Helper class for modelling sets
+var _values:Dictionary = {}
+
+func add(value:Variant) -> void:
+ _values[value] = value
+
+
+func remove(value:Variant) -> void:
+ _values.erase(value)
+
+
+func clear() -> void:
+ _values.clear()
+
+func is_empty() -> bool:
+ return _values.is_empty()
+
+
+func pull() -> Variant:
+ if is_empty():
+ return null
+
+ var key = _values.keys()[0]
+ remove(key)
+ return key
+
+
+func has(value:Variant) -> bool:
+ return _values.has(value)
+
+## Returns the first item for which the given matcher function returns
+## a true value.
+func first_match(matcher:Callable) -> Variant:
+ for key in _values.keys():
+ if matcher.call(key):
+ return key
+ return null
+
+func values() -> Array:
+ return _values.keys()
diff --git a/addons/guide/inputs/guide_input.gd b/addons/guide/inputs/guide_input.gd
new file mode 100644
index 0000000..cde42fa
--- /dev/null
+++ b/addons/guide/inputs/guide_input.gd
@@ -0,0 +1,50 @@
+@tool
+@icon("res://addons/guide/inputs/guide_input.svg")
+## A class representing some actuated input.
+class_name GUIDEInput
+extends Resource
+
+## The current valueo f this input. Depending on the input type only parts of the
+## returned vector may be relevant.
+var _value:Vector3 = Vector3.ZERO
+
+## Whether this input needs a reset per frame. _input is only called when
+## there is input happening, but some GUIDE inputs may need to be reset
+## in the absence of input.
+func _needs_reset() -> bool:
+ return false
+
+## Resets the input value to the default value. Is called once per frame if
+## _needs_reset returns true.
+func _reset() -> void:
+ _value = Vector3.ZERO
+
+## Called when an input event happens. Should update the
+## the input value of this input.
+func _input(event:InputEvent):
+ pass
+
+## Returns whether this input is the same input as the other input.
+func is_same_as(other:GUIDEInput) -> bool:
+ return false
+
+## Called when the input is started to be used by GUIDE. Can be used to perform
+## initializations.
+func _begin_usage() -> void :
+ pass
+
+## Called, when the input is no longer used by GUIDE. Can be used to perform
+## cleanup.
+func _end_usage() -> void:
+ pass
+
+
+func _editor_name() -> String:
+ return ""
+
+func _editor_description() -> String:
+ return ""
+
+
+func _native_value_type() -> GUIDEAction.GUIDEActionValueType:
+ return -1
diff --git a/addons/guide/inputs/guide_input.svg b/addons/guide/inputs/guide_input.svg
new file mode 100644
index 0000000..7e2314f
--- /dev/null
+++ b/addons/guide/inputs/guide_input.svg
@@ -0,0 +1,12 @@
+
+
+
diff --git a/addons/godot-rapier2d/Fluid2D.svg.import b/addons/guide/inputs/guide_input.svg.import
similarity index 70%
rename from addons/godot-rapier2d/Fluid2D.svg.import
rename to addons/guide/inputs/guide_input.svg.import
index 4a1f6c6..458a207 100644
--- a/addons/godot-rapier2d/Fluid2D.svg.import
+++ b/addons/guide/inputs/guide_input.svg.import
@@ -2,8 +2,8 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://cv17s0qjrqj16"
-path="res://.godot/imported/Fluid2D.svg-947567c51a8b586a0e695be7cba2d975.ctex"
+uid="uid://oku7f5t0ox3r"
+path="res://.godot/imported/guide_input.svg-d7e8ae255db039e6a02cccc3f844cc0e.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
@@ -11,8 +11,8 @@ metadata={
[deps]
-source_file="res://addons/godot-rapier2d/Fluid2D.svg"
-dest_files=["res://.godot/imported/Fluid2D.svg-947567c51a8b586a0e695be7cba2d975.ctex"]
+source_file="res://addons/guide/inputs/guide_input.svg"
+dest_files=["res://.godot/imported/guide_input.svg-d7e8ae255db039e6a02cccc3f844cc0e.ctex"]
[params]
@@ -33,6 +33,6 @@ process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
-svg/scale=1.0
+svg/scale=0.5
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false
diff --git a/addons/guide/inputs/guide_input_action.gd b/addons/guide/inputs/guide_input_action.gd
new file mode 100644
index 0000000..3c677a0
--- /dev/null
+++ b/addons/guide/inputs/guide_input_action.gd
@@ -0,0 +1,59 @@
+## An input that mirrors the action's value while the action is triggered.
+@tool
+class_name GUIDEInputAction
+extends GUIDEInput
+
+## The action that this input should mirror. This is live tracked, so any change in
+## the action will update the input.
+@export var action:GUIDEAction:
+ set(value):
+ if value == action:
+ return
+ action = value
+ emit_changed()
+
+func _begin_usage():
+ if is_instance_valid(action):
+ action.triggered.connect(_on)
+ action.completed.connect(_off)
+ action.ongoing.connect(_off)
+ if action.is_triggered():
+ _on()
+ return
+ # not triggered or no action.
+ _off()
+
+
+func _end_usage():
+ if is_instance_valid(action):
+ action.triggered.disconnect(_on)
+ action.completed.disconnect(_off)
+ action.ongoing.disconnect(_off)
+
+
+func _on() -> void:
+ # on is only called when the action is actually existing, so this is
+ # always not-null here
+ _value = action.value_axis_3d
+
+func _off() -> void:
+ _value = Vector3.ZERO
+
+
+func is_same_as(other:GUIDEInput) -> bool:
+ return other is GUIDEInputAction and other.action == action
+
+
+func _to_string():
+ return "(GUIDEInputAction: " + str(action) + ")"
+
+func _editor_name() -> String:
+ return "Action"
+
+
+func _editor_description() -> String:
+ return "An input that mirrors the action's value while the action is triggered."
+
+
+func _native_value_type() -> GUIDEAction.GUIDEActionValueType:
+ return GUIDEAction.GUIDEActionValueType.AXIS_3D
diff --git a/addons/guide/inputs/guide_input_any.gd b/addons/guide/inputs/guide_input_any.gd
new file mode 100644
index 0000000..5cbd1b5
--- /dev/null
+++ b/addons/guide/inputs/guide_input_any.gd
@@ -0,0 +1,115 @@
+## Input that triggers if any input from the given device class
+## is given. Only looks for button inputs, not axis inputs as axes
+## have a tendency to accidentally trigger.
+@tool
+class_name GUIDEInputAny
+extends GUIDEInput
+
+
+## Should input from mouse buttons be considered? Deprecated, use
+## mouse_buttons instead.
+## @deprecated
+var mouse:bool:
+ get: return mouse_buttons
+ set(value): mouse_buttons = value
+
+## Should input from joy buttons be considered. Deprecated, use
+## joy_buttons instead.
+## @deprecated
+var joy:bool:
+ get: return joy_buttons
+ set(value): joy_buttons = value
+
+## Should input from mouse buttons be considered?
+@export var mouse_buttons:bool = false
+
+## Should input from mouse movement be considered?
+@export var mouse_movement:bool = false
+
+## Minimum movement distance of the mouse before it is considered
+## moving.
+@export var minimum_mouse_movement_distance:float = 1.0
+
+## Should input from gamepad/joystick buttons be considered?
+@export var joy_buttons:bool = false
+
+## Should input from gamepad/joystick axes be considered?
+@export var joy_axes:bool = false
+
+## Minimum strength of a single joy axis actuation before it is considered
+## as actuated.
+@export var minimum_joy_axis_actuation_strength:float = 0.2
+
+## Should input from the keyboard be considered?
+@export var keyboard:bool = false
+
+## Should input from touch be considered?
+@export var touch:bool = false
+
+
+func _needs_reset() -> bool:
+ # Needs reset because we cannot detect the absence of input.
+ return true
+
+func _input(event:InputEvent):
+ if mouse_buttons and event is InputEventMouseButton:
+ _value = Vector3.RIGHT
+ return
+
+ if mouse_movement and event is InputEventMouseMotion \
+ and event.relative.length() >= minimum_mouse_movement_distance:
+ _value = Vector3.RIGHT
+ return
+
+ if joy_buttons and event is InputEventJoypadButton:
+ _value = Vector3.RIGHT
+ return
+
+ if joy_axes and event is InputEventJoypadMotion \
+ and abs(event.axis_value) >= minimum_joy_axis_actuation_strength:
+ _value = Vector3.RIGHT
+ return
+
+ if keyboard and event is InputEventKey:
+ _value = Vector3.RIGHT
+ return
+
+ if touch and (event is InputEventScreenTouch or event is InputEventScreenDrag):
+ _value = Vector3.RIGHT
+ return
+
+ _value = Vector3.ZERO
+
+
+func is_same_as(other:GUIDEInput) -> bool:
+ return other is GUIDEInputAny and \
+ other.mouse == mouse and \
+ other.joy == joy and \
+ other.keyboard == keyboard
+
+func _editor_name() -> String:
+ return "Any Input"
+
+
+func _editor_description() -> String:
+ return "Input that triggers if any input from the given device class is given."
+
+
+func _native_value_type() -> GUIDEAction.GUIDEActionValueType:
+ return GUIDEAction.GUIDEActionValueType.BOOL
+
+# support for legacy properties
+func _get_property_list():
+ return [
+ {
+ "name": "mouse",
+ "type": TYPE_BOOL,
+ "usage": PROPERTY_USAGE_NO_EDITOR
+ },
+ {
+ "name": "joy",
+ "type": TYPE_BOOL,
+ "usage": PROPERTY_USAGE_NO_EDITOR
+ }
+ ]
+
diff --git a/addons/guide/inputs/guide_input_joy_axis_1d.gd b/addons/guide/inputs/guide_input_joy_axis_1d.gd
new file mode 100644
index 0000000..54c4ae6
--- /dev/null
+++ b/addons/guide/inputs/guide_input_joy_axis_1d.gd
@@ -0,0 +1,43 @@
+## Input from a single joy axis.
+@tool
+class_name GUIDEInputJoyAxis1D
+extends GUIDEInputJoyBase
+
+## The joy axis to sample
+@export var axis:JoyAxis = JOY_AXIS_LEFT_X:
+ set(value):
+ if value == axis:
+ return
+ axis = value
+ emit_changed()
+
+func _input(event:InputEvent):
+ if not event is InputEventJoypadMotion:
+ return
+
+ if event.axis != axis:
+ return
+
+ if joy_index > -1 and event.device != _joy_id:
+ return
+
+ _value.x = event.axis_value
+
+
+func is_same_as(other:GUIDEInput) -> bool:
+ return other is GUIDEInputJoyAxis1D and \
+ other.axis == axis and \
+ other.joy_index == joy_index
+
+func _to_string():
+ return "(GUIDEInputJoyAxis1D: axis=" + str(axis) + ", joy_index=" + str(joy_index) + ")"
+
+func _editor_name() -> String:
+ return "Joy Axis 1D"
+
+func _editor_description() -> String:
+ return "The input from a single joy axis."
+
+
+func _native_value_type() -> GUIDEAction.GUIDEActionValueType:
+ return GUIDEAction.GUIDEActionValueType.AXIS_1D
diff --git a/addons/guide/inputs/guide_input_joy_axis_2d.gd b/addons/guide/inputs/guide_input_joy_axis_2d.gd
new file mode 100644
index 0000000..46359f9
--- /dev/null
+++ b/addons/guide/inputs/guide_input_joy_axis_2d.gd
@@ -0,0 +1,58 @@
+## Input from two joy axes.
+class_name GUIDEInputJoyAxis2D
+extends GUIDEInputJoyBase
+
+## The joy axis to sample for x input.
+@export var x:JoyAxis = JOY_AXIS_LEFT_X:
+ set(value):
+ if value == x:
+ return
+ x = value
+ emit_changed()
+
+
+## The joy axis to sample for y input.
+@export var y:JoyAxis = JOY_AXIS_LEFT_Y:
+ set(value):
+ if value == y:
+ return
+ y = value
+ emit_changed()
+
+
+func _input(event:InputEvent):
+ if not event is InputEventJoypadMotion:
+ return
+
+ if event.axis != x and event.axis != y:
+ return
+
+ if joy_index > -1 and event.device != _joy_id:
+ return
+
+ if event.axis == x:
+ _value.x = event.axis_value
+ return
+
+ if event.axis == y:
+ _value.y = event.axis_value
+
+func is_same_as(other:GUIDEInput) -> bool:
+ return other is GUIDEInputJoyAxis2D and \
+ other.x == x and \
+ other.y == y and \
+ other.joy_index == joy_index
+
+func _to_string():
+ return "(GUIDEInputJoyAxis2D: x=" + str(x) + ", y=" + str(y) + ", joy_index=" + str(joy_index) + ")"
+
+
+func _editor_name() -> String:
+ return "Joy Axis 2D"
+
+func _editor_description() -> String:
+ return "The input from two Joy axes. Usually from a stick."
+
+
+func _native_value_type() -> GUIDEAction.GUIDEActionValueType:
+ return GUIDEAction.GUIDEActionValueType.AXIS_2D
diff --git a/addons/guide/inputs/guide_input_joy_base.gd b/addons/guide/inputs/guide_input_joy_base.gd
new file mode 100644
index 0000000..621012b
--- /dev/null
+++ b/addons/guide/inputs/guide_input_joy_base.gd
@@ -0,0 +1,35 @@
+## Base class for joystick inputs.
+@tool
+class_name GUIDEInputJoyBase
+extends GUIDEInput
+
+## The index of the connected joy pad to check. If -1 checks all joypads.
+@export var joy_index:int = -1:
+ set(value):
+ if value == joy_index:
+ return
+ joy_index = value
+ emit_changed()
+
+## Cached joystick ID if we use a joy index.
+var _joy_id:int = -2
+
+func _begin_usage():
+ Input.joy_connection_changed.connect(_update_joy_id)
+ _update_joy_id(null, null)
+
+func _end_usage():
+ Input.joy_connection_changed.disconnect(_update_joy_id)
+
+func _update_joy_id(_ignore, _ignore2):
+ if joy_index < 0:
+ return
+
+ var joypads:Array[int] = Input.get_connected_joypads()
+ if joy_index < joypads.size():
+ _joy_id = joypads[joy_index]
+ else:
+ push_warning("Only ", joypads.size(), " joy pads/sticks connected. Cannot sample in put from index ", joy_index, ".")
+ _joy_id = -2
+
+
diff --git a/addons/guide/inputs/guide_input_joy_button.gd b/addons/guide/inputs/guide_input_joy_button.gd
new file mode 100644
index 0000000..8a83306
--- /dev/null
+++ b/addons/guide/inputs/guide_input_joy_button.gd
@@ -0,0 +1,44 @@
+@tool
+class_name GUIDEInputJoyButton
+extends GUIDEInputJoyBase
+
+@export var button:JoyButton = JOY_BUTTON_A:
+ set(value):
+ if value == button:
+ return
+ button = value
+ emit_changed()
+
+func _input(event:InputEvent):
+ if not event is InputEventJoypadButton:
+ return
+
+ if event.button_index != button:
+ return
+
+
+ if joy_index > -1 and event.device != _joy_id:
+ return
+
+ _value.x = 1.0 if event.pressed else 0.0
+
+
+func is_same_as(other:GUIDEInput) -> bool:
+ return other is GUIDEInputJoyButton and \
+ other.button == button and \
+ other.joy_index == joy_index
+
+
+func _to_string():
+ return "(GUIDEInputJoyButton: button=" + str(button) + ", joy_index=" + str(joy_index) + ")"
+
+
+func _editor_name() -> String:
+ return "Joy Button"
+
+func _editor_description() -> String:
+ return "A button press from a joy button."
+
+
+func _native_value_type() -> GUIDEAction.GUIDEActionValueType:
+ return GUIDEAction.GUIDEActionValueType.BOOL
diff --git a/addons/guide/inputs/guide_input_key.gd b/addons/guide/inputs/guide_input_key.gd
new file mode 100644
index 0000000..06b79cf
--- /dev/null
+++ b/addons/guide/inputs/guide_input_key.gd
@@ -0,0 +1,127 @@
+@tool
+class_name GUIDEInputKey
+extends GUIDEInput
+
+## The physical keycode of the key.
+@export var key:Key:
+ set(value):
+ if value == key:
+ return
+ key = value
+ emit_changed()
+
+
+@export_group("Modifiers")
+## Whether shift must be pressed.
+@export var shift:bool = false:
+ set(value):
+ if value == shift:
+ return
+ shift = value
+ emit_changed()
+
+## Whether control must be pressed.
+@export var control:bool = false:
+ set(value):
+ if value == control:
+ return
+ control = value
+ emit_changed()
+
+## Whether alt must be pressed.
+@export var alt:bool = false:
+ set(value):
+ if value == alt:
+ return
+ alt = value
+ emit_changed()
+
+
+## Whether meta/win/cmd must be pressed.
+@export var meta:bool = false:
+ set(value):
+ if value == meta:
+ return
+ meta = value
+ emit_changed()
+
+## Whether this input should fire if additional
+## modifier keys are currently pressed.
+@export var allow_additional_modifiers:bool = true:
+ set(value):
+ if value == allow_additional_modifiers:
+ return
+ allow_additional_modifiers = value
+ emit_changed()
+
+
+
+func _input(event:InputEvent):
+ if not event is InputEventKey:
+ return
+
+ # we start assuming the key is not pressed right now
+ _value.x = 0.0
+
+ # the key itself must be pressed
+ if not Input.is_physical_key_pressed(key):
+ return
+
+ # every required modifier must be pressed
+ if shift and not Input.is_physical_key_pressed(KEY_SHIFT):
+ return
+
+ if control and not Input.is_physical_key_pressed(KEY_CTRL):
+ return
+
+ if alt and not Input.is_physical_key_pressed(KEY_ALT):
+ return
+
+ if meta and not Input.is_physical_key_pressed(KEY_META):
+ return
+
+ # unless additional modifiers are allowed, every
+ # unselected modifier must not be pressed (except if the
+ # bound key is actually the modifier itself)
+
+ if not allow_additional_modifiers:
+ if not shift and key != KEY_SHIFT and Input.is_physical_key_pressed(KEY_SHIFT):
+ return
+
+ if not control and key != KEY_CTRL and Input.is_physical_key_pressed(KEY_CTRL):
+ return
+
+ if not alt and key != KEY_ALT and Input.is_physical_key_pressed(KEY_ALT):
+ return
+
+ if not meta and key != KEY_META and Input.is_physical_key_pressed(KEY_META):
+ return
+
+ # we're still here, so all required keys are pressed and
+ # no extra keys are pressed
+
+ _value.x = 1.0
+
+
+func is_same_as(other:GUIDEInput) -> bool:
+ return other is GUIDEInputKey \
+ and other.key == key \
+ and other.shift == shift \
+ and other.control == control \
+ and other.alt == alt \
+ and other.meta == meta \
+ and other.allow_additional_modifiers == allow_additional_modifiers
+
+func _to_string():
+ return "(GUIDEInputKey: key=" + str(key) + ", shift=" + str(shift) + ", alt=" + str(alt) + ", control=" + str(control) + ", meta="+ str(meta) + ")"
+
+
+func _editor_name() -> String:
+ return "Key"
+
+func _editor_description() -> String:
+ return "A button press on the keyboard."
+
+
+func _native_value_type() -> GUIDEAction.GUIDEActionValueType:
+ return GUIDEAction.GUIDEActionValueType.BOOL
diff --git a/addons/guide/inputs/guide_input_mouse_axis_1d.gd b/addons/guide/inputs/guide_input_mouse_axis_1d.gd
new file mode 100644
index 0000000..c489349
--- /dev/null
+++ b/addons/guide/inputs/guide_input_mouse_axis_1d.gd
@@ -0,0 +1,47 @@
+@tool
+class_name GUIDEInputMouseAxis1D
+extends GUIDEInput
+
+enum GUIDEInputMouseAxis {
+ X,
+ Y
+}
+
+@export var axis:GUIDEInputMouseAxis:
+ set(value):
+ if value == axis:
+ return
+ axis = value
+ emit_changed()
+
+# we don't get mouse updates when the mouse is not moving, so this needs to be
+# reset every frame
+func _needs_reset() -> bool:
+ return true
+
+func _input(event:InputEvent) -> void:
+ if event is InputEventMouseMotion:
+ match axis:
+ GUIDEInputMouseAxis.X:
+ _value.x = event.relative.x
+ GUIDEInputMouseAxis.Y:
+ _value.x = event.relative.y
+
+
+func is_same_as(other:GUIDEInput):
+ return other is GUIDEInputMouseAxis1D and other.axis == axis
+
+func _to_string():
+ return "(GUIDEInputMouseAxis1D: axis=" + str(axis) + ")"
+
+
+func _editor_name() -> String:
+ return "Mouse Axis 1D"
+
+
+func _editor_description() -> String:
+ return "Relative mouse movement on a single axis."
+
+
+func _native_value_type() -> GUIDEAction.GUIDEActionValueType:
+ return GUIDEAction.GUIDEActionValueType.AXIS_1D
diff --git a/addons/guide/inputs/guide_input_mouse_axis_2d.gd b/addons/guide/inputs/guide_input_mouse_axis_2d.gd
new file mode 100644
index 0000000..bdefe76
--- /dev/null
+++ b/addons/guide/inputs/guide_input_mouse_axis_2d.gd
@@ -0,0 +1,35 @@
+@tool
+class_name GUIDEInputMouseAxis2D
+extends GUIDEInput
+
+
+# we don't get mouse updates when the mouse is not moving, so this needs to be
+# reset every frame
+func _needs_reset() -> bool:
+ return true
+
+func _input(event:InputEvent) -> void:
+ if not event is InputEventMouseMotion:
+ return
+
+ _value.x = event.relative.x
+ _value.y = event.relative.y
+
+func is_same_as(other:GUIDEInput):
+ return other is GUIDEInputMouseAxis2D
+
+
+func _to_string():
+ return "(GUIDEInputMouseAxis2D)"
+
+
+func _editor_name() -> String:
+ return "Mouse Axis 2D"
+
+
+func _editor_description() -> String:
+ return "Relative mouse movement on 2 axes."
+
+
+func _native_value_type() -> GUIDEAction.GUIDEActionValueType:
+ return GUIDEAction.GUIDEActionValueType.AXIS_2D
diff --git a/addons/guide/inputs/guide_input_mouse_button.gd b/addons/guide/inputs/guide_input_mouse_button.gd
new file mode 100644
index 0000000..e5315e9
--- /dev/null
+++ b/addons/guide/inputs/guide_input_mouse_button.gd
@@ -0,0 +1,65 @@
+@tool
+class_name GUIDEInputMouseButton
+extends GUIDEInput
+
+
+@export var button:MouseButton = MOUSE_BUTTON_LEFT:
+ set(value):
+ if value == button:
+ return
+ button = value
+ emit_changed()
+
+
+func _needs_reset():
+ # mouse wheel up and down can potentially send multiple inputs within a single frame
+ # so we need to smooth this out a bit.
+ return button == MOUSE_BUTTON_WHEEL_UP or button == MOUSE_BUTTON_WHEEL_DOWN
+
+var _reset_to:Vector3
+var _was_pressed_this_frame:bool
+
+func _reset() -> void:
+ _was_pressed_this_frame = false
+ _value = _reset_to
+
+
+func _input(event:InputEvent):
+ if not event is InputEventMouseButton:
+ return
+
+ if event.button_index != button:
+ return
+
+
+ if _needs_reset():
+ # we always reset to the last event we received in a frame
+ # so after the frame is over we're still in sync.
+ _reset_to.x = 1.0 if event.pressed else 0.0
+
+ if event.pressed:
+ _was_pressed_this_frame = true
+
+ if not event.pressed and _was_pressed_this_frame:
+ # keep pressed state for this frame
+ return
+
+ _value.x = 1.0 if event.pressed else 0.0
+
+func is_same_as(other:GUIDEInput) -> bool:
+ return other is GUIDEInputMouseButton and other.button == button
+
+
+func _to_string():
+ return "(GUIDEInputMouseButton: button=" + str(button) + ")"
+
+
+func _editor_name() -> String:
+ return "Mouse Button"
+
+func _editor_description() -> String:
+ return "A press of a mouse button. The mouse wheel is also a button."
+
+
+func _native_value_type() -> GUIDEAction.GUIDEActionValueType:
+ return GUIDEAction.GUIDEActionValueType.BOOL
diff --git a/addons/guide/inputs/guide_input_mouse_position.gd b/addons/guide/inputs/guide_input_mouse_position.gd
new file mode 100644
index 0000000..c3389b4
--- /dev/null
+++ b/addons/guide/inputs/guide_input_mouse_position.gd
@@ -0,0 +1,41 @@
+@tool
+class_name GUIDEInputMousePosition
+extends GUIDEInput
+
+
+func _begin_usage() -> void :
+ _update_mouse_position()
+
+
+func _input(event:InputEvent) -> void:
+ if not event is InputEventMouseMotion:
+ return
+
+ _update_mouse_position()
+
+
+func _update_mouse_position():
+ var position:Vector2 = Engine.get_main_loop().root.get_mouse_position()
+
+ _value.x = position.x
+ _value.y = position.y
+
+
+func is_same_as(other:GUIDEInput):
+ return other is GUIDEInputMousePosition
+
+
+func _to_string():
+ return "(GUIDEInputMousePosition)"
+
+
+func _editor_name() -> String:
+ return "Mouse Position"
+
+
+func _editor_description() -> String:
+ return "Position of the mouse in the main viewport."
+
+
+func _native_value_type() -> GUIDEAction.GUIDEActionValueType:
+ return GUIDEAction.GUIDEActionValueType.AXIS_2D
diff --git a/addons/guide/inputs/guide_input_touch_angle.gd b/addons/guide/inputs/guide_input_touch_angle.gd
new file mode 100644
index 0000000..01fb829
--- /dev/null
+++ b/addons/guide/inputs/guide_input_touch_angle.gd
@@ -0,0 +1,83 @@
+## Input representing angle changes between two fingers.
+@tool
+class_name GUIDEInputTouchAngle
+extends GUIDEInput
+
+const GUIDETouchState = preload("guide_touch_state.gd")
+
+## Unit in which the angle should be provided
+enum AngleUnit {
+ ## Angle is provided in radians
+ RADIANS = 0,
+ ## Angle is provided in degrees.
+ DEGREES = 1
+}
+
+## The unit in which the angle should be provided
+@export var unit:AngleUnit = AngleUnit.RADIANS
+
+var _initial_angle:float = INF
+
+# We use the reset call to calculate the angle for this frame
+# so it can serve as reference for the next frame
+func _needs_reset() -> bool:
+ return true
+
+func _reset():
+ var angle = _calculate_angle()
+ # update initial angle when input is actuated or stops being actuated
+ if is_finite(_initial_angle) != is_finite(angle):
+ _initial_angle = angle
+
+func _input(event:InputEvent) -> void:
+ if not GUIDETouchState.process_input_event(event):
+ # not touch-related
+ return
+
+ var angle := _calculate_angle()
+ # if either current angle or initial angle is not set,
+ # we are zero
+ if not is_finite(angle) or not is_finite(_initial_angle):
+ _value = Vector3.ZERO
+ return
+
+ # we assume that _initial_distance is never 0 because
+ # you cannot have two fingers physically at the same place
+ # on a touch screen
+ _value = Vector3(angle - _initial_angle, 0, 0)
+
+
+func _calculate_angle() -> float:
+ var pos1:Vector2 = GUIDETouchState.get_finger_position(0, 2)
+ # if we have no position for first finger, we can immediately abort
+ if not pos1.is_finite():
+ return INF
+
+ var pos2:Vector2 = GUIDETouchState.get_finger_position(1, 2)
+ # if there is no second finger, we can abort as well
+ if not pos2.is_finite():
+ return INF
+
+ # calculate distance for the fingers
+ return -pos1.angle_to_point(pos2)
+
+
+func is_same_as(other:GUIDEInput):
+ return other is GUIDEInputTouchAngle and \
+ other.unit == unit
+
+
+func _to_string():
+ return "(GUIDEInputTouchAngle unit=" + ("radians" if unit == AngleUnit.RADIANS else "degrees") + ")"
+
+
+func _editor_name() -> String:
+ return "Touch Angle"
+
+
+func _editor_description() -> String:
+ return "Angle changes of two touching fingers."
+
+
+func _native_value_type() -> GUIDEAction.GUIDEActionValueType:
+ return GUIDEAction.GUIDEActionValueType.AXIS_1D
diff --git a/addons/guide/inputs/guide_input_touch_axis_1d.gd b/addons/guide/inputs/guide_input_touch_axis_1d.gd
new file mode 100644
index 0000000..3615efa
--- /dev/null
+++ b/addons/guide/inputs/guide_input_touch_axis_1d.gd
@@ -0,0 +1,44 @@
+@tool
+class_name GUIDEInputTouchAxis1D
+extends GUIDEInputTouchAxisBase
+
+enum GUIDEInputTouchAxis {
+ X,
+ Y
+}
+
+@export var axis:GUIDEInputTouchAxis:
+ set(value):
+ if value == axis:
+ return
+ axis = value
+ emit_changed()
+
+func is_same_as(other:GUIDEInput):
+ return other is GUIDEInputTouchAxis1D and \
+ other.finger_count == finger_count and \
+ other.finger_index == finger_index and \
+ other.axis == axis
+
+func _apply_value(value:Vector2):
+ match axis:
+ GUIDEInputTouchAxis.X:
+ _value = Vector3(value.x, 0, 0)
+ GUIDEInputTouchAxis.Y:
+ _value = Vector3(value.y, 0, 0)
+
+func _to_string():
+ return "(GUIDEInputTouchAxis1D finger_count=" + str(finger_count) + \
+ " finger_index=" + str(finger_index) +" axis=" + ("X" if axis == GUIDEInputTouchAxis.X else "Y") + ")"
+
+
+func _editor_name() -> String:
+ return "Touch Axis1D"
+
+
+func _editor_description() -> String:
+ return "Relative movement of a touching finger on a single axis."
+
+
+func _native_value_type() -> GUIDEAction.GUIDEActionValueType:
+ return GUIDEAction.GUIDEActionValueType.AXIS_1D
diff --git a/addons/guide/inputs/guide_input_touch_axis_2d.gd b/addons/guide/inputs/guide_input_touch_axis_2d.gd
new file mode 100644
index 0000000..13b79d7
--- /dev/null
+++ b/addons/guide/inputs/guide_input_touch_axis_2d.gd
@@ -0,0 +1,27 @@
+@tool
+class_name GUIDEInputTouchAxis2D
+extends GUIDEInputTouchAxisBase
+
+func _apply_value(value:Vector2):
+ _value = Vector3(value.x, value.y, 0)
+
+func is_same_as(other:GUIDEInput):
+ return other is GUIDEInputTouchAxis2D and \
+ other.finger_count == finger_count and \
+ other.finger_index == finger_index
+
+
+func _to_string():
+ return "(GUIDEInputTouchAxis2D finger_count=" + str(finger_count) + \
+ " finger_index=" + str(finger_index) +")"
+
+
+func _editor_name() -> String:
+ return "Touch Axis2D"
+
+
+func _editor_description() -> String:
+ return "2D relative movement of a touching finger."
+
+func _native_value_type() -> GUIDEAction.GUIDEActionValueType:
+ return GUIDEAction.GUIDEActionValueType.AXIS_2D
diff --git a/addons/guide/inputs/guide_input_touch_axis_base.gd b/addons/guide/inputs/guide_input_touch_axis_base.gd
new file mode 100644
index 0000000..3a2ab9f
--- /dev/null
+++ b/addons/guide/inputs/guide_input_touch_axis_base.gd
@@ -0,0 +1,46 @@
+## Base class for axis-like touch input.
+@tool
+class_name GUIDEInputTouchAxisBase
+extends GUIDEInputTouchBase
+
+const GUIDETouchState = preload("guide_touch_state.gd")
+
+var _last_position:Vector2 = Vector2.INF
+
+# We use the reset call to calculate the position for this frame
+# so it can serve as reference for the next frame
+func _needs_reset() -> bool:
+ return true
+
+func _reset() -> void:
+ _last_position = GUIDETouchState.get_finger_position(finger_index, finger_count)
+ _apply_value(_calculate_value(_last_position))
+
+func _input(event:InputEvent) -> void:
+ if not GUIDETouchState.process_input_event(event):
+ # not touch-related
+ return
+
+ # calculate live position from the cache
+ var new_position:Vector2 = GUIDETouchState.get_finger_position(finger_index, finger_count)
+
+ _apply_value(_calculate_value(new_position))
+
+func _apply_value(value:Vector2):
+ pass
+
+func _calculate_value(new_position:Vector2) -> Vector2:
+ # if we cannot calculate a delta because old or new position
+ # are undefined, we say the delta is zero
+ if not _last_position.is_finite() or not new_position.is_finite():
+ return Vector2.ZERO
+
+ return new_position - _last_position
+
+
+func is_same_as(other:GUIDEInput):
+ return other is GUIDEInputTouchAxis2D and \
+ other.finger_count == finger_count and \
+ other.finger_index == finger_index
+
+
diff --git a/addons/guide/inputs/guide_input_touch_base.gd b/addons/guide/inputs/guide_input_touch_base.gd
new file mode 100644
index 0000000..7f9aa0b
--- /dev/null
+++ b/addons/guide/inputs/guide_input_touch_base.gd
@@ -0,0 +1,22 @@
+## Base class for generic touch input
+@tool
+class_name GUIDEInputTouchBase
+extends GUIDEInput
+
+## The number of fingers to be tracked.
+@export_range(1, 5, 1, "or_greater") var finger_count:int = 1:
+ set(value):
+ if value < 1:
+ value = 1
+ finger_count = value
+ emit_changed()
+
+## The index of the finger for which the position/delta should be reported
+## (0 = first finger, 1 = second finger, etc.). If -1, reports the average position/delta for
+## all fingers currently touching.
+@export_range(-1, 4, 1, "or_greater") var finger_index:int = 0:
+ set(value):
+ if value < -1:
+ value = -1
+ finger_index = value
+ emit_changed()
diff --git a/addons/guide/inputs/guide_input_touch_distance.gd b/addons/guide/inputs/guide_input_touch_distance.gd
new file mode 100644
index 0000000..c4263a5
--- /dev/null
+++ b/addons/guide/inputs/guide_input_touch_distance.gd
@@ -0,0 +1,72 @@
+## Input representing the distance changes between two fingers.
+@tool
+class_name GUIDEInputTouchDistance
+extends GUIDEInput
+
+const GUIDETouchState = preload("guide_touch_state.gd")
+
+var _initial_distance:float = INF
+
+# We use the reset call to calculate the distance for this frame
+# so it can serve as reference for the next frame
+func _needs_reset() -> bool:
+ return true
+
+func _reset():
+ var distance = _calculate_distance()
+ # update initial distance when input is actuated or stops being actuated
+ if is_finite(_initial_distance) != is_finite(distance):
+ _initial_distance = distance
+
+
+func _input(event:InputEvent) -> void:
+ if not GUIDETouchState.process_input_event(event):
+ # not touch-related
+ return
+
+ var distance := _calculate_distance()
+ # if either current distance or initial distance is not set,
+ # we are zero
+ if not is_finite(distance) or not is_finite(_initial_distance):
+ _value = Vector3.ZERO
+ return
+
+ # we assume that _initial_distance is never 0 because
+ # you cannot have two fingers physically at the same place
+ # on a touch screen
+ _value = Vector3(distance / _initial_distance, 0, 0)
+
+
+func _calculate_distance() -> float:
+ var pos1:Vector2 = GUIDETouchState.get_finger_position(0, 2)
+ # if we have no position for first finger, we can immediately abort
+ if not pos1.is_finite():
+ return INF
+
+ var pos2:Vector2 = GUIDETouchState.get_finger_position(1, 2)
+ # if there is no second finger, we can abort as well
+ if not pos2.is_finite():
+ return INF
+
+ # calculate distance for the fingers
+ return pos1.distance_to(pos2)
+
+
+func is_same_as(other:GUIDEInput):
+ return other is GUIDEInputTouchDistance
+
+
+func _to_string():
+ return "(GUIDEInputTouchDistance)"
+
+
+func _editor_name() -> String:
+ return "Touch Distance"
+
+
+func _editor_description() -> String:
+ return "Distance of two touching fingers."
+
+
+func _native_value_type() -> GUIDEAction.GUIDEActionValueType:
+ return GUIDEAction.GUIDEActionValueType.AXIS_1D
diff --git a/addons/guide/inputs/guide_input_touch_position.gd b/addons/guide/inputs/guide_input_touch_position.gd
new file mode 100644
index 0000000..7778987
--- /dev/null
+++ b/addons/guide/inputs/guide_input_touch_position.gd
@@ -0,0 +1,47 @@
+@tool
+class_name GUIDEInputTouchPosition
+extends GUIDEInputTouchBase
+
+const GUIDETouchState = preload("guide_touch_state.gd")
+
+
+func _begin_usage():
+ _value = Vector3.INF
+
+
+func _input(event:InputEvent) -> void:
+ # update touch state
+ if not GUIDETouchState.process_input_event(event):
+ # not touch-related
+ return
+
+ # update finger position
+ var position := GUIDETouchState.get_finger_position(finger_index, finger_count)
+ if not position.is_finite():
+ _value = Vector3.INF
+ return
+
+ _value = Vector3(position.x, position.y, 0)
+
+
+func is_same_as(other:GUIDEInput):
+ return other is GUIDEInputTouchPosition and \
+ other.finger_count == finger_count and \
+ other.finger_index == finger_index
+
+
+func _to_string():
+ return "(GUIDEInputTouchPosition finger_count=" + str(finger_count) + \
+ " finger_index=" + str(finger_index) +")"
+
+
+func _editor_name() -> String:
+ return "Touch Position"
+
+
+func _editor_description() -> String:
+ return "Position of a touching finger."
+
+
+func _native_value_type() -> GUIDEAction.GUIDEActionValueType:
+ return GUIDEAction.GUIDEActionValueType.AXIS_2D
diff --git a/addons/guide/inputs/guide_touch_state.gd b/addons/guide/inputs/guide_touch_state.gd
new file mode 100644
index 0000000..dd10fb8
--- /dev/null
+++ b/addons/guide/inputs/guide_touch_state.gd
@@ -0,0 +1,73 @@
+@tool
+## Shared information about current touch state. This simplifies implementation of the touch inputs
+## and avoids having to process the same events multiple times.
+
+# Cached finger positions
+static var _finger_positions:Dictionary = {}
+
+# Events processed this frame.
+static var _processed_events:Dictionary = {}
+
+# Last frame we were called
+static var _last_frame:int = -1
+
+
+## Processes an input event and updates touch state. Returns true, if the given event
+## was touch-related.
+static func process_input_event(event:InputEvent) -> bool:
+ if not event is InputEventScreenTouch and not event is InputEventScreenDrag:
+ return false
+
+ var this_frame = Engine.get_process_frames()
+
+ # if we are in a new frame, clear the processed events
+ if this_frame != _last_frame:
+ _last_frame = this_frame
+ _processed_events.clear()
+
+ # if the event already was processed, skip processing it again
+ if _processed_events.has(event):
+ return true
+
+ _processed_events[event] = true
+
+ var index:int = event.index
+
+ if event is InputEventScreenTouch:
+ if event.pressed:
+ _finger_positions[index] = event.position
+ else:
+ _finger_positions.erase(index)
+
+ if event is InputEventScreenDrag:
+ _finger_positions[index] = event.position
+
+ return true
+
+
+## Gets the finger position of the finger at the given index.
+## If finger_index is < 0, returns the average of all finger positions.
+## Will only return a position if the amount of fingers
+## currently touching matches finger_count.
+##
+## If no finger position can be determined, returns Vector2.INF.
+static func get_finger_position(finger_index:int, finger_count:int) -> Vector2:
+ # if we have no finger positions right now, we can cut it short here
+ if _finger_positions.is_empty():
+ return Vector2.INF
+
+ # If the finger count doesn't match we have no position right now
+ if _finger_positions.size() != finger_count:
+ return Vector2.INF
+
+ # if a finger index is set, use this fingers position, if available
+ if finger_index > -1:
+ return _finger_positions.get(finger_index, Vector2.INF)
+
+
+ var result = Vector2.ZERO
+ for value in _finger_positions.values():
+ result += value
+
+ result /= float(finger_count)
+ return result
diff --git a/addons/guide/modifiers/guide_modifier.gd b/addons/guide/modifiers/guide_modifier.gd
new file mode 100644
index 0000000..6253480
--- /dev/null
+++ b/addons/guide/modifiers/guide_modifier.gd
@@ -0,0 +1,23 @@
+@tool
+@icon("res://addons/guide/modifiers/guide_modifier.svg")
+class_name GUIDEModifier
+extends Resource
+
+## Called when the modifier is started to be used by GUIDE. Can be used to perform
+## initializations.
+func _begin_usage() -> void :
+ pass
+
+## Called, when the modifier is no longer used by GUIDE. Can be used to perform
+## cleanup.
+func _end_usage() -> void:
+ pass
+
+func _modify_input(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> Vector3:
+ return input
+
+func _editor_name() -> String:
+ return ""
+
+func _editor_description() -> String:
+ return ""
diff --git a/addons/guide/modifiers/guide_modifier.svg b/addons/guide/modifiers/guide_modifier.svg
new file mode 100644
index 0000000..e51d736
--- /dev/null
+++ b/addons/guide/modifiers/guide_modifier.svg
@@ -0,0 +1,12 @@
+
+
+
diff --git a/addons/guide/modifiers/guide_modifier.svg.import b/addons/guide/modifiers/guide_modifier.svg.import
new file mode 100644
index 0000000..3d85142
--- /dev/null
+++ b/addons/guide/modifiers/guide_modifier.svg.import
@@ -0,0 +1,38 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://j64d8n4am2uh"
+path="res://.godot/imported/guide_modifier.svg-8cf939ca3244410aba00f7b558561d72.ctex"
+metadata={
+"has_editor_variant": true,
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/modifiers/guide_modifier.svg"
+dest_files=["res://.godot/imported/guide_modifier.svg-8cf939ca3244410aba00f7b558561d72.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=0.5
+editor/scale_with_editor_scale=true
+editor/convert_colors_with_editor_theme=false
diff --git a/addons/guide/modifiers/guide_modifier_3d_coordinates.gd b/addons/guide/modifiers/guide_modifier_3d_coordinates.gd
new file mode 100644
index 0000000..21abb90
--- /dev/null
+++ b/addons/guide/modifiers/guide_modifier_3d_coordinates.gd
@@ -0,0 +1,53 @@
+## Converts a position input in viewport coordinates (e.g. from the mouse position input)
+## into 3D coordinates (e.g. 3D world coordinates). Useful to get a 3D 'world' position.
+## Returns a Vector3.INF if no 3D world coordinates can be determined.
+@tool
+class_name GUIDEModifier3DCoordinates
+extends GUIDEModifier
+
+## The maximum depth of the ray cast used to detect the 3D position.
+@export var max_depth:float = 1000.0
+
+## Whether the rays cast should collide with areas.
+@export var collide_with_areas:bool = false
+
+## Collision mask to use for the ray cast.
+@export_flags_3d_physics var collision_mask:int
+
+
+func _modify_input(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> Vector3:
+ # if we collide with nothing, no need to even try
+ if collision_mask == 0:
+ return Vector3.INF
+
+ if not input.is_finite():
+ return Vector3.INF
+
+ var viewport = Engine.get_main_loop().root
+ var camera:Camera3D = viewport.get_camera_3d()
+ if camera == null:
+ return Vector3.INF
+
+
+ var input_position:Vector2 = Vector2(input.x, input.y)
+
+ var from:Vector3 = camera.project_ray_origin(input_position)
+ var to:Vector3 = from + camera.project_ray_normal(input_position) * max_depth
+ var query:= PhysicsRayQueryParameters3D.create(from, to, collision_mask)
+ query.collide_with_areas = collide_with_areas
+
+ var result = viewport.world_3d.direct_space_state.intersect_ray(query)
+ if result.has("position"):
+ return result.position
+
+ return Vector3.INF
+
+
+
+func _editor_name() -> String:
+ return "3D coordinates"
+
+
+func _editor_description() -> String:
+ return "Converts a position input in viewport coordinates (e.g. from the mouse position input)\n" + \
+ "into 3D coordinates (e.g. 3D world coordinates). Useful to get a 3D 'world' position."
diff --git a/addons/guide/modifiers/guide_modifier_8_way_direction.gd b/addons/guide/modifiers/guide_modifier_8_way_direction.gd
new file mode 100644
index 0000000..8ae2954
--- /dev/null
+++ b/addons/guide/modifiers/guide_modifier_8_way_direction.gd
@@ -0,0 +1,47 @@
+@tool
+## A filter that converts a 2D input into a boolean that is true when the
+## input direction matches the selected direction. Note, that north is negative Y,
+## because in Godot negative Y is up.
+class_name GUIDEModifier8WayDirection
+extends GUIDEModifier
+
+enum GUIDEDirection {
+ EAST = 0,
+ NORTH_EAST = 1,
+ NORTH = 2,
+ NORTH_WEST = 3,
+ WEST = 4,
+ SOUTH_WEST = 5,
+ SOUTH = 6,
+ SOUTH_EAST = 7
+}
+
+## The direction in which the input should point.
+@export var direction:GUIDEDirection = GUIDEDirection.EAST
+
+func _modify_input(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> Vector3:
+ if not input.is_finite():
+ return Vector3.INF
+
+ if input.is_zero_approx():
+ return Vector3.ZERO
+
+
+
+ # get the angle in which the direction is pointing in radians.
+ var angle_radians = atan2( -input.y, input.x );
+ var octant = roundi( 8 * angle_radians / TAU + 8 ) % 8;
+ if octant == direction:
+ return Vector3.RIGHT # (1, 0, 0) indicating boolean true
+ else:
+ return Vector3.ZERO
+
+
+func _editor_name() -> String:
+ return "8-way direction"
+
+
+func _editor_description() -> String:
+ return "Converts a 2D input into a boolean that is true when the\n" + \
+ "input direction matches the selected direction. Note, that north is negative Y,\n" + \
+ "because in Godot negative Y is up."
diff --git a/addons/guide/modifiers/guide_modifier_canvas_coordinates.gd b/addons/guide/modifiers/guide_modifier_canvas_coordinates.gd
new file mode 100644
index 0000000..23a5938
--- /dev/null
+++ b/addons/guide/modifiers/guide_modifier_canvas_coordinates.gd
@@ -0,0 +1,35 @@
+## Converts a position input in viewport coordinates (e.g. from the mouse position input)
+## into canvas coordinates (e.g. 2D world coordinates). Useful to get a 2D 'world' position.
+@tool
+class_name GUIDEModifierCanvasCoordinates
+extends GUIDEModifier
+
+## If checked, the input will be treated as relative input (position change)
+## rather than absolute input (position).
+@export var relative_input:bool:
+ set(value):
+ relative_input = value
+ emit_changed()
+
+func _modify_input(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> Vector3:
+ if not input.is_finite():
+ return Vector3.INF
+
+ var viewport = Engine.get_main_loop().root
+ var transform = viewport.canvas_transform.affine_inverse()
+ var coordinates = transform * Vector2(input.x, input.y)
+
+ if relative_input:
+ var origin = transform * Vector2.ZERO
+ coordinates -= origin
+
+ return Vector3(coordinates.x, coordinates.y, input.z)
+
+
+func _editor_name() -> String:
+ return "Canvas coordinates"
+
+
+func _editor_description() -> String:
+ return "Converts a position input in viewport coordinates (e.g. from the mouse position input)\n" + \
+ "into canvas coordinates (e.g. 2D world coordinates). Useful to get a 2D 'world' position."
diff --git a/addons/guide/modifiers/guide_modifier_curve.gd b/addons/guide/modifiers/guide_modifier_curve.gd
new file mode 100644
index 0000000..bb55b11
--- /dev/null
+++ b/addons/guide/modifiers/guide_modifier_curve.gd
@@ -0,0 +1,51 @@
+@tool
+## Applies a separate curve to each input axis.
+class_name GUIDEModifierCurve
+extends GUIDEModifier
+
+
+## The curve to apply to the x axis
+@export var curve: Curve = default_curve()
+
+## Apply modifier to X axis
+@export var x: bool = true
+
+## Apply modifier to Y axis
+@export var y: bool = true
+
+## Apply modifier to Z axis
+@export var z: bool = true
+
+
+## Create default curve resource with a smoothstep, 0.0 - 1.0 input/output range
+static func default_curve() -> Curve:
+ var curve = Curve.new()
+ curve.add_point(Vector2(0.0, 0.0))
+ curve.add_point(Vector2(1.0, 1.0))
+
+ return curve
+
+
+func _modify_input(input: Vector3, delta: float, value_type: GUIDEAction.GUIDEActionValueType) -> Vector3:
+ # Curve should never be null
+ if curve == null:
+ push_error("No curve added to Curve modifier.")
+ return input
+
+ if not input.is_finite():
+ return Vector3.INF
+
+ # Return vector with enabled axes modified, others remain unchanged.
+ return Vector3(
+ curve.sample(input.x) if x else input.x,
+ curve.sample(input.y) if y else input.y,
+ curve.sample(input.z) if z else input.z
+ )
+
+
+func _editor_name() -> String:
+ return "Curve"
+
+
+func _editor_description() -> String:
+ return "Applies a curve to each input axis."
diff --git a/addons/guide/modifiers/guide_modifier_deadzone.gd b/addons/guide/modifiers/guide_modifier_deadzone.gd
new file mode 100644
index 0000000..b40a00c
--- /dev/null
+++ b/addons/guide/modifiers/guide_modifier_deadzone.gd
@@ -0,0 +1,63 @@
+@tool
+## Inputs between the lower and upper threshold are mapped 0 -> 1.
+## Values outside the thresholds are clamped.
+class_name GUIDEModifierDeadzone
+extends GUIDEModifier
+
+## Lower threshold for the deadzone.
+@export_range(0,1) var lower_threshold:float = 0.2:
+ set(value):
+ if value > upper_threshold:
+ lower_threshold = upper_threshold
+ else:
+ lower_threshold = value
+ emit_changed()
+
+
+## Upper threshold for the deadzone.
+@export_range(0,1) var upper_threshold:float = 1.0:
+ set(value):
+ if value < lower_threshold:
+ upper_threshold = lower_threshold
+ else:
+ upper_threshold = value
+ emit_changed()
+
+
+func _rescale(value:float) -> float:
+ return min(1.0, (max(0.0, abs(value) - lower_threshold) / (upper_threshold - lower_threshold))) * sign(value)
+
+
+func _modify_input(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> Vector3:
+ if upper_threshold <= lower_threshold:
+ return input
+
+ if not input.is_finite():
+ return Vector3.INF
+
+ match value_type:
+ GUIDEAction.GUIDEActionValueType.BOOL, GUIDEAction.GUIDEActionValueType.AXIS_1D:
+ return Vector3(_rescale(input.x), input.y, input.z)
+
+ GUIDEAction.GUIDEActionValueType.AXIS_2D:
+ var v2d = Vector2(input.x, input.y)
+ if v2d.is_zero_approx():
+ return Vector3(0, 0, input.z)
+ v2d = v2d.normalized() * _rescale(v2d.length())
+ return Vector3(v2d.x, v2d.y, input.z)
+
+ GUIDEAction.GUIDEActionValueType.AXIS_3D:
+ if input.is_zero_approx():
+ return Vector3.ZERO
+ return input.normalized() * _rescale(input.length())
+ _:
+ push_error("Unsupported value type. This is a bug. Please report it.")
+ return input
+
+
+func _editor_name() -> String:
+ return "Deadzone"
+
+func _editor_description() -> String:
+ return "Inputs between the lower and upper threshold are mapped 0 -> 1.\n" + \
+ "Values outside the thresholds are clamped."
diff --git a/addons/guide/modifiers/guide_modifier_input_swizzle.gd b/addons/guide/modifiers/guide_modifier_input_swizzle.gd
new file mode 100644
index 0000000..115a50f
--- /dev/null
+++ b/addons/guide/modifiers/guide_modifier_input_swizzle.gd
@@ -0,0 +1,43 @@
+## Swizzle the input vector components. Useful to map 1D input to 2D or vice versa.
+@tool
+class_name GUIDEModifierInputSwizzle
+extends GUIDEModifier
+
+enum GUIDEInputSwizzleOperation {
+ ## Swap X and Y axes.
+ YXZ,
+ ## Swap X and Z axes.
+ ZYX,
+ ## Swap Y and Z axes.
+ XZY,
+ ## Y to X, Z to Y, X to Z.
+ YZX,
+ ## Y to Z, Z to X, X to Y.
+ ZXY
+}
+
+## The new order into which the input should be brought.
+@export var order:GUIDEInputSwizzleOperation = GUIDEInputSwizzleOperation.YXZ
+
+
+func _modify_input(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> Vector3:
+ match order:
+ GUIDEInputSwizzleOperation.YXZ:
+ return Vector3(input.y, input.x, input.z)
+ GUIDEInputSwizzleOperation.ZYX:
+ return Vector3(input.z, input.y, input.x)
+ GUIDEInputSwizzleOperation.XZY:
+ return Vector3(input.x, input.z, input.y)
+ GUIDEInputSwizzleOperation.YZX:
+ return Vector3(input.y, input.z, input.x)
+ GUIDEInputSwizzleOperation.ZXY:
+ return Vector3(input.z, input.x, input.y)
+ _:
+ push_error("Unknown order ", order , " this is most likely a bug, please report it.")
+ return input
+
+func _editor_name() -> String:
+ return "Input Swizzle"
+
+func _editor_description() -> String:
+ return "Swizzle the input vector components. Useful to map 1D input to 2D or vice versa."
diff --git a/addons/guide/modifiers/guide_modifier_map_range.gd b/addons/guide/modifiers/guide_modifier_map_range.gd
new file mode 100644
index 0000000..65f2275
--- /dev/null
+++ b/addons/guide/modifiers/guide_modifier_map_range.gd
@@ -0,0 +1,67 @@
+@tool
+## Maps an input range to an output range and optionally clamps the output.
+class_name GUIDEModifierMapRange
+extends GUIDEModifier
+
+## Should the output be clamped to the range?
+@export var apply_clamp:bool = true
+
+## The minimum input value
+@export var input_min:float = 0.0
+
+## The maximum input value
+@export var input_max:float = 1.0
+
+## The minimum output value
+@export var output_min:float = 0.0
+
+## The maximum output value
+@export var output_max:float = 1.0
+
+## Apply modifier to X axis
+@export var x:bool = true
+
+## Apply modifier to Y axis
+@export var y:bool = true
+
+## Apply modifier to Z axis
+@export var z:bool = true
+
+var _omin:float
+var _omax:float
+
+func _begin_usage():
+ # we calculate the min and max of the output range here, so we can use them later and don't have to
+ # recalculate them every time the modifier is used
+ _omin = min(output_min, output_max)
+ _omax = max(output_min, output_max)
+
+func _modify_input(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> Vector3:
+ if not input.is_finite():
+ return Vector3.INF
+
+ var x_value:float = remap(input.x, input_min, input_max, output_min, output_max)
+ var y_value:float = remap(input.y, input_min, input_max, output_min, output_max)
+ var z_value:float = remap(input.z, input_min, input_max, output_min, output_max)
+
+ if apply_clamp:
+ # clamp doesn't handle reverse ranges, so we need to use our calculated normalized output range
+ # to clamp the output values
+ x_value = clamp(x_value, _omin, _omax)
+ y_value = clamp(y_value, _omin, _omax)
+ z_value = clamp(z_value, _omin, _omax)
+
+ # Return vector with enabled axes set, others unchanged
+ return Vector3(
+ x_value if x else input.x,
+ y_value if y else input.y,
+ z_value if z else input.z,
+ )
+
+
+func _editor_name() -> String:
+ return "Map Range"
+
+
+func _editor_description() -> String:
+ return "Maps an input range to an output range and optionally clamps the output"
diff --git a/addons/guide/modifiers/guide_modifier_negate.gd b/addons/guide/modifiers/guide_modifier_negate.gd
new file mode 100644
index 0000000..895d1ef
--- /dev/null
+++ b/addons/guide/modifiers/guide_modifier_negate.gd
@@ -0,0 +1,52 @@
+## Inverts input per axis.
+@tool
+class_name GUIDEModifierNegate
+extends GUIDEModifier
+
+## Whether the X axis should be inverted.
+@export var x:bool = true:
+ set(value):
+ if x == value:
+ return
+ x = value
+ _update_caches()
+ emit_changed()
+
+## Whether the Y axis should be inverted.
+@export var y:bool = true:
+ set(value):
+ if y == value:
+ return
+ y = value
+ _update_caches()
+ emit_changed()
+
+## Whether the Z axis should be inverted.
+@export var z:bool = true:
+ set(value):
+ if z == value:
+ return
+ z = value
+ _update_caches()
+ emit_changed()
+
+var _multiplier:Vector3 = Vector3.ONE * -1
+
+func _update_caches():
+ _multiplier.x = -1 if x else 1
+ _multiplier.y = -1 if y else 1
+ _multiplier.z = -1 if z else 1
+
+
+func _modify_input(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> Vector3:
+ if not input.is_finite():
+ return Vector3.INF
+
+ return input * _multiplier
+
+func _editor_name() -> String:
+ return "Negate"
+
+
+func _editor_description() -> String:
+ return "Inverts input per axis."
diff --git a/addons/guide/modifiers/guide_modifier_normalize.gd b/addons/guide/modifiers/guide_modifier_normalize.gd
new file mode 100644
index 0000000..7b10f52
--- /dev/null
+++ b/addons/guide/modifiers/guide_modifier_normalize.gd
@@ -0,0 +1,17 @@
+## Normalizes the input vector.
+@tool
+class_name GUIDEModifierNormalize
+extends GUIDEModifier
+
+func _modify_input(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> Vector3:
+ if not input.is_finite():
+ return Vector3.INF
+
+ return input.normalized()
+
+func _editor_name() -> String:
+ return "Normalize"
+
+
+func _editor_description() -> String:
+ return "Normalizes the input vector."
diff --git a/addons/guide/modifiers/guide_modifier_positive_negative.gd b/addons/guide/modifiers/guide_modifier_positive_negative.gd
new file mode 100644
index 0000000..1f0ce35
--- /dev/null
+++ b/addons/guide/modifiers/guide_modifier_positive_negative.gd
@@ -0,0 +1,65 @@
+## Limits inputs to positive or negative values.
+@tool
+class_name GUIDEModifierPositiveNegative
+extends GUIDEModifier
+
+enum LimitRange {
+ POSITIVE = 1,
+ NEGATIVE = 2
+}
+
+## The range of numbers to which the input should be limited
+@export var range:LimitRange = LimitRange.POSITIVE
+
+## Whether the X axis should be affected.
+@export var x:bool = true:
+ set(value):
+ if x == value:
+ return
+ x = value
+ emit_changed()
+
+## Whether the Y axis should be affected.
+@export var y:bool = true:
+ set(value):
+ if y == value:
+ return
+ y = value
+ emit_changed()
+
+## Whether the Z axis should be affected.
+@export var z:bool = true:
+ set(value):
+ if z == value:
+ return
+ z = value
+ emit_changed()
+
+
+
+func _modify_input(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> Vector3:
+ if not input.is_finite():
+ return Vector3.INF
+
+ match range:
+ LimitRange.POSITIVE:
+ return Vector3(
+ max(0, input.x) if x else input.x, \
+ max(0, input.y) if y else input.y, \
+ max(0, input.z) if z else input.z \
+ )
+ LimitRange.NEGATIVE:
+ return Vector3(
+ min(0, input.x) if x else input.x, \
+ min(0, input.y) if y else input.y, \
+ min(0, input.z) if z else input.z \
+ )
+ # should never happen
+ return input
+
+func _editor_name() -> String:
+ return "Positive/Negative"
+
+
+func _editor_description() -> String:
+ return "Clamps the input to positive or negative values."
diff --git a/addons/guide/modifiers/guide_modifier_scale.gd b/addons/guide/modifiers/guide_modifier_scale.gd
new file mode 100644
index 0000000..68e0564
--- /dev/null
+++ b/addons/guide/modifiers/guide_modifier_scale.gd
@@ -0,0 +1,35 @@
+@tool
+## Scales the input by the given value and optionally, delta time.
+class_name GUIDEModifierScale
+extends GUIDEModifier
+
+## The scale by which the input should be scaled.
+@export var scale:Vector3 = Vector3.ONE:
+ set(value):
+ scale = value
+ emit_changed()
+
+
+## If true, delta time will be multiplied in addition to the scale.
+@export var apply_delta_time:bool = false:
+ set(value):
+ apply_delta_time = value
+ emit_changed()
+
+
+func _modify_input(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> Vector3:
+ if not input.is_finite():
+ return Vector3.INF
+
+ if apply_delta_time:
+ return input * scale * delta
+ else:
+ return input * scale
+
+
+func _editor_name() -> String:
+ return "Scale"
+
+
+func _editor_description() -> String:
+ return "Scales the input by the given value and optionally, delta time."
diff --git a/addons/guide/modifiers/guide_modifier_virtual_cursor.gd b/addons/guide/modifiers/guide_modifier_virtual_cursor.gd
new file mode 100644
index 0000000..79ada82
--- /dev/null
+++ b/addons/guide/modifiers/guide_modifier_virtual_cursor.gd
@@ -0,0 +1,105 @@
+## Stateful modifier which provides a virtual "mouse" cursor driven by input. The modifier
+## returns the current cursor position in pixels releative to the origin of the currently
+## active window.
+@tool
+class_name GUIDEModifierVirtualCursor
+extends GUIDEModifier
+
+enum ScreenScale {
+ ## Input is not scaled with input screen size. This means that the cursor will
+ ## visually move slower on higher resolutions.
+ NONE = 0,
+ ## Input is scaled with the longer axis of the screen size (e.g. width in
+ ## landscape mode, height in portrait mode). The cursor will move with
+ ## the same visual speed on all resolutions.
+ LONGER_AXIS = 1,
+ ## Input is scaled with the shorter axis of the screen size (e.g. height in
+ ## landscape mode, width in portrait mode). The cursor will move with the
+ ## same visual speed on all resolutions.
+ SHORTER_AXIS = 2
+}
+
+## The initial position of the virtual cursor (given in screen relative coordinates)
+@export var initial_position:Vector2 = Vector2(0.5, 0.5):
+ set(value):
+ initial_position = value.clamp(Vector2.ZERO, Vector2.ONE)
+
+## The cursor movement speed in pixels.
+@export var speed:Vector3 = Vector3.ONE
+
+## Screen scaling to be applied to the cursor movement. This controls
+## whether the cursor movement speed is resolution dependent or not.
+## If set to anything but [code]None[/code] then the input value will
+## be multiplied with the window width/height depending on the setting.
+@export var screen_scale:ScreenScale = ScreenScale.LONGER_AXIS
+
+## The scale by which the input should be scaled.
+## @deprecated: use [member speed] instead.
+var scale:Vector3:
+ get: return speed
+ set(value): speed = value
+
+## If true, the cursor movement speed is in pixels per second, otherwise it is in pixels
+## per frame.
+@export var apply_delta_time:bool = true
+
+
+## Cursor offset in pixels.
+var _offset:Vector3 = Vector3.ZERO
+
+## Returns the scaled screen size. This takes Godot's scaling factor for windows into account.
+func _get_scaled_screen_size():
+ # Get window size, including scaling factor
+ var window = Engine.get_main_loop().get_root()
+ return window.get_screen_transform().affine_inverse() * Vector2(window.size)
+
+func _begin_usage():
+ var window_size = _get_scaled_screen_size()
+ _offset = Vector3(window_size.x * initial_position.x, window_size.y * initial_position.y, 0)
+
+
+func _modify_input(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> Vector3:
+ if not input.is_finite():
+ # input is invalid, so just return current cursor position
+ return _offset
+
+ var window_size = _get_scaled_screen_size()
+ input *= speed
+
+ if apply_delta_time:
+ input *= delta
+
+ var screen_scale_factor:float = 1.0
+ match screen_scale:
+ ScreenScale.LONGER_AXIS:
+ screen_scale_factor = max(window_size.x, window_size.y)
+ ScreenScale.SHORTER_AXIS:
+ screen_scale_factor = min(window_size.x, window_size.y)
+
+ input *= screen_scale_factor
+
+ # apply input and clamp to window size
+ _offset = (_offset + input).clamp(Vector3.ZERO, Vector3(window_size.x, window_size.y, 0))
+
+ return _offset
+
+func _editor_name() -> String:
+ return "Virtual Cursor"
+
+
+func _editor_description() -> String:
+ return "Stateful modifier which provides a virtual \"mouse\" cursor driven by input. The modifier\n" + \
+ "returns the current cursor position in pixels releative to the origin of the currently \n" + \
+ "active window."
+
+
+# support for legacy properties
+func _get_property_list():
+ return [
+ {
+ "name": "scale",
+ "type": TYPE_VECTOR3,
+ "usage": PROPERTY_USAGE_NO_EDITOR
+ }
+ ]
+
diff --git a/addons/guide/modifiers/guide_modifier_window_relative.gd b/addons/guide/modifiers/guide_modifier_window_relative.gd
new file mode 100644
index 0000000..66c88d7
--- /dev/null
+++ b/addons/guide/modifiers/guide_modifier_window_relative.gd
@@ -0,0 +1,26 @@
+## Converts the value of the input into window-relative units between 0 and 1.
+## E.g. if a mouse cursor moves half a screen to the right and down, then
+## this modifier will return (0.5, 0.5).
+@tool
+class_name GUIDEModifierWindowRelative
+extends GUIDEModifier
+
+
+func _modify_input(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> Vector3:
+ if not input.is_finite():
+ return Vector3.INF
+
+ var window = Engine.get_main_loop().get_root()
+ # We want real pixels, so we need to factor in any scaling that the window does.
+ var window_size:Vector2 = window.get_screen_transform().affine_inverse() * Vector2(window.size)
+ return Vector3(input.x / window_size.x, input.y / window_size.y, input.z)
+
+
+func _editor_name() -> String:
+ return "Window relative"
+
+
+func _editor_description() -> String:
+ return "Converts the value of the input into window-relative units between 0 and 1.\n" + \
+ "E.g. if a mouse cursor moves half a screen to the right and down, then \n" + \
+ "this modifier will return (0.5, 0.5)."
diff --git a/addons/guide/plugin.cfg b/addons/guide/plugin.cfg
new file mode 100644
index 0000000..81e6856
--- /dev/null
+++ b/addons/guide/plugin.cfg
@@ -0,0 +1,7 @@
+[plugin]
+
+name="Godot Unified Input Detection Engine (G.U.I.D.E)"
+description=""
+author="Jan Thomä"
+version="0.5.2"
+script="plugin.gd"
diff --git a/addons/guide/plugin.gd b/addons/guide/plugin.gd
new file mode 100644
index 0000000..cb5410b
--- /dev/null
+++ b/addons/guide/plugin.gd
@@ -0,0 +1,45 @@
+@tool
+extends EditorPlugin
+const MainPanel = preload("editor/mapping_context_editor/mapping_context_editor.tscn")
+
+var _main_panel:Control
+
+
+func _enable_plugin():
+ add_autoload_singleton("GUIDE", "res://addons/guide/guide.gd")
+
+func _enter_tree() -> void:
+ _main_panel = MainPanel.instantiate()
+ _main_panel.initialize(self)
+ EditorInterface.get_editor_main_screen().add_child(_main_panel)
+ # Hide the main panel. Very much required.
+ _make_visible(false)
+
+func _exit_tree() -> void:
+ if is_instance_valid(_main_panel):
+ _main_panel.queue_free()
+ GUIDEInputFormatter.cleanup()
+
+func _disable_plugin():
+ remove_autoload_singleton("GUIDE")
+
+
+func _edit(object):
+ if object is GUIDEMappingContext:
+ _main_panel.edit(object)
+
+func _get_plugin_name() -> String:
+ return "G.U.I.D.E"
+
+func _get_plugin_icon() -> Texture2D:
+ return preload("res://addons/guide/editor/logo_editor_small.svg")
+
+func _has_main_screen() -> bool:
+ return true
+
+func _handles(object:Variant) -> bool:
+ return object is GUIDEMappingContext
+
+func _make_visible(visible):
+ if is_instance_valid(_main_panel):
+ _main_panel.visible = visible
diff --git a/addons/guide/remapping/guide_input_detector.gd b/addons/guide/remapping/guide_input_detector.gd
new file mode 100644
index 0000000..6b2a5d0
--- /dev/null
+++ b/addons/guide/remapping/guide_input_detector.gd
@@ -0,0 +1,281 @@
+@tool
+## Helper node for detecting inputs. Detects the next input matching a specification and
+## emits a signal with the detected input.
+class_name GUIDEInputDetector
+extends Node
+
+## The device type for which the input should be filtered.
+enum DeviceType {
+ ## Only detect input from keyboard.
+ KEYBOARD = 1,
+ ## Only detect input from the mouse.
+ MOUSE = 2,
+ ## Only detect input from joysticks/gamepads.
+ JOY = 4
+ # touch doesn't make a lot of sense as this is usually
+ # not remappable.
+}
+
+## Which joy index should be used for detected joy events
+enum JoyIndex {
+ # Use -1, so the detected input will match any joystick
+ ANY = 0,
+ # Use the actual index of the detected joystick.
+ DETECTED = 1
+}
+
+## A countdown between initiating a dection and the actual start of the
+## detection. This is useful because when the user clicks a button to
+## start a detection, we want to make sure that the player is actually
+## ready (and not accidentally moves anything). If set to 0, no countdown
+## will be started.
+@export_range(0, 2, 0.1, "or_greater") var detection_countdown_seconds:float = 0.5
+
+## Minimum amplitude to detect any axis.
+@export_range(0, 1, 0.1, "or_greater") var minimum_axis_amplitude:float = 0.2
+
+## If any of these inputs is encountered, the detector will
+## treat this as "abort detection".
+@export var abort_detection_on:Array[GUIDEInput] = []
+
+## Which joy index should be returned for detected joy events.
+@export var use_joy_index:JoyIndex = JoyIndex.ANY
+
+## Whether trigger buttons on controllers should be detected when
+## then action value type is limited to boolean.
+@export var allow_triggers_for_boolean_actions:bool = true
+
+## Emitted when the detection has started (e.g. countdown has elapsed).
+## Can be used to signal this to the player.
+signal detection_started()
+
+## Emitted when the input detector detects an input of the given type.
+## If detection was aborted the given input is null.
+signal input_detected(input:GUIDEInput)
+
+# The timer for the detection countdown.
+var _timer:Timer
+
+
+
+func _ready():
+ _timer = Timer.new()
+ _timer.one_shot = true
+ add_child(_timer, false, Node.INTERNAL_MODE_FRONT)
+ _timer.timeout.connect(_begin_detection)
+
+var _is_detecting:bool
+
+## Whether the input detector is currently detecting input.
+var is_detecting:bool:
+ get: return _is_detecting
+
+var _value_type:GUIDEAction.GUIDEActionValueType
+var _device_types:Array[DeviceType] = []
+
+## Detects a boolean input type.
+func detect_bool(device_types:Array[DeviceType] = []) -> void:
+ detect(GUIDEAction.GUIDEActionValueType.BOOL, device_types)
+
+
+## Detects a 1D axis input type.
+func detect_axis_1d(device_types:Array[DeviceType] = []) -> void:
+ detect(GUIDEAction.GUIDEActionValueType.AXIS_1D, device_types)
+
+
+## Detects a 2D axis input type.
+func detect_axis_2d(device_types:Array[DeviceType] = []) -> void:
+ detect(GUIDEAction.GUIDEActionValueType.AXIS_2D, device_types)
+
+
+## Detects a 3D axis input type.
+func detect_axis_3d(device_types:Array[DeviceType] = []) -> void:
+ detect(GUIDEAction.GUIDEActionValueType.AXIS_3D, device_types)
+
+
+## Aborts a running detection. If no detection currently runs
+## does nothing.
+func abort_detection() -> void:
+ _timer.stop()
+ if _is_detecting:
+ _is_detecting = false
+ input_detected.emit(null)
+
+## Detects the given input type. If device types are given
+## will only detect inputs from the given device types.
+## Otherwise will detect inputs from all supported device types.
+func detect(value_type:GUIDEAction.GUIDEActionValueType,
+ device_types:Array[DeviceType] = []) -> void:
+ if device_types == null:
+ push_error("Device types must not be null. Supply an empty array if you want to detect input from all devices.")
+ return
+
+ # reset all abort inputs
+ for input in abort_detection_on:
+ input._reset()
+
+ abort_detection()
+ _value_type = value_type
+ _device_types = device_types
+ _timer.start(detection_countdown_seconds)
+
+
+func _begin_detection():
+ _is_detecting = true
+ detection_started.emit()
+
+
+func _input(event:InputEvent) -> void:
+ if not _is_detecting:
+ return
+
+ # feed the event into the abort inputs
+ for input in abort_detection_on:
+ input._input(event)
+ # if it triggers, we abort
+ if input._value.is_finite() and input._value.length() > 0:
+ # eat the input so it doesn't accidentally trigger something else
+ get_viewport().set_input_as_handled()
+ abort_detection()
+ return
+
+ # check if the event matches the device type we are
+ # looking for
+ if not _matches_device_types(event):
+ return
+
+ # then check if it can be mapped to the desired
+ # value type
+ match _value_type:
+ GUIDEAction.GUIDEActionValueType.BOOL:
+ _try_detect_bool(event)
+ GUIDEAction.GUIDEActionValueType.AXIS_1D:
+ _try_detect_axis_1d(event)
+ GUIDEAction.GUIDEActionValueType.AXIS_2D:
+ _try_detect_axis_2d(event)
+ GUIDEAction.GUIDEActionValueType.AXIS_3D:
+ _try_detect_axis_3d(event)
+
+
+func _matches_device_types(event:InputEvent) -> bool:
+ if _device_types.is_empty():
+ return true
+
+ if event is InputEventKey:
+ return _device_types.has(DeviceType.KEYBOARD)
+
+ if event is InputEventMouse:
+ return _device_types.has(DeviceType.MOUSE)
+
+ if event is InputEventJoypadButton or event is InputEventJoypadMotion:
+ return _device_types.has(DeviceType.JOY)
+
+ return false
+
+
+func _try_detect_bool(event:InputEvent) -> void:
+ if event is InputEventKey and event.is_released():
+ var result := GUIDEInputKey.new()
+ result.key = event.physical_keycode
+ result.shift = event.shift_pressed
+ result.control = event.ctrl_pressed
+ result.meta = event.meta_pressed
+ result.alt = event.alt_pressed
+ _deliver(result)
+ return
+
+ if event is InputEventMouseButton and event.is_released():
+ var result := GUIDEInputMouseButton.new()
+ result.button = event.button_index
+ _deliver(result)
+ return
+
+ if event is InputEventJoypadButton and event.is_released():
+ var result := GUIDEInputJoyButton.new()
+ result.button = event.button_index
+ result.joy_index = _find_joy_index(event.device)
+ _deliver(result)
+
+ if allow_triggers_for_boolean_actions:
+ # only allow joypad trigger buttons
+ if not (event is InputEventJoypadMotion):
+ return
+ if event.axis != JOY_AXIS_TRIGGER_LEFT and \
+ event.axis != JOY_AXIS_TRIGGER_RIGHT:
+ return
+
+ var result := GUIDEInputJoyAxis1D.new()
+ result.axis = event.axis
+ result.joy_index = _find_joy_index(event.device)
+ _deliver(result)
+
+
+
+func _try_detect_axis_1d(event:InputEvent) -> void:
+ if event is InputEventMouseMotion:
+ var result := GUIDEInputMouseAxis1D.new()
+ # Pick the direction in which the mouse was moved more.
+ if abs(event.relative.x) > abs(event.relative.y):
+ result.axis = GUIDEInputMouseAxis1D.GUIDEInputMouseAxis.X
+ else:
+ result.axis = GUIDEInputMouseAxis1D.GUIDEInputMouseAxis.Y
+ _deliver(result)
+ return
+
+ if event is InputEventJoypadMotion:
+ if abs(event.axis_value) < minimum_axis_amplitude:
+ return
+
+ var result := GUIDEInputJoyAxis1D.new()
+ result.axis = event.axis
+ result.joy_index = _find_joy_index(event.device)
+ _deliver(result)
+
+
+func _try_detect_axis_2d(event:InputEvent) -> void:
+ if event is InputEventMouseMotion:
+ var result := GUIDEInputMouseAxis2D.new()
+ _deliver(result)
+ return
+
+ if event is InputEventJoypadMotion:
+ if event.axis_value < minimum_axis_amplitude:
+ return
+
+ var result := GUIDEInputJoyAxis2D.new()
+ match event.axis:
+ JOY_AXIS_LEFT_X, JOY_AXIS_LEFT_Y:
+ result.x = JOY_AXIS_LEFT_X
+ result.y = JOY_AXIS_LEFT_Y
+ JOY_AXIS_RIGHT_X, JOY_AXIS_RIGHT_Y:
+ result.x = JOY_AXIS_RIGHT_X
+ result.y = JOY_AXIS_RIGHT_Y
+ _:
+ # not supported for detection
+ return
+ result.joy_index = _find_joy_index(event.device)
+ _deliver(result)
+ return
+
+
+func _try_detect_axis_3d(event:InputEvent) -> void:
+ # currently no input for 3D
+ pass
+
+
+func _find_joy_index(device_id:int) -> int:
+ if use_joy_index == JoyIndex.ANY:
+ return -1
+
+ var pads := Input.get_connected_joypads()
+ for i in pads.size():
+ if pads[i] == device_id:
+ return i
+
+ return -1
+
+func _deliver(input:GUIDEInput) -> void:
+ _is_detecting = false
+ # eat the input so it doesn't accidentally trigger something else
+ get_viewport().set_input_as_handled()
+ input_detected.emit(input)
diff --git a/addons/guide/remapping/guide_remapper.gd b/addons/guide/remapping/guide_remapper.gd
new file mode 100644
index 0000000..4037819
--- /dev/null
+++ b/addons/guide/remapping/guide_remapper.gd
@@ -0,0 +1,307 @@
+class_name GUIDERemapper
+
+## Emitted when the bound input of an item changes.
+signal item_changed(item:ConfigItem, input:GUIDEInput)
+
+var _remapping_config:GUIDERemappingConfig = GUIDERemappingConfig.new()
+var _mapping_contexts:Array[GUIDEMappingContext] = []
+
+const GUIDESet = preload("../guide_set.gd")
+
+## Loads the default bindings as they are currently configured in the mapping contexts and a mapping
+## config for editing. Note that the given mapping config will not be modified, so editing can be
+## cancelled. Call get_mapping_config to get the modified mapping config.
+func initialize(mapping_contexts:Array[GUIDEMappingContext], remapping_config:GUIDERemappingConfig):
+ _remapping_config = remapping_config.duplicate() if remapping_config != null else GUIDERemappingConfig.new()
+
+ _mapping_contexts.clear()
+
+ for mapping_context in mapping_contexts:
+ if not is_instance_valid(mapping_context):
+ push_error("Cannot add null mapping context. Ignoring.")
+ return
+ _mapping_contexts.append(mapping_context)
+
+
+## Returns the mapping config with all modifications applied.
+func get_mapping_config() -> GUIDERemappingConfig:
+ return _remapping_config.duplicate()
+
+
+func set_custom_data(key:Variant, value:Variant):
+ _remapping_config.custom_data[key] = value
+
+
+func get_custom_data(key:Variant, default:Variant = null) -> Variant:
+ return _remapping_config.custom_data.get(key, default)
+
+
+func remove_custom_data(key:Variant) -> void:
+ _remapping_config.custom_data.erase(key)
+
+
+## Returns all remappable items. Can be filtered by context, display category or
+## action.
+func get_remappable_items(context:GUIDEMappingContext = null,
+ display_category:String = "",
+ action:GUIDEAction = null) -> Array[ConfigItem]:
+
+ if action != null and not action.is_remappable:
+ push_warning("Action filter was set but filtered action is not remappable.")
+ return []
+
+
+ var result:Array[ConfigItem] = []
+ for a_context:GUIDEMappingContext in _mapping_contexts:
+ if context != null and context != a_context:
+ continue
+ for action_mapping:GUIDEActionMapping in a_context.mappings:
+ var mapped_action:GUIDEAction = action_mapping.action
+ # filter non-remappable actions
+ if not mapped_action.is_remappable:
+ continue
+
+ # if action filter is set, only pick mappings for this action
+ if action != null and action != mapped_action:
+ continue
+
+ # make config items
+ for index:int in action_mapping.input_mappings.size():
+ var input_mapping:GUIDEInputMapping = action_mapping.input_mappings[index]
+ if input_mapping.override_action_settings and not input_mapping.is_remappable:
+ # skip non-remappable items
+ continue
+
+ # Calculate effective display category
+ var effective_display_category:String = \
+ _get_effective_display_category(mapped_action, input_mapping)
+
+ # if display category filter is set, only pick mappings
+ # in this category
+ if display_category.length() > 0 and effective_display_category != display_category:
+ continue
+
+ var item = ConfigItem.new(a_context, action_mapping.action, index, input_mapping)
+ item_changed.connect(item._item_changed)
+ result.append(item)
+
+ return result
+
+
+static func _get_effective_display_category(action:GUIDEAction, input_mapping:GUIDEInputMapping) -> String:
+ var result:String = ""
+ if input_mapping.override_action_settings:
+ result = input_mapping.display_category
+
+ if result.is_empty():
+ result = action.display_category
+
+ return result
+
+
+static func _get_effective_display_name(action:GUIDEAction, input_mapping:GUIDEInputMapping) -> String:
+ var result:String = ""
+ if input_mapping.override_action_settings:
+ result = input_mapping.display_name
+
+ if result.is_empty():
+ result = action.display_name
+
+ return result
+
+static func _is_effectively_remappable(action:GUIDEAction, input_mapping:GUIDEInputMapping) -> bool:
+ return action.is_remappable and ((not input_mapping.override_action_settings) or input_mapping.is_remappable)
+
+
+static func _get_effective_value_type(action:GUIDEAction, input_mapping:GUIDEInputMapping) -> GUIDEAction.GUIDEActionValueType:
+ if input_mapping.override_action_settings and input_mapping.input != null:
+ return input_mapping.input._native_value_type()
+
+ return action.action_value_type
+
+
+## Returns a list of all collisions in all contexts when this new input would be applied to the config item.
+func get_input_collisions(item:ConfigItem, input:GUIDEInput) -> Array[ConfigItem]:
+ if not _check_item(item):
+ return []
+ var result:Array[ConfigItem] = []
+
+ if input == null:
+ # no item collides with absent input
+ return result
+
+ # walk over all known contexts and find any mappings.
+ for context:GUIDEMappingContext in _mapping_contexts:
+ for action_mapping:GUIDEActionMapping in context.mappings:
+ for index:int in action_mapping.input_mappings.size():
+ var action := action_mapping.action
+ if context == item.context and action == item.action and index == item.index:
+ # collisions with self are allowed
+ continue
+
+ var input_mapping:GUIDEInputMapping = action_mapping.input_mappings[index]
+ var bound_input:GUIDEInput = input_mapping.input
+ # check if this is currently overridden
+ if _remapping_config._has(context, action, index):
+ bound_input = _remapping_config._get_bound_input_or_null(context, action, index)
+
+ # We have a collision
+ if bound_input != null and bound_input.is_same_as(input):
+ var collision_item := ConfigItem.new(context, action, index, input_mapping)
+ item_changed.connect(collision_item._item_changed)
+ result.append(collision_item)
+
+ return result
+
+
+## Gets the input currently bound to the action in the given context. Can be null if the input
+## is currently not bound.
+func get_bound_input_or_null(item:ConfigItem) -> GUIDEInput:
+ if not _check_item(item):
+ return null
+
+ # If the remapping config has a binding for this, this binding wins.
+ if _remapping_config._has(item.context, item.action, item.index):
+ return _remapping_config._get_bound_input_or_null(item.context, item.action, item.index)
+
+ # otherwise return the default binding for this action in the context
+ for action_mapping:GUIDEActionMapping in item.context.mappings:
+ if action_mapping.action == item.action:
+ if action_mapping.input_mappings.size() > item.index:
+ return action_mapping.input_mappings[item.index].input
+ else:
+ push_error("Action mapping does not have an index of ", item.index , ".")
+
+ return null
+
+## Sets the bound input to the new value for the given config item. Ignores collisions
+## because collision resolution is highly game specific. Use get_input_collisions to find
+## potential collisions and then resolve them in a way that suits the game. Note that
+## bound input can be set to null, which deliberately unbinds the input. If you want
+## to restore the defaults, call restore_default instead.
+func set_bound_input(item:ConfigItem, input:GUIDEInput) -> void:
+ if not _check_item(item):
+ return
+
+ # first remove any custom binding we have
+ _remapping_config._clear(item.context, item.action, item.index)
+
+ # Now check if the input is the same as the default
+ var bound_input:GUIDEInput = get_bound_input_or_null(item)
+
+ if bound_input == null and input == null:
+ item_changed.emit(item, input)
+ return # nothing to do
+
+ if bound_input == null:
+ _remapping_config._bind(item.context, item.action, input, item.index)
+ item_changed.emit(item, input)
+ return
+
+ if bound_input != null and input != null and bound_input.is_same_as(input):
+ item_changed.emit(item, input)
+ return # nothing to do
+
+ _remapping_config._bind(item.context, item.action, input, item.index)
+ item_changed.emit(item, input)
+
+
+## Returns the default binding for the given config item.
+func get_default_input(item:ConfigItem) -> GUIDEInput:
+ if not _check_item(item):
+ return null
+
+ for mapping:GUIDEActionMapping in item.context.mappings:
+ if mapping.action == item.action:
+ # _check_item verifies the index exists, so no need to check here.
+ return mapping.input_mappings[item.index].input
+
+ return null
+
+
+## Restores the default binding for the given config item. Note that this may
+## introduce a conflict if other bindings have bound conflicting input. You can
+## call get_default_input for the given item to get the default input and then
+## call get_input_collisions for that to find out whether you would get a collision.
+func restore_default_for(item:ConfigItem) -> void:
+ if not _check_item(item):
+ return
+
+ _remapping_config._clear(item.context, item.action, item.index)
+ item_changed.emit(item, get_bound_input_or_null(item))
+
+
+
+## Verifies that the given item is valid.
+func _check_item(item:ConfigItem) -> bool:
+ if not _mapping_contexts.has(item.context):
+ push_error("Given context is not known to this mapper. Did you call initialize()?")
+ return false
+
+ var action_found := false
+ var size_ok := false
+ for mapping in item.context.mappings:
+ if mapping.action == item.action:
+ action_found = true
+ if mapping.input_mappings.size() > item.index and item.index >= 0:
+ size_ok = true
+ break
+
+ if not action_found:
+ push_error("Given action does not belong to the given context.")
+ return false
+
+ if not size_ok:
+ push_error("Given index does not exist for the given action's input binding.")
+
+
+ if not item.action.is_remappable:
+ push_error("Given action is not remappable.")
+ return false
+
+ return true
+
+
+class ConfigItem:
+ ## Emitted when the input to this item has changed.
+ signal changed(input:GUIDEInput)
+
+ var _input_mapping:GUIDEInputMapping
+
+ ## The display category for this config item
+ var display_category:String:
+ get: return GUIDERemapper._get_effective_display_category(action, _input_mapping)
+
+ ## The display name for this config item.
+ var display_name:String:
+ get: return GUIDERemapper._get_effective_display_name(action, _input_mapping)
+
+ ## Whether this item is remappable.
+ var is_remappable:bool:
+ get: return GUIDERemapper._is_effectively_remappable(action, _input_mapping)
+
+ ## The value type for this config item.
+ var value_type:GUIDEAction.GUIDEActionValueType:
+ get: return GUIDERemapper._get_effective_value_type(action, _input_mapping)
+
+ var context:GUIDEMappingContext
+ var action:GUIDEAction
+ var index:int
+
+ func _init(context:GUIDEMappingContext, action:GUIDEAction, index:int, input_mapping:GUIDEInputMapping):
+ self.context = context
+ self.action = action
+ self.index = index
+ _input_mapping = input_mapping
+
+ ## Checks whether this config item is the same as some other
+ ## e.g. refers to the same input mapping.
+ func is_same_as(other:ConfigItem) -> bool:
+ return context == other.context and \
+ action == other.action and \
+ index == other.index
+
+ func _item_changed(item:ConfigItem, input:GUIDEInput):
+ if item.is_same_as(self):
+ changed.emit(input)
+
diff --git a/addons/guide/remapping/guide_remapping_config.gd b/addons/guide/remapping/guide_remapping_config.gd
new file mode 100644
index 0000000..7bd6453
--- /dev/null
+++ b/addons/guide/remapping/guide_remapping_config.gd
@@ -0,0 +1,85 @@
+@icon("res://addons/guide/guide_internal.svg")
+## A remapping configuration. This only holds changes to the context mapping,
+## so to get the full input map you need to apply this on top of one or more
+## mapping contexts. The settings from this config take precedence over the
+## settings from the mapping contexts.
+class_name GUIDERemappingConfig
+extends Resource
+
+## Dictionary with remapped inputs. Structure is:
+## {
+## mapping_context : {
+## action : {
+## index : bound input
+## ...
+## }, ...
+## }
+## The bound input can be NULL which means that this was deliberately unbound.
+@export var remapped_inputs:Dictionary = {}
+
+## Dictionary for additional custom data to store (e.g. modifier settings, etc.)
+## Note that this data is completely under application control and it's the responsibility
+## of the application to ensure that this data is serializable and gets applied at
+## the necessary point in time.
+@export var custom_data:Dictionary = {}
+
+## Binds the given input to the given action. Index can be given to have
+## alternative bindings for the same action.
+func _bind(mapping_context:GUIDEMappingContext, action:GUIDEAction, input:GUIDEInput, index:int = 0) -> void:
+ if not remapped_inputs.has(mapping_context):
+ remapped_inputs[mapping_context] = {}
+
+ if not remapped_inputs[mapping_context].has(action):
+ remapped_inputs[mapping_context][action] = {}
+
+ remapped_inputs[mapping_context][action][index] = input
+
+
+## Unbinds the given input from the given action. This is a deliberate unbind
+## which means that the action should not be triggerable by the input anymore. It
+## its not the same as _clear.
+func _unbind(mapping_context:GUIDEMappingContext, action:GUIDEAction, index:int = 0) -> void:
+ _bind(mapping_context, action, null, index)
+
+
+## Removes the given input action binding from this configuration. The action will
+## now have the default input that it has in the mapping_context. This is not the
+## same as _unbind.
+func _clear(mapping_context:GUIDEMappingContext, action:GUIDEAction, index:int = 0) -> void:
+ if not remapped_inputs.has(mapping_context):
+ return
+
+ if not remapped_inputs[mapping_context].has(action):
+ return
+
+ remapped_inputs[mapping_context][action].erase(index)
+
+ if remapped_inputs[mapping_context][action].is_empty():
+ remapped_inputs[mapping_context].erase(action)
+
+ if remapped_inputs[mapping_context].is_empty():
+ remapped_inputs.erase(mapping_context)
+
+
+## Returns the bound input for the given action name and index. Returns null
+## if there is matching binding.
+func _get_bound_input_or_null(mapping_context:GUIDEMappingContext, action:GUIDEAction, index:int = 0) -> GUIDEInput:
+ if not remapped_inputs.has(mapping_context):
+ return null
+
+ if not remapped_inputs[mapping_context].has(action):
+ return null
+
+ return remapped_inputs[mapping_context][action].get(index, null)
+
+
+## Returns whether or not this mapping has a configuration for the given combination (even if the
+## combination is set to null).
+func _has(mapping_context:GUIDEMappingContext, action:GUIDEAction, index:int = 0) -> bool:
+ if not remapped_inputs.has(mapping_context):
+ return false
+
+ if not remapped_inputs[mapping_context].has(action):
+ return false
+
+ return remapped_inputs[mapping_context][action].has(index)
diff --git a/addons/guide/triggers/guide_trigger.gd b/addons/guide/triggers/guide_trigger.gd
new file mode 100644
index 0000000..d158ece
--- /dev/null
+++ b/addons/guide/triggers/guide_trigger.gd
@@ -0,0 +1,65 @@
+@tool
+@icon("res://addons/guide/triggers/guide_trigger.svg")
+class_name GUIDETrigger
+extends Resource
+
+enum GUIDETriggerState {
+ ## The trigger did not fire.
+ NONE,
+ ## The trigger's conditions are partially met
+ ONGOING,
+ ## The trigger has fired.
+ TRIGGERED
+}
+
+enum GUIDETriggerType {
+ # If there are more than one explicit triggers at least one must trigger
+ # for the action to trigger.
+ EXPLICIT = 1,
+ # All implicit triggers must trigger for the action to trigger.
+ IMPLICIT = 2,
+ # All blocking triggers prevent the action from triggering.
+ BLOCKING = 3
+}
+
+
+@export var actuation_threshold:float = 0.5
+var _last_value:Vector3
+
+## Returns the trigger type of this trigger.
+func _get_trigger_type() -> GUIDETriggerType:
+ return GUIDETriggerType.EXPLICIT
+
+
+func _update_state(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> GUIDETriggerState:
+ return GUIDETriggerState.NONE
+
+
+func _is_actuated(input:Vector3, value_type:GUIDEAction.GUIDEActionValueType) -> bool:
+ match value_type:
+ GUIDEAction.GUIDEActionValueType.AXIS_1D, GUIDEAction.GUIDEActionValueType.BOOL:
+ return _is_axis1d_actuated(input)
+ GUIDEAction.GUIDEActionValueType.AXIS_2D:
+ return _is_axis2d_actuated(input)
+ GUIDEAction.GUIDEActionValueType.AXIS_3D:
+ return _is_axis3d_actuated(input)
+
+ return false
+
+## Checks if a 1D input is actuated.
+func _is_axis1d_actuated(input:Vector3) -> bool:
+ return is_finite(input.x) and abs(input.x) > actuation_threshold
+
+## Checks if a 2D input is actuated.
+func _is_axis2d_actuated(input:Vector3) -> bool:
+ return is_finite(input.x) and is_finite(input.y) and Vector2(input.x, input.y).length_squared() > actuation_threshold * actuation_threshold
+
+## Checks if a 3D input is actuated.
+func _is_axis3d_actuated(input:Vector3) -> bool:
+ return input.is_finite() and input.length_squared() > actuation_threshold * actuation_threshold
+
+func _editor_name() -> String:
+ return "GUIDETrigger"
+
+func _editor_description() -> String:
+ return ""
diff --git a/addons/guide/triggers/guide_trigger.svg b/addons/guide/triggers/guide_trigger.svg
new file mode 100644
index 0000000..48f8822
--- /dev/null
+++ b/addons/guide/triggers/guide_trigger.svg
@@ -0,0 +1,12 @@
+
+
+
diff --git a/addons/guide/triggers/guide_trigger.svg.import b/addons/guide/triggers/guide_trigger.svg.import
new file mode 100644
index 0000000..e63657a
--- /dev/null
+++ b/addons/guide/triggers/guide_trigger.svg.import
@@ -0,0 +1,38 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://ca1eiagyinhl7"
+path="res://.godot/imported/guide_trigger.svg-cd87acbd491929cf49a255f8481b0b63.ctex"
+metadata={
+"has_editor_variant": true,
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/triggers/guide_trigger.svg"
+dest_files=["res://.godot/imported/guide_trigger.svg-cd87acbd491929cf49a255f8481b0b63.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=0.5
+editor/scale_with_editor_scale=true
+editor/convert_colors_with_editor_theme=false
diff --git a/addons/guide/triggers/guide_trigger_chorded_action.gd b/addons/guide/triggers/guide_trigger_chorded_action.gd
new file mode 100644
index 0000000..2b06f8c
--- /dev/null
+++ b/addons/guide/triggers/guide_trigger_chorded_action.gd
@@ -0,0 +1,28 @@
+## Fires, when the given action is currently triggering. This trigger is implicit,
+## so it will prevent the action from triggering even if other triggers are successful.
+@tool
+class_name GUIDETriggerChordedAction
+extends GUIDETrigger
+
+@export var action:GUIDEAction
+
+
+func _get_trigger_type() -> GUIDETriggerType:
+ return GUIDETriggerType.IMPLICIT
+
+func _update_state(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> GUIDETriggerState:
+ if action == null:
+ push_warning("Chorded trigger without action will never trigger.")
+ return GUIDETriggerState.NONE
+
+ if action.is_triggered():
+ return GUIDETriggerState.TRIGGERED
+ return GUIDETriggerState.NONE
+
+
+func _editor_name() -> String:
+ return "Chorded Action"
+
+func _editor_description() -> String:
+ return "Fires, when the given action is currently triggering. This trigger is implicit,\n" + \
+ "so it will prevent the action from triggering even if other triggers are successful."
diff --git a/addons/guide/triggers/guide_trigger_combo.gd b/addons/guide/triggers/guide_trigger_combo.gd
new file mode 100644
index 0000000..2464e65
--- /dev/null
+++ b/addons/guide/triggers/guide_trigger_combo.gd
@@ -0,0 +1,117 @@
+@tool
+class_name GUIDETriggerCombo
+extends GUIDETrigger
+
+enum ActionEventType {
+ TRIGGERED = 1,
+ STARTED = 2,
+ ONGOING = 4,
+ CANCELLED = 8,
+ COMPLETED = 16
+}
+
+## If set to true, the combo trigger will print information
+## about state changes to the debug log.
+@export var enable_debug_print:bool = false
+@export var steps:Array[GUIDETriggerComboStep] = []
+@export var cancellation_actions:Array[GUIDETriggerComboCancelAction] = []
+
+var _current_step:int = -1
+var _remaining_time:float = 0
+
+func _update_state(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> GUIDETriggerState:
+ if steps.is_empty():
+ push_warning("Combo with no steps will never fire.")
+ return GUIDETriggerState.NONE
+
+ # initial setup
+ if _current_step == -1:
+ for step in steps:
+ step._prepare()
+ for action in cancellation_actions:
+ action._prepare()
+ _reset()
+
+
+ var current_action := steps[_current_step].action
+ if current_action == null:
+ push_warning("Step ", _current_step , " has no action ", resource_path)
+ return GUIDETriggerState.NONE
+
+ # check if any of our cancellation actions fired
+ for action in cancellation_actions:
+ # if the action is the current action we don't count its firing as cancellation
+ if action.action == current_action:
+ continue
+
+ if action._has_fired:
+ if enable_debug_print:
+ print("Combo cancelled by action '", action.action._editor_name(), "'.")
+ _reset()
+ return GUIDETriggerState.NONE
+
+ # check if any of the steps has fired out of order
+ for step in steps:
+ if step.action == current_action:
+ continue
+
+ if step._has_fired:
+ if enable_debug_print:
+ print("Combo out of order step by action '", step.action._editor_name(), "'.")
+ _reset()
+ return GUIDETriggerState.NONE
+
+ # check if we took too long (unless we're in the first step)
+ if _current_step > 0:
+ _remaining_time -= delta
+ if _remaining_time <= 0.0:
+ if enable_debug_print:
+ print("Step time for step ", _current_step , " exceeded.")
+ _reset()
+ return GUIDETriggerState.NONE
+
+ # if the current action was fired, if so advance to the next
+ if steps[_current_step]._has_fired:
+ # reset this step, so it will not count as misfired next round
+ steps[_current_step]._has_fired = false
+ if _current_step + 1 >= steps.size():
+ # we finished the combo
+ if enable_debug_print:
+ print("Combo fired.")
+ _reset()
+ return GUIDETriggerState.TRIGGERED
+
+ # otherwise, pick the next step
+ _current_step += 1
+ if enable_debug_print:
+ print("Combo advanced to step " , _current_step, ".")
+ _remaining_time = steps[_current_step].time_to_actuate
+
+ # Reset all steps and cancellation actions to "not fired" in
+ # case they were triggered by this action. Otherwise a double-tap
+ # would immediately fire for both taps once the first is through
+ for step in steps:
+ step._has_fired = false
+ for action in cancellation_actions:
+ action._has_fired = false
+
+ # and in any case we're still processing.
+ return GUIDETriggerState.ONGOING
+
+
+func _reset():
+ if enable_debug_print:
+ print("Combo reset.")
+ _current_step = 0
+ _remaining_time = steps[0].time_to_actuate
+ for step in steps:
+ step._has_fired = false
+ for action in cancellation_actions:
+ action._has_fired = false
+
+func _editor_name() -> String:
+ return "Combo"
+
+func _editor_description() -> String:
+ return "Fires, when the input exceeds the actuation threshold."
+
diff --git a/addons/guide/triggers/guide_trigger_combo_cancel_action.gd b/addons/guide/triggers/guide_trigger_combo_cancel_action.gd
new file mode 100644
index 0000000..10a0e83
--- /dev/null
+++ b/addons/guide/triggers/guide_trigger_combo_cancel_action.gd
@@ -0,0 +1,27 @@
+@icon("res://addons/guide/guide_internal.svg")
+class_name GUIDETriggerComboCancelAction
+extends Resource
+
+@export var action:GUIDEAction
+@export_flags("Triggered:1", "Started:2", "Ongoing:4", "Cancelled:8","Completed:16")
+var completion_events:int = GUIDETriggerCombo.ActionEventType.TRIGGERED
+
+var _has_fired:bool = false
+
+func _prepare():
+ if completion_events & GUIDETriggerCombo.ActionEventType.TRIGGERED:
+ action.triggered.connect(_fired)
+ if completion_events & GUIDETriggerCombo.ActionEventType.STARTED:
+ action.started.connect(_fired)
+ if completion_events & GUIDETriggerCombo.ActionEventType.ONGOING:
+ action.ongoing.connect(_fired)
+ if completion_events & GUIDETriggerCombo.ActionEventType.CANCELLED:
+ action.cancelled.connect(_fired)
+ if completion_events & GUIDETriggerCombo.ActionEventType.COMPLETED:
+ action.completed.connect(_fired)
+ _has_fired = false
+
+
+func _fired():
+ _has_fired = true
+
diff --git a/addons/guide/triggers/guide_trigger_combo_step.gd b/addons/guide/triggers/guide_trigger_combo_step.gd
new file mode 100644
index 0000000..4e22bf7
--- /dev/null
+++ b/addons/guide/triggers/guide_trigger_combo_step.gd
@@ -0,0 +1,29 @@
+@icon("res://addons/guide/guide_internal.svg")
+class_name GUIDETriggerComboStep
+extends Resource
+
+@export var action:GUIDEAction
+@export_flags("Triggered:1", "Started:2", "Ongoing:4", "Cancelled:8","Completed:16")
+var completion_events:int = GUIDETriggerCombo.ActionEventType.TRIGGERED
+@export var time_to_actuate:float = 0.5
+
+
+var _has_fired:bool = false
+
+func _prepare():
+ if completion_events & GUIDETriggerCombo.ActionEventType.TRIGGERED:
+ action.triggered.connect(_fired)
+ if completion_events & GUIDETriggerCombo.ActionEventType.STARTED:
+ action.started.connect(_fired)
+ if completion_events & GUIDETriggerCombo.ActionEventType.ONGOING:
+ action.ongoing.connect(_fired)
+ if completion_events & GUIDETriggerCombo.ActionEventType.CANCELLED:
+ action.cancelled.connect(_fired)
+ if completion_events & GUIDETriggerCombo.ActionEventType.COMPLETED:
+ action.completed.connect(_fired)
+ _has_fired = false
+
+
+func _fired():
+ _has_fired = true
+
diff --git a/addons/guide/triggers/guide_trigger_down.gd b/addons/guide/triggers/guide_trigger_down.gd
new file mode 100644
index 0000000..5327d62
--- /dev/null
+++ b/addons/guide/triggers/guide_trigger_down.gd
@@ -0,0 +1,20 @@
+## Fires, when the input exceeds the actuation threshold. This is
+## the default trigger when no trigger is specified.
+@tool
+class_name GUIDETriggerDown
+extends GUIDETrigger
+
+func _update_state(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> GUIDETriggerState:
+ # if the input is actuated, then the trigger is triggered.
+ if _is_actuated(input, value_type):
+ return GUIDETriggerState.TRIGGERED
+ # otherwise, the trigger is not triggered.
+ return GUIDETriggerState.NONE
+
+
+func _editor_name() -> String:
+ return "Down"
+
+func _editor_description() -> String:
+ return "Fires, when the input exceeds the actuation threshold. This is\n" +\
+ "the default trigger when no trigger is specified."
diff --git a/addons/guide/triggers/guide_trigger_hold.gd b/addons/guide/triggers/guide_trigger_hold.gd
new file mode 100644
index 0000000..8ad5bb2
--- /dev/null
+++ b/addons/guide/triggers/guide_trigger_hold.gd
@@ -0,0 +1,43 @@
+@tool
+## A trigger that activates when the input is held down for a certain amount of time.
+class_name GUIDETriggerHold
+extends GUIDETrigger
+
+## The time for how long the input must be held.
+@export var hold_treshold:float = 1.0
+## If true, the trigger will only fire once until the input is released. Otherwise the trigger will fire every frame.
+@export var is_one_shot:bool = false
+
+var _accumulated_time:float = 0
+var _did_shoot:bool = false
+
+func _update_state(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> GUIDETriggerState:
+ # if the input is actuated, accumulate time and check if the hold threshold has been reached
+ if _is_actuated(input, value_type):
+ _accumulated_time += delta
+
+ if _accumulated_time >= hold_treshold:
+ # if the trigger is one shot and we already shot, then we will not trigger again.
+ if is_one_shot and _did_shoot:
+ return GUIDETriggerState.NONE
+ else:
+ # otherwise, we will just trigger.
+ _did_shoot = true
+ return GUIDETriggerState.TRIGGERED
+ else:
+ # if the hold threshold has not been reached, then the trigger is ongoing.
+ return GUIDETriggerState.ONGOING
+ else:
+ # if the input is not actuated, then the trigger is not triggered and we reset the accumulated time.
+ # and our one shot flag.
+ _accumulated_time = 0
+ _did_shoot = false
+ return GUIDETriggerState.NONE
+
+
+func _editor_name() -> String:
+ return "Hold"
+
+func _editor_description() -> String:
+ return "Fires, once the input has remained actuated for hold_threshold seconds.\n" + \
+ "My fire once or repeatedly."
diff --git a/addons/guide/triggers/guide_trigger_pressed.gd b/addons/guide/triggers/guide_trigger_pressed.gd
new file mode 100644
index 0000000..97fd0e1
--- /dev/null
+++ b/addons/guide/triggers/guide_trigger_pressed.gd
@@ -0,0 +1,22 @@
+@tool
+## A trigger that activates when the input is pushed down. Will only emit a
+## trigger event once. Holding the input will not trigger further events.
+class_name GUIDETriggerPressed
+extends GUIDETrigger
+
+
+func _update_state(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> GUIDETriggerState:
+ if _is_actuated(input, value_type):
+ if not _is_actuated(_last_value, value_type):
+ return GUIDETriggerState.TRIGGERED
+
+ return GUIDETriggerState.NONE
+
+
+func _editor_name() -> String:
+ return "Pressed"
+
+
+func _editor_description() -> String:
+ return "Fires once, when the input exceeds actuation threshold. Holding the input\n" + \
+ "will not fire additional triggers."
diff --git a/addons/guide/triggers/guide_trigger_pulse.gd b/addons/guide/triggers/guide_trigger_pulse.gd
new file mode 100644
index 0000000..61ee88c
--- /dev/null
+++ b/addons/guide/triggers/guide_trigger_pulse.gd
@@ -0,0 +1,86 @@
+@tool
+## A trigger that activates when the input is pushed down and then repeatedly sends trigger events at a fixed interval.
+## Note: the trigger will be either triggering or ongoing until the input is released.
+## Note: at most one pulse will be emitted per frame.
+class_name GUIDETriggerPulse
+extends GUIDETrigger
+
+## If true, the trigger will trigger immediately when the input is actuated. Otherwise, the trigger will wait for the initial delay.
+@export var trigger_on_start:bool = true
+## The delay after the initial actuation before pulsing begins.
+@export var initial_delay:float = 0.3:
+ set(value):
+ initial_delay = max(0, value)
+
+## The interval between pulses. Set to 0 to pulse every frame.
+@export var pulse_interval:float = 0.1:
+ set(value):
+ pulse_interval = max(0, value)
+
+## Maximum number of pulses. If <= 0, the trigger will pulse indefinitely.
+@export var max_pulses:int = 0
+
+var _delay_until_next_pulse:float = 0
+var _emitted_pulses:int = 0
+
+func _update_state(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> GUIDETriggerState:
+ if _is_actuated(input, value_type):
+ if not _is_actuated(_last_value, value_type):
+ # we went from "not actuated" to actuated, pulsing starts
+ _delay_until_next_pulse = initial_delay
+ if trigger_on_start:
+ return GUIDETriggerState.TRIGGERED
+ else:
+ return GUIDETriggerState.ONGOING
+
+ # if we already are pulsing and have exceeded the maximum number of pulses, we will not pulse anymore.
+ if max_pulses > 0 and _emitted_pulses >= max_pulses:
+ return GUIDETriggerState.NONE
+
+ # subtract the delta from the delay until the next pulse
+ _delay_until_next_pulse -= delta
+
+ if _delay_until_next_pulse > 0:
+ # we are still waiting for the next pulse, nothing to do.
+ return GUIDETriggerState.ONGOING
+
+ # now delta could be larger than our pulse, in which case we loose a few pulses.
+ # as we can pulse at most once per frame.
+
+ # in case someone sets the pulse interval to 0, we will pulse every frame.
+ if is_equal_approx(pulse_interval, 0):
+ _delay_until_next_pulse = 0
+ if max_pulses > 0:
+ _emitted_pulses += 1
+ return GUIDETriggerState.TRIGGERED
+
+ # Now add the delay until the next pulse
+ _delay_until_next_pulse += pulse_interval
+
+ # If the interval is really small, we can potentially have skipped some pulses
+ if _delay_until_next_pulse <= 0:
+ # we have skipped some pulses
+ var skipped_pulses:int = int(-_delay_until_next_pulse / pulse_interval)
+ _delay_until_next_pulse += skipped_pulses * pulse_interval
+ if max_pulses > 0:
+ _emitted_pulses += skipped_pulses
+ if _emitted_pulses >= max_pulses:
+ return GUIDETriggerState.NONE
+
+ # Record a pulse and return triggered
+ if max_pulses > 0:
+ _emitted_pulses += 1
+ return GUIDETriggerState.TRIGGERED
+
+ # if the input is not actuated, then the trigger is not triggered.
+ _emitted_pulses = 0
+ _delay_until_next_pulse = 0
+ return GUIDETriggerState.NONE
+
+
+func _editor_name() -> String:
+ return "Pulse"
+
+
+func _editor_description() -> String:
+ return "Fires at an interval while the input is actuated."
diff --git a/addons/guide/triggers/guide_trigger_released.gd b/addons/guide/triggers/guide_trigger_released.gd
new file mode 100644
index 0000000..d1509e9
--- /dev/null
+++ b/addons/guide/triggers/guide_trigger_released.gd
@@ -0,0 +1,21 @@
+@tool
+## A trigger that activates when the input is released down. Will only emit a
+## trigger event once.
+class_name GUIDETriggerReleased
+extends GUIDETrigger
+
+
+func _update_state(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> GUIDETriggerState:
+ if not _is_actuated(input, value_type):
+ if _is_actuated(_last_value, value_type):
+ return GUIDETriggerState.TRIGGERED
+
+ return GUIDETriggerState.NONE
+
+
+func _editor_name() -> String:
+ return "Released"
+
+
+func _editor_description() -> String:
+ return "Fires once, when the input goes from actuated to not actuated. The opposite of the Pressed trigger."
diff --git a/addons/guide/triggers/guide_trigger_stability.gd b/addons/guide/triggers/guide_trigger_stability.gd
new file mode 100644
index 0000000..cfe01f9
--- /dev/null
+++ b/addons/guide/triggers/guide_trigger_stability.gd
@@ -0,0 +1,72 @@
+@tool
+## Triggers depending on whether the input changes while actuated. This trigger is
+## is implicit, so it must succeed for all other triggers to succeed.
+class_name GUIDETriggerStability
+extends GUIDETrigger
+
+enum TriggerWhen {
+ ## Input must be stable
+ INPUT_IS_STABLE,
+ ## Input must change
+ INPUT_CHANGES
+}
+
+
+## The maximum amount that the input can change after actuation before it is
+## considered "changed".
+@export var max_deviation:float = 1
+
+## When should the trigger trigger?
+@export var trigger_when:TriggerWhen = TriggerWhen.INPUT_IS_STABLE
+
+
+var _initial_value:Vector3
+var _deviated:bool = false
+
+
+func _get_trigger_type() -> GUIDETriggerType:
+ return GUIDETriggerType.IMPLICIT
+
+
+func _update_state(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> GUIDETriggerState:
+ if _is_actuated(input, value_type):
+ if _deviated:
+ if trigger_when == TriggerWhen.INPUT_IS_STABLE:
+ return GUIDETriggerState.NONE
+ return GUIDETriggerState.TRIGGERED
+
+
+ if not _is_actuated(_last_value, value_type):
+ # we went from "not actuated" to actuated, start
+ _initial_value = input
+ if trigger_when == TriggerWhen.INPUT_IS_STABLE:
+ return GUIDETriggerState.TRIGGERED
+ else:
+ return GUIDETriggerState.ONGOING
+
+ # calculate how far the input is from the initial value
+ if _initial_value.distance_squared_to(input) > (max_deviation * max_deviation):
+ _deviated = true
+ if trigger_when == TriggerWhen.INPUT_IS_STABLE:
+ return GUIDETriggerState.NONE
+ return GUIDETriggerState.TRIGGERED
+
+ if trigger_when == TriggerWhen.INPUT_IS_STABLE:
+ return GUIDETriggerState.TRIGGERED
+
+ return GUIDETriggerState.ONGOING
+
+ # if the input is not actuated
+ _deviated = false
+ return GUIDETriggerState.NONE
+
+
+
+
+func _editor_name() -> String:
+ return "Stability"
+
+
+func _editor_description() -> String:
+ return "Triggers depending on whether the input changes while actuated. This trigger\n" +\
+ "is implicit, so it must succeed for all other triggers to succeed."
diff --git a/addons/guide/triggers/guide_trigger_tap.gd b/addons/guide/triggers/guide_trigger_tap.gd
new file mode 100644
index 0000000..a0d0c26
--- /dev/null
+++ b/addons/guide/triggers/guide_trigger_tap.gd
@@ -0,0 +1,48 @@
+@tool
+## A trigger that activates when the input is tapped and released before the time threshold is reached.
+class_name GUIDETriggerTap
+extends GUIDETrigger
+
+## The time threshold for the tap to be considered a tap.
+@export var tap_threshold: float = 0.2
+
+var _accumulated_time: float = 0
+
+
+func _update_state(input: Vector3, delta: float, value_type:GUIDEAction.GUIDEActionValueType) -> GUIDETriggerState:
+ if _is_actuated(input, value_type):
+ # if the input was actuated before, and the tap threshold has been exceeded, the trigger is locked down
+ # until the input is released and we can exit out early
+ if _is_actuated(_last_value, value_type) and _accumulated_time > tap_threshold:
+ return GUIDETriggerState.NONE
+
+ # accumulate time
+ _accumulated_time += delta
+
+ if _accumulated_time < tap_threshold:
+ return GUIDETriggerState.ONGOING
+ else:
+ # we have exceeded the tap threshold, so the tap is not triggered.
+ return GUIDETriggerState.NONE
+
+ else: # not actuated right now
+ # if the input was actuated before...
+ if _is_actuated(_last_value, value_type):
+ # ... and the accumulated time is less than the threshold, then the tap is triggered.
+ if _accumulated_time < tap_threshold:
+ _accumulated_time = 0
+ return GUIDETriggerState.TRIGGERED
+
+ # Otherwise, the tap is not triggered, but we reset the accumulated time
+ # so the trigger is now again ready to be triggered.
+ _accumulated_time = 0
+
+ # in either case, the trigger is not triggered.
+ return GUIDETriggerState.NONE
+
+func _editor_name() -> String:
+ return "Tap"
+
+
+func _editor_description() -> String:
+ return "Fires when the input is actuated and released within the given timeframe."
diff --git a/addons/guide/ui/guide_icon_renderer.gd b/addons/guide/ui/guide_icon_renderer.gd
new file mode 100644
index 0000000..558303e
--- /dev/null
+++ b/addons/guide/ui/guide_icon_renderer.gd
@@ -0,0 +1,30 @@
+## Base class for icon renderers. Note that all icon renderers must be tool
+## scripts.
+@tool
+class_name GUIDEIconRenderer
+extends Control
+
+## The priority of this icon renderer. Built-in renderers use priority 0. Built-in
+## fallback renderer uses priority 100. The smaller the number the higher the priority.
+@export var priority:int = 0
+
+## Whether or not this renderer can render an icon for this input.
+func supports(input:GUIDEInput) -> bool:
+ return false
+
+## Set up the scene so that the given input can be rendered. This will
+## only be called for input where `supports` has returned true.
+func render(input:GUIDEInput) -> void:
+ pass
+
+
+## A cache key for the given input. This should be unique for this renderer
+## and the given input. The same input should yield the same cache key for
+## each renderer.
+func cache_key(input:GUIDEInput) -> String:
+ push_error("Custom renderers must override the cache_key function to ensure proper caching.")
+ return "i-forgot-the-cache-key"
+
+func _ready():
+ process_mode = Node.PROCESS_MODE_ALWAYS
+
diff --git a/addons/guide/ui/guide_input_formatter.gd b/addons/guide/ui/guide_input_formatter.gd
new file mode 100644
index 0000000..1fd670c
--- /dev/null
+++ b/addons/guide/ui/guide_input_formatter.gd
@@ -0,0 +1,358 @@
+@tool
+## Helper class for formatting GUIDE input for the UI.
+class_name GUIDEInputFormatter
+
+const IconMaker = preload("icon_maker/icon_maker.gd")
+const KeyRenderer = preload("renderers/keyboard/key_renderer.tscn")
+const MouseRenderer = preload("renderers/mouse/mouse_renderer.tscn")
+const TouchRenderer = preload("renderers/touch/touch_renderer.tscn")
+const JoyRenderer = preload("renderers/joy/joy_renderer.tscn")
+const XboxRenderer = preload("renderers/controllers/xbox/xbox_controller_renderer.tscn")
+const PlayStationRenderer = preload("renderers/controllers/playstation/playstation_controller_renderer.tscn")
+const SwitchRenderer = preload("renderers/controllers/switch/switch_controller_renderer.tscn")
+const ActionRenderer = preload("renderers/misc/action_renderer.tscn")
+const FallbackRenderer = preload("renderers/misc/fallback_renderer.tscn")
+const DefaultTextProvider = preload("text_providers/default_text_provider.gd")
+const XboxTextProvider = preload("text_providers/controllers/xbox/xbox_controller_text_provider.gd")
+const PlayStationTextProvider = preload("text_providers/controllers/playstation/playstation_controller_text_provider.gd")
+const SwitchTextProvider = preload("text_providers/controllers/switch/switch_controller_text_provider.gd")
+
+# These are shared across all instances
+static var _icon_maker:IconMaker
+static var _icon_renderers:Array[GUIDEIconRenderer] = []
+static var _text_providers:Array[GUIDETextProvider] = []
+static var _is_ready:bool = false
+
+## Separator to separate mixed input.
+static var mixed_input_separator:String = ", "
+## Separator to separate chorded input.
+static var chorded_input_separator:String = " + "
+## Separator to separate combo input.
+static var combo_input_separator:String = " > "
+
+# These are per-instance
+var _action_resolver:Callable
+var _icon_size:int
+
+static func _ensure_readiness():
+ if _is_ready:
+ return
+
+ # reconnect to an icon maker that might be there
+ var root = Engine.get_main_loop().root
+ for child in root.get_children():
+ if child is IconMaker:
+ _icon_maker = child
+
+ if _icon_maker == null:
+ _icon_maker = preload("icon_maker/icon_maker.tscn").instantiate()
+ root.add_child.call_deferred(_icon_maker)
+
+ add_icon_renderer(KeyRenderer.instantiate())
+ add_icon_renderer(MouseRenderer.instantiate())
+ add_icon_renderer(TouchRenderer.instantiate())
+ add_icon_renderer(ActionRenderer.instantiate())
+ add_icon_renderer(JoyRenderer.instantiate())
+ add_icon_renderer(XboxRenderer.instantiate())
+ add_icon_renderer(PlayStationRenderer.instantiate())
+ add_icon_renderer(SwitchRenderer.instantiate())
+ add_icon_renderer(FallbackRenderer.instantiate())
+
+ add_text_provider(DefaultTextProvider.new())
+ add_text_provider(XboxTextProvider.new())
+ add_text_provider(PlayStationTextProvider.new())
+ add_text_provider(SwitchTextProvider.new())
+
+ _is_ready = true
+
+
+## This will clean up the rendering infrastructure used for generating
+## icons. Note that in a normal game you will have no need to call this
+## as the infrastructure is needed throughout the run of your game.
+## It might be useful in tests though, to get rid of spurious warnings
+## about orphaned nodes.
+static func cleanup():
+ _is_ready = false
+
+ # free all the nodes to avoid memory leaks
+ for renderer in _icon_renderers:
+ renderer.queue_free()
+
+ _icon_renderers.clear()
+
+ _text_providers.clear()
+ if is_instance_valid(_icon_maker):
+ _icon_maker.queue_free()
+
+
+func _init(icon_size:int = 32, resolver:Callable = func(action) -> GUIDEActionMapping: return null ):
+ _icon_size = icon_size
+ _action_resolver = resolver
+
+
+## Adds an icon renderer for rendering icons.
+static func add_icon_renderer(renderer:GUIDEIconRenderer) -> void:
+ _icon_renderers.append(renderer)
+ _icon_renderers.sort_custom(func(r1, r2): return r1.priority < r2.priority)
+
+## Removes an icon renderer.
+static func remove_icon_renderer(renderer:GUIDEIconRenderer) -> void:
+ _icon_renderers.erase(renderer)
+
+## Adds a text provider for rendering text.
+static func add_text_provider(provider:GUIDETextProvider) -> void:
+ _text_providers.append(provider)
+ _text_providers.sort_custom(func(r1, r2): return r1.priority < r2.priority)
+
+
+## Removes a text provider
+static func remove_text_provider(provider:GUIDETextProvider) -> void:
+ _text_providers.erase(provider)
+
+
+## Returns an input formatter that can format actions using the currently active inputs.
+static func for_active_contexts(icon_size:int = 32) -> GUIDEInputFormatter:
+ var resolver = func(action:GUIDEAction) -> GUIDEActionMapping:
+ for mapping in GUIDE._active_action_mappings:
+ if mapping.action == action:
+ return mapping
+ return null
+ return GUIDEInputFormatter.new(icon_size, resolver)
+
+
+## Returns an input formatter that can format actions using the given context.
+static func for_context(context:GUIDEMappingContext, icon_size:int = 32) -> GUIDEInputFormatter:
+ var resolver:Callable = func(action:GUIDEAction) -> GUIDEActionMapping:
+ for mapping in context.mappings:
+ if mapping.action == action:
+ return mapping
+ return null
+
+ return GUIDEInputFormatter.new(icon_size, resolver)
+
+
+## Formats the action input as richtext with icons suitable for a RichTextLabel. This function
+## is async as icons may need to be rendered in the background which can take a few frames, so
+## you will need to await on it.
+func action_as_richtext_async(action:GUIDEAction) -> String:
+ return await _materialized_as_richtext_async(_materialize_action_input(action))
+
+
+## Formats the action input as plain text which can be used in any UI component. This is a bit
+## more light-weight than formatting as icons and returns immediately.
+func action_as_text(action:GUIDEAction) -> String:
+ return _materialized_as_text(_materialize_action_input(action))
+
+## Formats the input as richtext with icons suitable for a RichTextLabel. This function
+## is async as icons may need to be rendered in the background which can take a few frames, so
+## you will need to await on it.
+func input_as_richtext_async(input:GUIDEInput, materialize_actions:bool = true) -> String:
+ return await _materialized_as_richtext_async(_materialize_input(input, materialize_actions))
+
+
+## Formats the input as plain text which can be used in any UI component. This is a bit
+## more light-weight than formatting as icons and returns immediately.
+func input_as_text(input:GUIDEInput, materialize_actions:bool = true) -> String:
+ return _materialized_as_text(_materialize_input(input, materialize_actions))
+
+
+## Renders materialized input as text.
+func _materialized_as_text(input:MaterializedInput) -> String:
+ _ensure_readiness()
+ if input is MaterializedSimpleInput:
+ var text:String = ""
+ for provider in _text_providers:
+ if provider.supports(input.input):
+ text = provider.get_text(input.input)
+ # first provider wins
+ break
+ if text == "":
+ pass
+ ## push_warning("No formatter found for input ", input)
+ return text
+
+ var separator = _separator_for_input(input)
+ if separator == "" or input.parts.is_empty():
+ return ""
+
+ var parts:Array[String] = []
+ for part in input.parts:
+ parts.append(_materialized_as_text(part))
+
+ return separator.join(parts)
+
+## Renders materialized input as rich text.
+func _materialized_as_richtext_async(input:MaterializedInput) -> String:
+ _ensure_readiness()
+ if input is MaterializedSimpleInput:
+ var icon:Texture2D = null
+ for renderer in _icon_renderers:
+ if renderer.supports(input.input):
+ icon = await _icon_maker.make_icon(input.input, renderer, _icon_size)
+ # first renderer wins
+ break
+ if icon == null:
+ push_warning("No renderer found for input ", input)
+ return ""
+
+ return "[img]%s[/img]" % [icon.resource_path]
+
+
+ var separator = _separator_for_input(input)
+ if separator == "" or input.parts.is_empty():
+ return ""
+
+ var parts:Array[String] = []
+ for part in input.parts:
+ parts.append(await _materialized_as_richtext_async(part))
+
+ return separator.join(parts)
+
+
+func _separator_for_input(input:MaterializedInput) -> String:
+ if input is MaterializedMixedInput:
+ return mixed_input_separator
+ elif input is MaterializedComboInput:
+ return combo_input_separator
+ elif input is MaterializedChordedInput:
+ return chorded_input_separator
+
+ push_error("Unknown materialized input type")
+ return ""
+
+
+## Materializes action input.
+func _materialize_action_input(action:GUIDEAction) -> MaterializedInput:
+ var result := MaterializedMixedInput.new()
+ if action == null:
+ push_warning("Trying to get inputs for a null action.")
+ return result
+
+ # get the mapping for this action
+ var mapping:GUIDEActionMapping = _action_resolver.call(action)
+
+ # if we have no mapping, well that's it, return an empty mixed input
+ if mapping == null:
+ return result
+
+ # collect input mappings
+ for input_mapping in mapping.input_mappings:
+ var chorded_actions:Array[MaterializedInput] = []
+ var combos:Array[MaterializedInput] = []
+
+ for trigger in input_mapping.triggers:
+ # if we have a combo trigger, materialize its input.
+ if trigger is GUIDETriggerCombo:
+ var combo := MaterializedComboInput.new()
+ for step:GUIDETriggerComboStep in trigger.steps:
+ combo.parts.append(_materialize_action_input(step.action))
+ combos.append(combo)
+
+ # if we have a chorded action, materialize its input
+ if trigger is GUIDETriggerChordedAction:
+ chorded_actions.append(_materialize_action_input(trigger.action))
+
+ if not chorded_actions.is_empty():
+ # if we have chorded action then the whole mapping is chorded.
+ var chord := MaterializedChordedInput.new()
+ for chorded_action in chorded_actions:
+ chord.parts.append(chorded_action)
+ for combo in combos:
+ chord.parts.append(combo)
+ if combos.is_empty():
+ if input_mapping.input != null:
+ chord.parts.append(_materialize_input(input_mapping.input))
+ result.parts.append(chord)
+ else:
+ for combo in combos:
+ result.parts.append(combo)
+ if combos.is_empty():
+ if input_mapping.input != null:
+ result.parts.append(_materialize_input(input_mapping.input))
+ return result
+
+## Materializes direct input.
+func _materialize_input(input:GUIDEInput, materialize_actions:bool = true) -> MaterializedInput:
+ if input == null:
+ push_warning("Trying to materialize a null input.")
+ return MaterializedMixedInput.new()
+
+ # if its an action input, get its parts
+ if input is GUIDEInputAction:
+ if materialize_actions:
+ return _materialize_action_input(input.action)
+ else:
+ return MaterializedSimpleInput.new(input)
+
+ # if its a key input, split out the modifiers
+ if input is GUIDEInputKey:
+ var chord := MaterializedChordedInput.new()
+ if input.control:
+ var ctrl = GUIDEInputKey.new()
+ ctrl.key = KEY_CTRL
+ chord.parts.append(MaterializedSimpleInput.new(ctrl))
+ if input.alt:
+ var alt = GUIDEInputKey.new()
+ alt.key = KEY_ALT
+ chord.parts.append(MaterializedSimpleInput.new(alt))
+ if input.shift:
+ var shift = GUIDEInputKey.new()
+ shift.key = KEY_SHIFT
+ chord.parts.append(MaterializedSimpleInput.new(shift))
+ if input.meta:
+ var meta = GUIDEInputKey.new()
+ meta.key = KEY_META
+ chord.parts.append(MaterializedSimpleInput.new(meta))
+
+ # got no modifiers?
+ if chord.parts.is_empty():
+ return MaterializedSimpleInput.new(input)
+
+ chord.parts.append(MaterializedSimpleInput.new(input))
+ return chord
+
+ # everything else is just a simple input
+ return MaterializedSimpleInput.new(input)
+
+class MaterializedInput:
+ pass
+
+class MaterializedSimpleInput:
+ extends MaterializedInput
+ var input:GUIDEInput
+
+ func _init(input:GUIDEInput):
+ self.input = input
+
+class MaterializedMixedInput:
+ extends MaterializedInput
+ var parts:Array[MaterializedInput] = []
+
+class MaterializedChordedInput:
+ extends MaterializedInput
+ var parts:Array[MaterializedInput] = []
+
+class MaterializedComboInput:
+ extends MaterializedInput
+ var parts:Array[MaterializedInput] = []
+
+
+## Returns the name of the associated joystick/pad of the given input.
+## If the input is no joy input or the device name cannot be determined
+## returns an empty string.
+static func _joy_name_for_input(input:GUIDEInput) -> String:
+ if not input is GUIDEInputJoyBase:
+ return ""
+
+ var joypads:Array[int] = Input.get_connected_joypads()
+ var joy_index = input.joy_index
+ if joy_index < 0:
+ # pick the first one
+ joy_index = 0
+
+ # We don't have such a controller, so bail out.
+ if joypads.size() <= joy_index:
+ return ""
+
+ var id = joypads[joy_index]
+ return Input.get_joy_name(id)
diff --git a/addons/guide/ui/guide_text_provider.gd b/addons/guide/ui/guide_text_provider.gd
new file mode 100644
index 0000000..2f84075
--- /dev/null
+++ b/addons/guide/ui/guide_text_provider.gd
@@ -0,0 +1,22 @@
+## Base class for text providers. A text provider provides a textual representation
+## of an input which is displayed to the user.
+## scripts.
+@tool
+class_name GUIDETextProvider
+
+## The priority of this text provider. The built-in text provider uses priority 0.
+## The smaller the number the higher the priority.
+@export var priority:int = 0
+
+## Whether or not this provider can provide a text for this input.
+func supports(input:GUIDEInput) -> bool:
+ return false
+
+## Provides the text for the given input. Will only be called when the
+## input is supported by this text provider. Note that for key input
+## this is not supposed to look at the modifiers. This function will
+## be called separately for each modifier.
+func get_text(input:GUIDEInput) -> String:
+ return "not implemented"
+
+
diff --git a/addons/guide/ui/icon_maker/icon_maker.gd b/addons/guide/ui/icon_maker/icon_maker.gd
new file mode 100644
index 0000000..1a03a0d
--- /dev/null
+++ b/addons/guide/ui/icon_maker/icon_maker.gd
@@ -0,0 +1,103 @@
+@tool
+extends Node
+
+const CACHE_DIR:String = "user://_guide_cache"
+
+@onready var _sub_viewport:SubViewport = %SubViewport
+@onready var _root:Node2D = %Root
+@onready var _scene_holder = %SceneHolder
+
+var _pending_requests:Array[Job] = []
+var _current_request:Job = null
+var _fetch_image:bool = false
+
+func _ready():
+ # keep working when game is paused
+ process_mode = Node.PROCESS_MODE_ALWAYS
+ # don't needlessly eat performance
+ if _pending_requests.is_empty():
+ set_process(false)
+
+
+func clear_cache():
+ var files = DirAccess.get_files_at(CACHE_DIR)
+ for file in files:
+ DirAccess.remove_absolute(CACHE_DIR + "/" + file)
+
+## Makes an icon for the given input and returns a Texture2D with the icon. Icons
+## are cached on disk so subsequent calls for the same input will be faster.
+func make_icon(input:GUIDEInput, renderer:GUIDEIconRenderer, height_px:int) -> Texture2D:
+ DirAccess.make_dir_recursive_absolute(CACHE_DIR)
+ var cache_key = (str(height_px) + renderer.cache_key(input)).sha256_text()
+ var cache_path = "user://_guide_cache/" + cache_key + ".res"
+ if ResourceLoader.exists(cache_path):
+ return ResourceLoader.load(cache_path, "Texture2D")
+
+ var job = Job.new()
+ job.height = height_px
+ job.input = input
+ job.renderer = renderer
+ _pending_requests.append(job)
+ set_process(true)
+
+ await job.done
+
+ var image_texture = ImageTexture.create_from_image(job.result)
+ ResourceSaver.save(image_texture, cache_path)
+ image_texture.take_over_path(cache_path)
+
+ return image_texture
+
+
+
+func _process(delta):
+ if _current_request == null and _pending_requests.is_empty():
+ # nothing more to do..
+ set_process(false)
+ return
+
+ # nothing in progress, so pick the next request
+ if _current_request == null:
+ _current_request = _pending_requests.pop_front()
+ var renderer = _current_request.renderer
+ _root.add_child(renderer)
+
+ renderer.render(_current_request.input)
+ await get_tree().process_frame
+
+ var actual_size = renderer.get_rect().size
+ var scale = float(_current_request.height) / float(actual_size.y)
+ _root.scale = Vector2.ONE * scale
+ _sub_viewport.size = actual_size * scale
+
+ _sub_viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
+
+ # give the renderer some time to update itself. 3 frames seem
+ # to work nicely and keep things speedy.
+ await get_tree().process_frame
+ await get_tree().process_frame
+ await get_tree().process_frame
+
+ _fetch_image = true
+ return
+
+ # fetch the image after the renderer is done
+ if _fetch_image:
+ # we're done. make a copy of the viewport texture
+ var image:Image = _scene_holder.texture.get_image()
+ _current_request.result = image
+ _current_request.done.emit()
+ _current_request = null
+ # remove the renderer
+ _root.remove_child(_root.get_child(0))
+ _sub_viewport.render_target_update_mode = SubViewport.UPDATE_DISABLED
+ _fetch_image = false
+
+class Job:
+ signal done()
+ var height:int
+ var input:GUIDEInput
+ var renderer:GUIDEIconRenderer
+ var result:Image
+
+
diff --git a/addons/guide/ui/icon_maker/icon_maker.tscn b/addons/guide/ui/icon_maker/icon_maker.tscn
new file mode 100644
index 0000000..662d2ce
--- /dev/null
+++ b/addons/guide/ui/icon_maker/icon_maker.tscn
@@ -0,0 +1,24 @@
+[gd_scene load_steps=3 format=3 uid="uid://8thurteeibtu"]
+
+[ext_resource type="Script" path="res://addons/guide/ui/icon_maker/icon_maker.gd" id="1_hdbjk"]
+
+[sub_resource type="ViewportTexture" id="ViewportTexture_kra7t"]
+viewport_path = NodePath("SubViewport")
+
+[node name="GUIDEIconMaker" type="Node2D"]
+script = ExtResource("1_hdbjk")
+
+[node name="SubViewport" type="SubViewport" parent="."]
+unique_name_in_owner = true
+transparent_bg = true
+gui_disable_input = true
+gui_snap_controls_to_pixels = false
+
+[node name="Root" type="Node2D" parent="SubViewport"]
+unique_name_in_owner = true
+scale = Vector2(0.1, 0.1)
+
+[node name="SceneHolder" type="Sprite2D" parent="."]
+unique_name_in_owner = true
+visible = false
+texture = SubResource("ViewportTexture_kra7t")
diff --git a/addons/guide/ui/renderers/controllers/controller_renderer.gd b/addons/guide/ui/renderers/controllers/controller_renderer.gd
new file mode 100644
index 0000000..96b6fdc
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/controller_renderer.gd
@@ -0,0 +1,171 @@
+@tool
+extends GUIDEIconRenderer
+
+@export var controller_name_matches:Array[String] = []
+@export var a_button:Texture2D
+@export var b_button:Texture2D
+@export var x_button:Texture2D
+@export var y_button:Texture2D
+@export var left_stick:Texture2D
+@export var left_stick_click:Texture2D
+@export var right_stick:Texture2D
+@export var right_stick_click:Texture2D
+@export var left_bumper:Texture2D
+@export var right_bumper:Texture2D
+@export var left_trigger:Texture2D
+@export var right_trigger:Texture2D
+@export var dpad_up:Texture2D
+@export var dpad_left:Texture2D
+@export var dpad_right:Texture2D
+@export var dpad_down:Texture2D
+@export var start:Texture2D
+@export var misc1:Texture2D
+@export var back:Texture2D
+
+
+@onready var _a_button:TextureRect = %AButton
+@onready var _b_button:TextureRect = %BButton
+@onready var _x_button:TextureRect = %XButton
+@onready var _y_button:TextureRect = %YButton
+@onready var _left_stick:TextureRect = %LeftStick
+@onready var _left_stick_click:TextureRect = %LeftStickClick
+@onready var _right_stick:TextureRect = %RightStick
+@onready var _right_stick_click:TextureRect = %RightStickClick
+@onready var _left_bumper:Control = %LeftBumper
+@onready var _right_bumper:Control = %RightBumper
+@onready var _left_trigger:Control = %LeftTrigger
+@onready var _right_trigger:TextureRect = %RightTrigger
+@onready var _dpad_up:TextureRect = %DpadUp
+@onready var _dpad_left:TextureRect = %DpadLeft
+@onready var _dpad_right:TextureRect = %DpadRight
+@onready var _dpad_down:TextureRect = %DpadDown
+@onready var _start:TextureRect = %Start
+@onready var _misc1:TextureRect = %Misc1
+@onready var _back:TextureRect = %Back
+@onready var _left_right:Control = %LeftRight
+@onready var _up_down:Control = %UpDown
+@onready var _controls:Control = %Controls
+@onready var _directions:Control = %Directions
+
+
+func _ready():
+ super()
+ _a_button.texture = a_button
+ _b_button.texture = b_button
+ _x_button.texture = x_button
+ _y_button.texture = y_button
+ _left_stick.texture = left_stick
+ _left_stick_click.texture = left_stick_click
+ _right_stick.texture = right_stick
+ _right_stick_click.texture = right_stick_click
+ _left_bumper.texture = left_bumper
+ _right_bumper.texture = right_bumper
+ _left_trigger.texture = left_trigger
+ _right_trigger.texture = right_trigger
+ _dpad_up.texture = dpad_up
+ _dpad_left.texture = dpad_left
+ _dpad_right.texture = dpad_right
+ _dpad_down.texture = dpad_down
+ _start.texture = start
+ _misc1.texture = misc1
+ _back.texture = back
+
+func supports(input:GUIDEInput) -> bool:
+ var joy_name = GUIDEInputFormatter._joy_name_for_input(input)
+ if joy_name == "":
+ return false
+
+ # Look if the controller name matches one of the supported ones
+ var haystack = joy_name.to_lower()
+ for needle in controller_name_matches:
+ if haystack.contains(needle.to_lower()):
+ return true
+
+ return false
+
+func render(input:GUIDEInput) -> void:
+ for control in _controls.get_children():
+ control.visible = false
+ for direction in _directions.get_children():
+ direction.visible = false
+ _directions.visible = false
+
+
+ if input is GUIDEInputJoyAxis1D:
+ match input.axis:
+ JOY_AXIS_LEFT_X:
+ _left_stick.visible = true
+ _show_left_right()
+ JOY_AXIS_LEFT_Y:
+ _left_stick.visible = true
+ _show_up_down()
+ JOY_AXIS_RIGHT_X:
+ _right_stick.visible = true
+ _show_left_right()
+ JOY_AXIS_RIGHT_Y:
+ _right_stick.visible = true
+ _show_up_down()
+ JOY_AXIS_TRIGGER_LEFT:
+ _left_trigger.visible = true
+ JOY_AXIS_TRIGGER_RIGHT:
+ _right_trigger.visible = true
+
+ if input is GUIDEInputJoyAxis2D:
+ # We assume that there is no input mixing horizontal and vertical
+ # from different sticks into a 2D axis as this would confuse the
+ # players.
+ match input.x:
+ JOY_AXIS_LEFT_X, JOY_AXIS_LEFT_Y:
+ _left_stick.visible = true
+ JOY_AXIS_RIGHT_X, JOY_AXIS_RIGHT_Y:
+ _right_stick.visible = true
+
+ if input is GUIDEInputJoyButton:
+ match input.button:
+ JOY_BUTTON_A:
+ _a_button.visible = true
+ JOY_BUTTON_B:
+ _b_button.visible = true
+ JOY_BUTTON_X:
+ _x_button.visible = true
+ JOY_BUTTON_Y:
+ _y_button.visible = true
+ JOY_BUTTON_DPAD_LEFT:
+ _dpad_left.visible = true
+ JOY_BUTTON_DPAD_RIGHT:
+ _dpad_right.visible = true
+ JOY_BUTTON_DPAD_UP:
+ _dpad_up.visible = true
+ JOY_BUTTON_DPAD_DOWN:
+ _dpad_down.visible = true
+ JOY_BUTTON_LEFT_SHOULDER:
+ _left_bumper.visible = true
+ JOY_BUTTON_RIGHT_SHOULDER:
+ _right_bumper.visible = true
+ JOY_BUTTON_LEFT_STICK:
+ _left_stick_click.visible = true
+ JOY_BUTTON_RIGHT_STICK:
+ _right_stick_click.visible = true
+ JOY_BUTTON_RIGHT_STICK:
+ _right_stick_click.visible = true
+ JOY_BUTTON_START:
+ _start.visible = true
+ JOY_BUTTON_BACK:
+ _back.visible = true
+ JOY_BUTTON_MISC1:
+ _misc1.visible = true
+
+ call("queue_sort")
+
+
+func _show_left_right():
+ _directions.visible = true
+ _left_right.visible = true
+
+func _show_up_down():
+ _directions.visible = true
+ _up_down.visible = true
+
+
+func cache_key(input:GUIDEInput) -> String:
+ return "7581f483-bc68-411f-98ad-dc246fd2593a" + input.to_string() + GUIDEInputFormatter._joy_name_for_input(input)
diff --git a/addons/guide/ui/renderers/controllers/controller_renderer.tscn b/addons/guide/ui/renderers/controllers/controller_renderer.tscn
new file mode 100644
index 0000000..9ab0538
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/controller_renderer.tscn
@@ -0,0 +1,135 @@
+[gd_scene load_steps=4 format=3 uid="uid://bsaylcb5ixjxk"]
+
+[ext_resource type="Script" path="res://addons/guide/ui/renderers/controllers/controller_renderer.gd" id="1_yt13e"]
+[ext_resource type="Texture2D" uid="uid://bmgxqbypegjxh" path="res://addons/guide/ui/renderers/textures/arrow_horizontal.svg" id="2_nv2ob"]
+[ext_resource type="Texture2D" uid="uid://bu5nlug6uf03w" path="res://addons/guide/ui/renderers/textures/arrow_vertical.svg" id="3_ejti1"]
+
+[node name="ControllerRenderer" type="MarginContainer"]
+offset_right = 100.0
+offset_bottom = 100.0
+size_flags_horizontal = 0
+script = ExtResource("1_yt13e")
+priority = -1
+
+[node name="HBoxContainer" type="HBoxContainer" parent="."]
+layout_mode = 2
+size_flags_horizontal = 0
+
+[node name="Controls" type="MarginContainer" parent="HBoxContainer"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(100, 100)
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="AButton" type="TextureRect" parent="HBoxContainer/Controls"]
+unique_name_in_owner = true
+layout_mode = 2
+stretch_mode = 5
+
+[node name="BButton" type="TextureRect" parent="HBoxContainer/Controls"]
+unique_name_in_owner = true
+layout_mode = 2
+stretch_mode = 5
+
+[node name="XButton" type="TextureRect" parent="HBoxContainer/Controls"]
+unique_name_in_owner = true
+layout_mode = 2
+stretch_mode = 5
+
+[node name="YButton" type="TextureRect" parent="HBoxContainer/Controls"]
+unique_name_in_owner = true
+layout_mode = 2
+stretch_mode = 5
+
+[node name="LeftStick" type="TextureRect" parent="HBoxContainer/Controls"]
+unique_name_in_owner = true
+layout_mode = 2
+stretch_mode = 5
+
+[node name="LeftStickClick" type="TextureRect" parent="HBoxContainer/Controls"]
+unique_name_in_owner = true
+layout_mode = 2
+stretch_mode = 5
+
+[node name="RightStick" type="TextureRect" parent="HBoxContainer/Controls"]
+unique_name_in_owner = true
+layout_mode = 2
+stretch_mode = 5
+
+[node name="RightStickClick" type="TextureRect" parent="HBoxContainer/Controls"]
+unique_name_in_owner = true
+layout_mode = 2
+stretch_mode = 5
+
+[node name="LeftBumper" type="TextureRect" parent="HBoxContainer/Controls"]
+unique_name_in_owner = true
+layout_mode = 2
+stretch_mode = 5
+
+[node name="RightBumper" type="TextureRect" parent="HBoxContainer/Controls"]
+unique_name_in_owner = true
+layout_mode = 2
+stretch_mode = 5
+
+[node name="LeftTrigger" type="TextureRect" parent="HBoxContainer/Controls"]
+unique_name_in_owner = true
+layout_mode = 2
+stretch_mode = 5
+
+[node name="RightTrigger" type="TextureRect" parent="HBoxContainer/Controls"]
+unique_name_in_owner = true
+layout_mode = 2
+stretch_mode = 5
+
+[node name="DpadUp" type="TextureRect" parent="HBoxContainer/Controls"]
+unique_name_in_owner = true
+layout_mode = 2
+stretch_mode = 5
+
+[node name="DpadLeft" type="TextureRect" parent="HBoxContainer/Controls"]
+unique_name_in_owner = true
+layout_mode = 2
+stretch_mode = 5
+
+[node name="DpadRight" type="TextureRect" parent="HBoxContainer/Controls"]
+unique_name_in_owner = true
+layout_mode = 2
+stretch_mode = 5
+
+[node name="DpadDown" type="TextureRect" parent="HBoxContainer/Controls"]
+unique_name_in_owner = true
+layout_mode = 2
+stretch_mode = 5
+
+[node name="Start" type="TextureRect" parent="HBoxContainer/Controls"]
+unique_name_in_owner = true
+layout_mode = 2
+stretch_mode = 5
+
+[node name="Misc1" type="TextureRect" parent="HBoxContainer/Controls"]
+unique_name_in_owner = true
+layout_mode = 2
+stretch_mode = 5
+
+[node name="Back" type="TextureRect" parent="HBoxContainer/Controls"]
+unique_name_in_owner = true
+layout_mode = 2
+stretch_mode = 5
+
+[node name="Directions" type="MarginContainer" parent="HBoxContainer"]
+unique_name_in_owner = true
+visible = false
+custom_minimum_size = Vector2(100, 100)
+layout_mode = 2
+
+[node name="LeftRight" type="TextureRect" parent="HBoxContainer/Directions"]
+unique_name_in_owner = true
+layout_mode = 2
+texture = ExtResource("2_nv2ob")
+stretch_mode = 5
+
+[node name="UpDown" type="TextureRect" parent="HBoxContainer/Directions"]
+unique_name_in_owner = true
+layout_mode = 2
+texture = ExtResource("3_ejti1")
+stretch_mode = 5
diff --git a/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Circle.png b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Circle.png
new file mode 100644
index 0000000..05a89a9
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Circle.png differ
diff --git a/examples/sharp_corner_tapering/assets/tex_metal_corner_inner.png.import b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Circle.png.import
similarity index 63%
rename from examples/sharp_corner_tapering/assets/tex_metal_corner_inner.png.import
rename to addons/guide/ui/renderers/controllers/playstation/icons/PS5_Circle.png.import
index 835818b..9aa8906 100644
--- a/examples/sharp_corner_tapering/assets/tex_metal_corner_inner.png.import
+++ b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Circle.png.import
@@ -2,16 +2,16 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://bobwi3r6aiiqg"
-path="res://.godot/imported/tex_metal_corner_inner.png-cdfd7b81c523bc5bb5c7863d7307007e.ctex"
+uid="uid://civpcnwgbu5ky"
+path="res://.godot/imported/PS5_Circle.png-991ec3d8ff387e8a1997f29928333c68.ctex"
metadata={
"vram_texture": false
}
[deps]
-source_file="res://examples/sharp_corner_tapering/assets/tex_metal_corner_inner.png"
-dest_files=["res://.godot/imported/tex_metal_corner_inner.png-cdfd7b81c523bc5bb5c7863d7307007e.ctex"]
+source_file="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_Circle.png"
+dest_files=["res://.godot/imported/PS5_Circle.png-991ec3d8ff387e8a1997f29928333c68.ctex"]
[params]
diff --git a/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Cross.png b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Cross.png
new file mode 100644
index 0000000..395a898
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Cross.png differ
diff --git a/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Cross.png.import b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Cross.png.import
new file mode 100644
index 0000000..f0a694d
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Cross.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cfy1rx4d4wjdh"
+path="res://.godot/imported/PS5_Cross.png-94e7143faf483eb3d6ca6505fc615cd3.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_Cross.png"
+dest_files=["res://.godot/imported/PS5_Cross.png-94e7143faf483eb3d6ca6505fc615cd3.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/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad.png b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad.png
new file mode 100644
index 0000000..49e6405
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad.png differ
diff --git a/examples/sharp_corner_tapering/assets/tex_metal_fill.png.import b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad.png.import
similarity index 65%
rename from examples/sharp_corner_tapering/assets/tex_metal_fill.png.import
rename to addons/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad.png.import
index 2b1eb9c..ed99363 100644
--- a/examples/sharp_corner_tapering/assets/tex_metal_fill.png.import
+++ b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad.png.import
@@ -2,16 +2,16 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://bj658oli0klj3"
-path="res://.godot/imported/tex_metal_fill.png-307d5ebcedc9e8c4f154a2c09687cb41.ctex"
+uid="uid://ubnurptd6ee2"
+path="res://.godot/imported/PS5_Dpad.png-ef26d9f78f150d4ab2b9e6bbe325f986.ctex"
metadata={
"vram_texture": false
}
[deps]
-source_file="res://examples/sharp_corner_tapering/assets/tex_metal_fill.png"
-dest_files=["res://.godot/imported/tex_metal_fill.png-307d5ebcedc9e8c4f154a2c09687cb41.ctex"]
+source_file="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad.png"
+dest_files=["res://.godot/imported/PS5_Dpad.png-ef26d9f78f150d4ab2b9e6bbe325f986.ctex"]
[params]
diff --git a/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad_Down.png b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad_Down.png
new file mode 100644
index 0000000..a8f893a
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad_Down.png differ
diff --git a/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad_Down.png.import b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad_Down.png.import
new file mode 100644
index 0000000..26ef580
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad_Down.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://vk1vje3280tk"
+path="res://.godot/imported/PS5_Dpad_Down.png-ba21ca6e311100c142d2b003152ea1d2.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad_Down.png"
+dest_files=["res://.godot/imported/PS5_Dpad_Down.png-ba21ca6e311100c142d2b003152ea1d2.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/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad_Left.png b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad_Left.png
new file mode 100644
index 0000000..2bdc048
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad_Left.png differ
diff --git a/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad_Left.png.import b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad_Left.png.import
new file mode 100644
index 0000000..9a31227
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad_Left.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bkpw61ctv0fbg"
+path="res://.godot/imported/PS5_Dpad_Left.png-bd78cf7c0092facc48bbf8fd7816f7a2.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad_Left.png"
+dest_files=["res://.godot/imported/PS5_Dpad_Left.png-bd78cf7c0092facc48bbf8fd7816f7a2.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/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad_Right.png b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad_Right.png
new file mode 100644
index 0000000..b7cd568
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad_Right.png differ
diff --git a/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad_Right.png.import b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad_Right.png.import
new file mode 100644
index 0000000..257b99c
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad_Right.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dybnayy8y7rxe"
+path="res://.godot/imported/PS5_Dpad_Right.png-064b9c5c42d22a9c2be3902ca2e33638.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad_Right.png"
+dest_files=["res://.godot/imported/PS5_Dpad_Right.png-064b9c5c42d22a9c2be3902ca2e33638.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/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad_Up.png b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad_Up.png
new file mode 100644
index 0000000..99180d2
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad_Up.png differ
diff --git a/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad_Up.png.import b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad_Up.png.import
new file mode 100644
index 0000000..bfa2c71
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad_Up.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bvbd876sy2430"
+path="res://.godot/imported/PS5_Dpad_Up.png-b8fc9319fe2231915e5e8e21174b1c1c.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad_Up.png"
+dest_files=["res://.godot/imported/PS5_Dpad_Up.png-b8fc9319fe2231915e5e8e21174b1c1c.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/guide/ui/renderers/controllers/playstation/icons/PS5_L1.png b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_L1.png
new file mode 100644
index 0000000..07e505a
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_L1.png differ
diff --git a/examples/sharp_corner_tapering/assets/tex_metal_edge.png.import b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_L1.png.import
similarity index 65%
rename from examples/sharp_corner_tapering/assets/tex_metal_edge.png.import
rename to addons/guide/ui/renderers/controllers/playstation/icons/PS5_L1.png.import
index 4e4b107..dd46c43 100644
--- a/examples/sharp_corner_tapering/assets/tex_metal_edge.png.import
+++ b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_L1.png.import
@@ -2,16 +2,16 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://b3aqa3bj1osvp"
-path="res://.godot/imported/tex_metal_edge.png-5aaf6e458197a953a7afeee0e270fea4.ctex"
+uid="uid://cqgpumb0tf5xr"
+path="res://.godot/imported/PS5_L1.png-daedbc1549c79d92cbcf68661193a3b8.ctex"
metadata={
"vram_texture": false
}
[deps]
-source_file="res://examples/sharp_corner_tapering/assets/tex_metal_edge.png"
-dest_files=["res://.godot/imported/tex_metal_edge.png-5aaf6e458197a953a7afeee0e270fea4.ctex"]
+source_file="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_L1.png"
+dest_files=["res://.godot/imported/PS5_L1.png-daedbc1549c79d92cbcf68661193a3b8.ctex"]
[params]
diff --git a/addons/guide/ui/renderers/controllers/playstation/icons/PS5_L2.png b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_L2.png
new file mode 100644
index 0000000..05f3dd2
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_L2.png differ
diff --git a/addons/guide/ui/renderers/controllers/playstation/icons/PS5_L2.png.import b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_L2.png.import
new file mode 100644
index 0000000..376d592
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_L2.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bhoi6nfung5ye"
+path="res://.godot/imported/PS5_L2.png-2ad86a3ad9afd70333db64063ae812ae.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_L2.png"
+dest_files=["res://.godot/imported/PS5_L2.png-2ad86a3ad9afd70333db64063ae812ae.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/guide/ui/renderers/controllers/playstation/icons/PS5_Left_Stick.png b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Left_Stick.png
new file mode 100644
index 0000000..0245ff8
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Left_Stick.png differ
diff --git a/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Left_Stick.png.import b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Left_Stick.png.import
new file mode 100644
index 0000000..55a4f30
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Left_Stick.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c3qet180o0dn6"
+path="res://.godot/imported/PS5_Left_Stick.png-472622a0a1752a811747d3e6c02f5438.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_Left_Stick.png"
+dest_files=["res://.godot/imported/PS5_Left_Stick.png-472622a0a1752a811747d3e6c02f5438.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/guide/ui/renderers/controllers/playstation/icons/PS5_Left_Stick_Click.png b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Left_Stick_Click.png
new file mode 100644
index 0000000..66e5271
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Left_Stick_Click.png differ
diff --git a/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Left_Stick_Click.png.import b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Left_Stick_Click.png.import
new file mode 100644
index 0000000..84fe65f
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Left_Stick_Click.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c0b1sdadfcnbk"
+path="res://.godot/imported/PS5_Left_Stick_Click.png-f837f37222a7c945cd4b672d0d7e3ba1.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_Left_Stick_Click.png"
+dest_files=["res://.godot/imported/PS5_Left_Stick_Click.png-f837f37222a7c945cd4b672d0d7e3ba1.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/guide/ui/renderers/controllers/playstation/icons/PS5_Microphone.png b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Microphone.png
new file mode 100644
index 0000000..bb0f331
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Microphone.png differ
diff --git a/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Microphone.png.import b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Microphone.png.import
new file mode 100644
index 0000000..6199d2d
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Microphone.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://eljpu2rrb3k4"
+path="res://.godot/imported/PS5_Microphone.png-3a2db423599523aa5c1b828df7d224bc.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_Microphone.png"
+dest_files=["res://.godot/imported/PS5_Microphone.png-3a2db423599523aa5c1b828df7d224bc.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/guide/ui/renderers/controllers/playstation/icons/PS5_Options.png b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Options.png
new file mode 100644
index 0000000..3e56fe2
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Options.png differ
diff --git a/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Options.png.import b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Options.png.import
new file mode 100644
index 0000000..b464245
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Options.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bkttgyeuecjw"
+path="res://.godot/imported/PS5_Options.png-4bd9928e2e3aca6fb17663799d26e7a5.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_Options.png"
+dest_files=["res://.godot/imported/PS5_Options.png-4bd9928e2e3aca6fb17663799d26e7a5.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/guide/ui/renderers/controllers/playstation/icons/PS5_Options_Alt.png b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Options_Alt.png
new file mode 100644
index 0000000..ca28364
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Options_Alt.png differ
diff --git a/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Options_Alt.png.import b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Options_Alt.png.import
new file mode 100644
index 0000000..cb2f529
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Options_Alt.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://byed3fsjbp82u"
+path="res://.godot/imported/PS5_Options_Alt.png-4b64997ac577d658c383b1e727319cf5.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_Options_Alt.png"
+dest_files=["res://.godot/imported/PS5_Options_Alt.png-4b64997ac577d658c383b1e727319cf5.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/guide/ui/renderers/controllers/playstation/icons/PS5_R1.png b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_R1.png
new file mode 100644
index 0000000..2cff97a
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_R1.png differ
diff --git a/addons/guide/ui/renderers/controllers/playstation/icons/PS5_R1.png.import b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_R1.png.import
new file mode 100644
index 0000000..3cb7aec
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_R1.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://rwgkfm18pk3l"
+path="res://.godot/imported/PS5_R1.png-2f57506c67c952763f228117ce37754b.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_R1.png"
+dest_files=["res://.godot/imported/PS5_R1.png-2f57506c67c952763f228117ce37754b.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/guide/ui/renderers/controllers/playstation/icons/PS5_R2.png b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_R2.png
new file mode 100644
index 0000000..a13f17f
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_R2.png differ
diff --git a/addons/guide/ui/renderers/controllers/playstation/icons/PS5_R2.png.import b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_R2.png.import
new file mode 100644
index 0000000..533cfd7
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_R2.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://u6ba23igjbj5"
+path="res://.godot/imported/PS5_R2.png-9671164f26e8ed5c0f2352c255960e7c.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_R2.png"
+dest_files=["res://.godot/imported/PS5_R2.png-9671164f26e8ed5c0f2352c255960e7c.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/guide/ui/renderers/controllers/playstation/icons/PS5_Right_Stick.png b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Right_Stick.png
new file mode 100644
index 0000000..85c1556
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Right_Stick.png differ
diff --git a/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Right_Stick.png.import b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Right_Stick.png.import
new file mode 100644
index 0000000..555e054
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Right_Stick.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bukgaq1m26bw3"
+path="res://.godot/imported/PS5_Right_Stick.png-884107fa82c161e8696ba874c711b8d7.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_Right_Stick.png"
+dest_files=["res://.godot/imported/PS5_Right_Stick.png-884107fa82c161e8696ba874c711b8d7.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/guide/ui/renderers/controllers/playstation/icons/PS5_Right_Stick_Click.png b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Right_Stick_Click.png
new file mode 100644
index 0000000..eecd4e8
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Right_Stick_Click.png differ
diff --git a/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Right_Stick_Click.png.import b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Right_Stick_Click.png.import
new file mode 100644
index 0000000..90f3926
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Right_Stick_Click.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c4krmros0va1i"
+path="res://.godot/imported/PS5_Right_Stick_Click.png-b097f7eaaf3fdd2f3db31ab4d9ef06b4.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_Right_Stick_Click.png"
+dest_files=["res://.godot/imported/PS5_Right_Stick_Click.png-b097f7eaaf3fdd2f3db31ab4d9ef06b4.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/guide/ui/renderers/controllers/playstation/icons/PS5_Share.png b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Share.png
new file mode 100644
index 0000000..d96e698
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Share.png differ
diff --git a/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Share.png.import b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Share.png.import
new file mode 100644
index 0000000..018c729
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Share.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bw2h7xxdtp31i"
+path="res://.godot/imported/PS5_Share.png-ecf2ad701cb784ee9e69b7052bffc94f.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_Share.png"
+dest_files=["res://.godot/imported/PS5_Share.png-ecf2ad701cb784ee9e69b7052bffc94f.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/guide/ui/renderers/controllers/playstation/icons/PS5_Share_Alt.png b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Share_Alt.png
new file mode 100644
index 0000000..10941aa
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Share_Alt.png differ
diff --git a/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Share_Alt.png.import b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Share_Alt.png.import
new file mode 100644
index 0000000..5bf9adc
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Share_Alt.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bwje5248woygn"
+path="res://.godot/imported/PS5_Share_Alt.png-f38d2e9e85009e094eb2254e0d890a1d.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_Share_Alt.png"
+dest_files=["res://.godot/imported/PS5_Share_Alt.png-f38d2e9e85009e094eb2254e0d890a1d.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/guide/ui/renderers/controllers/playstation/icons/PS5_Square.png b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Square.png
new file mode 100644
index 0000000..20f6065
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Square.png differ
diff --git a/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Square.png.import b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Square.png.import
new file mode 100644
index 0000000..501b7da
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Square.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dm6vfcwtodame"
+path="res://.godot/imported/PS5_Square.png-c0fff0babe3326f24867d317d430c13a.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_Square.png"
+dest_files=["res://.godot/imported/PS5_Square.png-c0fff0babe3326f24867d317d430c13a.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/guide/ui/renderers/controllers/playstation/icons/PS5_Touch_Pad.png b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Touch_Pad.png
new file mode 100644
index 0000000..1a77d0c
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Touch_Pad.png differ
diff --git a/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Touch_Pad.png.import b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Touch_Pad.png.import
new file mode 100644
index 0000000..025c11c
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Touch_Pad.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bxxkjsl2u83mp"
+path="res://.godot/imported/PS5_Touch_Pad.png-b3baca99700ac1cd505b545f684de924.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_Touch_Pad.png"
+dest_files=["res://.godot/imported/PS5_Touch_Pad.png-b3baca99700ac1cd505b545f684de924.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/guide/ui/renderers/controllers/playstation/icons/PS5_Triangle.png b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Triangle.png
new file mode 100644
index 0000000..4950d17
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Triangle.png differ
diff --git a/examples/sharp_corner_tapering/assets/tex_metal_taper_corner_right.png.import b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Triangle.png.import
similarity index 67%
rename from examples/sharp_corner_tapering/assets/tex_metal_taper_corner_right.png.import
rename to addons/guide/ui/renderers/controllers/playstation/icons/PS5_Triangle.png.import
index 7adf36e..0aa199d 100644
--- a/examples/sharp_corner_tapering/assets/tex_metal_taper_corner_right.png.import
+++ b/addons/guide/ui/renderers/controllers/playstation/icons/PS5_Triangle.png.import
@@ -2,16 +2,16 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://cdnfaf3bslk38"
-path="res://.godot/imported/tex_metal_taper_corner_right.png-75d13fe99bb8e540489390ee64c63ea5.ctex"
+uid="uid://bjjj12v4g82g4"
+path="res://.godot/imported/PS5_Triangle.png-6cfcb99a3dd2daba1763b52afa5e6f91.ctex"
metadata={
"vram_texture": false
}
[deps]
-source_file="res://examples/sharp_corner_tapering/assets/tex_metal_taper_corner_right.png"
-dest_files=["res://.godot/imported/tex_metal_taper_corner_right.png-75d13fe99bb8e540489390ee64c63ea5.ctex"]
+source_file="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_Triangle.png"
+dest_files=["res://.godot/imported/PS5_Triangle.png-6cfcb99a3dd2daba1763b52afa5e6f91.ctex"]
[params]
diff --git a/addons/guide/ui/renderers/controllers/playstation/playstation_controller_renderer.tscn b/addons/guide/ui/renderers/controllers/playstation/playstation_controller_renderer.tscn
new file mode 100644
index 0000000..d4809e0
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/playstation/playstation_controller_renderer.tscn
@@ -0,0 +1,101 @@
+[gd_scene load_steps=21 format=3 uid="uid://bwv1638hcrfni"]
+
+[ext_resource type="PackedScene" uid="uid://bsaylcb5ixjxk" path="res://addons/guide/ui/renderers/controllers/controller_renderer.tscn" id="1_bq6gh"]
+[ext_resource type="Texture2D" uid="uid://cfy1rx4d4wjdh" path="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_Cross.png" id="2_oqi6t"]
+[ext_resource type="Texture2D" uid="uid://civpcnwgbu5ky" path="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_Circle.png" id="3_m332j"]
+[ext_resource type="Texture2D" uid="uid://dm6vfcwtodame" path="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_Square.png" id="4_dqhg4"]
+[ext_resource type="Texture2D" uid="uid://bjjj12v4g82g4" path="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_Triangle.png" id="5_42ocy"]
+[ext_resource type="Texture2D" uid="uid://c3qet180o0dn6" path="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_Left_Stick.png" id="6_wwoxb"]
+[ext_resource type="Texture2D" uid="uid://c0b1sdadfcnbk" path="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_Left_Stick_Click.png" id="7_gethe"]
+[ext_resource type="Texture2D" uid="uid://bukgaq1m26bw3" path="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_Right_Stick.png" id="8_u2725"]
+[ext_resource type="Texture2D" uid="uid://c4krmros0va1i" path="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_Right_Stick_Click.png" id="9_wfckm"]
+[ext_resource type="Texture2D" uid="uid://cqgpumb0tf5xr" path="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_L1.png" id="10_34ib6"]
+[ext_resource type="Texture2D" uid="uid://rwgkfm18pk3l" path="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_R1.png" id="11_53ury"]
+[ext_resource type="Texture2D" uid="uid://bhoi6nfung5ye" path="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_L2.png" id="12_tyubh"]
+[ext_resource type="Texture2D" uid="uid://u6ba23igjbj5" path="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_R2.png" id="13_pr5lk"]
+[ext_resource type="Texture2D" uid="uid://bvbd876sy2430" path="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad_Up.png" id="14_h0miw"]
+[ext_resource type="Texture2D" uid="uid://bkpw61ctv0fbg" path="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad_Left.png" id="15_q5yu5"]
+[ext_resource type="Texture2D" uid="uid://dybnayy8y7rxe" path="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad_Right.png" id="16_ulk14"]
+[ext_resource type="Texture2D" uid="uid://vk1vje3280tk" path="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_Dpad_Down.png" id="17_wm4fj"]
+[ext_resource type="Texture2D" uid="uid://bkttgyeuecjw" path="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_Options.png" id="18_eabm3"]
+[ext_resource type="Texture2D" uid="uid://eljpu2rrb3k4" path="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_Microphone.png" id="19_oj5w7"]
+[ext_resource type="Texture2D" uid="uid://bw2h7xxdtp31i" path="res://addons/guide/ui/renderers/controllers/playstation/icons/PS5_Share.png" id="20_p3s2m"]
+
+[node name="ControllerRenderer" instance=ExtResource("1_bq6gh")]
+controller_name_matches = Array[String](["DualSense", "DualShock", "PlayStation", "PS3", "PS4", "PS5"])
+a_button = ExtResource("2_oqi6t")
+b_button = ExtResource("3_m332j")
+x_button = ExtResource("4_dqhg4")
+y_button = ExtResource("5_42ocy")
+left_stick = ExtResource("6_wwoxb")
+left_stick_click = ExtResource("7_gethe")
+right_stick = ExtResource("8_u2725")
+right_stick_click = ExtResource("9_wfckm")
+left_bumper = ExtResource("10_34ib6")
+right_bumper = ExtResource("11_53ury")
+left_trigger = ExtResource("12_tyubh")
+right_trigger = ExtResource("13_pr5lk")
+dpad_up = ExtResource("14_h0miw")
+dpad_left = ExtResource("15_q5yu5")
+dpad_right = ExtResource("16_ulk14")
+dpad_down = ExtResource("17_wm4fj")
+start = ExtResource("18_eabm3")
+misc1 = ExtResource("19_oj5w7")
+back = ExtResource("20_p3s2m")
+
+[node name="AButton" parent="HBoxContainer/Controls" index="0"]
+texture = ExtResource("2_oqi6t")
+
+[node name="BButton" parent="HBoxContainer/Controls" index="1"]
+texture = ExtResource("3_m332j")
+
+[node name="XButton" parent="HBoxContainer/Controls" index="2"]
+texture = ExtResource("4_dqhg4")
+
+[node name="YButton" parent="HBoxContainer/Controls" index="3"]
+texture = ExtResource("5_42ocy")
+
+[node name="LeftStick" parent="HBoxContainer/Controls" index="4"]
+texture = ExtResource("6_wwoxb")
+
+[node name="LeftStickClick" parent="HBoxContainer/Controls" index="5"]
+texture = ExtResource("7_gethe")
+
+[node name="RightStick" parent="HBoxContainer/Controls" index="6"]
+texture = ExtResource("8_u2725")
+
+[node name="RightStickClick" parent="HBoxContainer/Controls" index="7"]
+texture = ExtResource("9_wfckm")
+
+[node name="LeftBumper" parent="HBoxContainer/Controls" index="8"]
+texture = ExtResource("10_34ib6")
+
+[node name="RightBumper" parent="HBoxContainer/Controls" index="9"]
+texture = ExtResource("11_53ury")
+
+[node name="LeftTrigger" parent="HBoxContainer/Controls" index="10"]
+texture = ExtResource("12_tyubh")
+
+[node name="RightTrigger" parent="HBoxContainer/Controls" index="11"]
+texture = ExtResource("13_pr5lk")
+
+[node name="DpadUp" parent="HBoxContainer/Controls" index="12"]
+texture = ExtResource("14_h0miw")
+
+[node name="DpadLeft" parent="HBoxContainer/Controls" index="13"]
+texture = ExtResource("15_q5yu5")
+
+[node name="DpadRight" parent="HBoxContainer/Controls" index="14"]
+texture = ExtResource("16_ulk14")
+
+[node name="DpadDown" parent="HBoxContainer/Controls" index="15"]
+texture = ExtResource("17_wm4fj")
+
+[node name="Start" parent="HBoxContainer/Controls" index="16"]
+texture = ExtResource("18_eabm3")
+
+[node name="Misc1" parent="HBoxContainer/Controls" index="17"]
+texture = ExtResource("19_oj5w7")
+
+[node name="Back" parent="HBoxContainer/Controls" index="18"]
+texture = ExtResource("20_p3s2m")
diff --git a/addons/guide/ui/renderers/controllers/switch/icons/Switch_A.png b/addons/guide/ui/renderers/controllers/switch/icons/Switch_A.png
new file mode 100644
index 0000000..df756ef
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/switch/icons/Switch_A.png differ
diff --git a/addons/guide/ui/renderers/controllers/switch/icons/Switch_A.png.import b/addons/guide/ui/renderers/controllers/switch/icons/Switch_A.png.import
new file mode 100644
index 0000000..309879f
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/switch/icons/Switch_A.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cl75ptbm7sfi5"
+path="res://.godot/imported/Switch_A.png-f1d58b04f27891568073a11e43627862.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_A.png"
+dest_files=["res://.godot/imported/Switch_A.png-f1d58b04f27891568073a11e43627862.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/guide/ui/renderers/controllers/switch/icons/Switch_B.png b/addons/guide/ui/renderers/controllers/switch/icons/Switch_B.png
new file mode 100644
index 0000000..ea7e743
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/switch/icons/Switch_B.png differ
diff --git a/addons/guide/ui/renderers/controllers/switch/icons/Switch_B.png.import b/addons/guide/ui/renderers/controllers/switch/icons/Switch_B.png.import
new file mode 100644
index 0000000..a6de4c9
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/switch/icons/Switch_B.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bptn4jygg3p8q"
+path="res://.godot/imported/Switch_B.png-fbb8f305e166298807aa18fab0c22a62.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_B.png"
+dest_files=["res://.godot/imported/Switch_B.png-fbb8f305e166298807aa18fab0c22a62.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/guide/ui/renderers/controllers/switch/icons/Switch_Controller_Left.png b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Controller_Left.png
new file mode 100644
index 0000000..f90a244
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Controller_Left.png differ
diff --git a/addons/guide/ui/renderers/controllers/switch/icons/Switch_Controller_Left.png.import b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Controller_Left.png.import
new file mode 100644
index 0000000..4d4974c
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Controller_Left.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b0ha1pv08n3fn"
+path="res://.godot/imported/Switch_Controller_Left.png-832f94a111c828ab506576e8c22b3c3a.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_Controller_Left.png"
+dest_files=["res://.godot/imported/Switch_Controller_Left.png-832f94a111c828ab506576e8c22b3c3a.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/guide/ui/renderers/controllers/switch/icons/Switch_Controller_Right.png b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Controller_Right.png
new file mode 100644
index 0000000..873da7e
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Controller_Right.png differ
diff --git a/addons/guide/ui/renderers/controllers/switch/icons/Switch_Controller_Right.png.import b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Controller_Right.png.import
new file mode 100644
index 0000000..c3d22d6
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Controller_Right.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://qv33ijfxtj1x"
+path="res://.godot/imported/Switch_Controller_Right.png-0738167dcf8a308918a4e0351ec370a7.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_Controller_Right.png"
+dest_files=["res://.godot/imported/Switch_Controller_Right.png-0738167dcf8a308918a4e0351ec370a7.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/guide/ui/renderers/controllers/switch/icons/Switch_Controllers.png b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Controllers.png
new file mode 100644
index 0000000..b6ee54d
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Controllers.png differ
diff --git a/addons/guide/ui/renderers/controllers/switch/icons/Switch_Controllers.png.import b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Controllers.png.import
new file mode 100644
index 0000000..f4751a8
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Controllers.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dqfhfnjqwcjpk"
+path="res://.godot/imported/Switch_Controllers.png-0ab7b7957a575a33aec8f6138ec1b468.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_Controllers.png"
+dest_files=["res://.godot/imported/Switch_Controllers.png-0ab7b7957a575a33aec8f6138ec1b468.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/guide/ui/renderers/controllers/switch/icons/Switch_Controllers_Separate.png b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Controllers_Separate.png
new file mode 100644
index 0000000..0c019da
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Controllers_Separate.png differ
diff --git a/addons/guide/ui/renderers/controllers/switch/icons/Switch_Controllers_Separate.png.import b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Controllers_Separate.png.import
new file mode 100644
index 0000000..88af5e1
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Controllers_Separate.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://8lsr71y25n8q"
+path="res://.godot/imported/Switch_Controllers_Separate.png-8b202bd393de46b2f97712ac19581121.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_Controllers_Separate.png"
+dest_files=["res://.godot/imported/Switch_Controllers_Separate.png-8b202bd393de46b2f97712ac19581121.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/guide/ui/renderers/controllers/switch/icons/Switch_Down.png b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Down.png
new file mode 100644
index 0000000..7b7b2b2
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Down.png differ
diff --git a/addons/guide/ui/renderers/controllers/switch/icons/Switch_Down.png.import b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Down.png.import
new file mode 100644
index 0000000..77b7d18
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Down.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://qt8r1byskjmu"
+path="res://.godot/imported/Switch_Down.png-a8bebe4deb11df0456c90115a2306f62.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_Down.png"
+dest_files=["res://.godot/imported/Switch_Down.png-a8bebe4deb11df0456c90115a2306f62.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/guide/ui/renderers/controllers/switch/icons/Switch_Dpad.png b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Dpad.png
new file mode 100644
index 0000000..12f01eb
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Dpad.png differ
diff --git a/addons/guide/ui/renderers/controllers/switch/icons/Switch_Dpad.png.import b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Dpad.png.import
new file mode 100644
index 0000000..9d3db24
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Dpad.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bxvp4elmagomg"
+path="res://.godot/imported/Switch_Dpad.png-9f1893107a829bf94f95a8bcfa879f1c.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_Dpad.png"
+dest_files=["res://.godot/imported/Switch_Dpad.png-9f1893107a829bf94f95a8bcfa879f1c.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/guide/ui/renderers/controllers/switch/icons/Switch_Dpad_Down.png b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Dpad_Down.png
new file mode 100644
index 0000000..37f6d5b
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Dpad_Down.png differ
diff --git a/addons/guide/ui/renderers/controllers/switch/icons/Switch_Dpad_Down.png.import b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Dpad_Down.png.import
new file mode 100644
index 0000000..8a8f63f
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Dpad_Down.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dq2ypo4cx3ucs"
+path="res://.godot/imported/Switch_Dpad_Down.png-fda4a0d96c9c1d604adf4addc863361f.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_Dpad_Down.png"
+dest_files=["res://.godot/imported/Switch_Dpad_Down.png-fda4a0d96c9c1d604adf4addc863361f.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/guide/ui/renderers/controllers/switch/icons/Switch_Dpad_Left.png b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Dpad_Left.png
new file mode 100644
index 0000000..8efd7a4
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Dpad_Left.png differ
diff --git a/addons/guide/ui/renderers/controllers/switch/icons/Switch_Dpad_Left.png.import b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Dpad_Left.png.import
new file mode 100644
index 0000000..e6468b6
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Dpad_Left.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://rcrsxqeu6mns"
+path="res://.godot/imported/Switch_Dpad_Left.png-7d4e2c9108e5188065fbd9645e1af97e.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_Dpad_Left.png"
+dest_files=["res://.godot/imported/Switch_Dpad_Left.png-7d4e2c9108e5188065fbd9645e1af97e.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/guide/ui/renderers/controllers/switch/icons/Switch_Dpad_Right.png b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Dpad_Right.png
new file mode 100644
index 0000000..8b5411d
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Dpad_Right.png differ
diff --git a/addons/guide/ui/renderers/controllers/switch/icons/Switch_Dpad_Right.png.import b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Dpad_Right.png.import
new file mode 100644
index 0000000..41f76e4
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Dpad_Right.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dubah62ttpnc0"
+path="res://.godot/imported/Switch_Dpad_Right.png-74599bcfe029ca89e967999e956aa664.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_Dpad_Right.png"
+dest_files=["res://.godot/imported/Switch_Dpad_Right.png-74599bcfe029ca89e967999e956aa664.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/guide/ui/renderers/controllers/switch/icons/Switch_Dpad_Up.png b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Dpad_Up.png
new file mode 100644
index 0000000..700a8ba
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Dpad_Up.png differ
diff --git a/addons/guide/ui/renderers/controllers/switch/icons/Switch_Dpad_Up.png.import b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Dpad_Up.png.import
new file mode 100644
index 0000000..5609977
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Dpad_Up.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://u6uclokrrbaq"
+path="res://.godot/imported/Switch_Dpad_Up.png-38cc365cd950cad00eb7342f63b614a5.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_Dpad_Up.png"
+dest_files=["res://.godot/imported/Switch_Dpad_Up.png-38cc365cd950cad00eb7342f63b614a5.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/guide/ui/renderers/controllers/switch/icons/Switch_Home.png b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Home.png
new file mode 100644
index 0000000..9b6733c
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Home.png differ
diff --git a/addons/guide/ui/renderers/controllers/switch/icons/Switch_Home.png.import b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Home.png.import
new file mode 100644
index 0000000..29e9d4d
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Home.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://uf6oq3wbq11f"
+path="res://.godot/imported/Switch_Home.png-31680591ab356324906bfaaeace20e43.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_Home.png"
+dest_files=["res://.godot/imported/Switch_Home.png-31680591ab356324906bfaaeace20e43.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/guide/ui/renderers/controllers/switch/icons/Switch_LB.png b/addons/guide/ui/renderers/controllers/switch/icons/Switch_LB.png
new file mode 100644
index 0000000..ddfa3b9
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/switch/icons/Switch_LB.png differ
diff --git a/addons/guide/ui/renderers/controllers/switch/icons/Switch_LB.png.import b/addons/guide/ui/renderers/controllers/switch/icons/Switch_LB.png.import
new file mode 100644
index 0000000..f004694
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/switch/icons/Switch_LB.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cb6gvej03avm3"
+path="res://.godot/imported/Switch_LB.png-fc77289764fd409ac6c0408486b0c16b.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_LB.png"
+dest_files=["res://.godot/imported/Switch_LB.png-fc77289764fd409ac6c0408486b0c16b.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/guide/ui/renderers/controllers/switch/icons/Switch_LT.png b/addons/guide/ui/renderers/controllers/switch/icons/Switch_LT.png
new file mode 100644
index 0000000..6942e1f
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/switch/icons/Switch_LT.png differ
diff --git a/addons/guide/ui/renderers/controllers/switch/icons/Switch_LT.png.import b/addons/guide/ui/renderers/controllers/switch/icons/Switch_LT.png.import
new file mode 100644
index 0000000..3badb9f
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/switch/icons/Switch_LT.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://savy2mhybmun"
+path="res://.godot/imported/Switch_LT.png-b154a02af7bcd266253a208d8d610852.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_LT.png"
+dest_files=["res://.godot/imported/Switch_LT.png-b154a02af7bcd266253a208d8d610852.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/guide/ui/renderers/controllers/switch/icons/Switch_Left.png b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Left.png
new file mode 100644
index 0000000..fd58439
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Left.png differ
diff --git a/addons/guide/ui/renderers/controllers/switch/icons/Switch_Left.png.import b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Left.png.import
new file mode 100644
index 0000000..20957cb
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Left.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cyjwul8mif4s2"
+path="res://.godot/imported/Switch_Left.png-cc76cc1aa00b43ca3e312439715d169a.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_Left.png"
+dest_files=["res://.godot/imported/Switch_Left.png-cc76cc1aa00b43ca3e312439715d169a.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/guide/ui/renderers/controllers/switch/icons/Switch_Left_Stick.png b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Left_Stick.png
new file mode 100644
index 0000000..d861ca5
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Left_Stick.png differ
diff --git a/addons/guide/ui/renderers/controllers/switch/icons/Switch_Left_Stick.png.import b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Left_Stick.png.import
new file mode 100644
index 0000000..2a5be36
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Left_Stick.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cha2jimmsyrsg"
+path="res://.godot/imported/Switch_Left_Stick.png-1cc52d3c1e1f259e0115217e02740b99.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_Left_Stick.png"
+dest_files=["res://.godot/imported/Switch_Left_Stick.png-1cc52d3c1e1f259e0115217e02740b99.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/guide/ui/renderers/controllers/switch/icons/Switch_Left_Stick_Click.png b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Left_Stick_Click.png
new file mode 100644
index 0000000..66e5271
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Left_Stick_Click.png differ
diff --git a/addons/guide/ui/renderers/controllers/switch/icons/Switch_Left_Stick_Click.png.import b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Left_Stick_Click.png.import
new file mode 100644
index 0000000..fc56eb6
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Left_Stick_Click.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://by1vmleujtq1i"
+path="res://.godot/imported/Switch_Left_Stick_Click.png-7992de9526c87e19b6a04a21954e96af.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_Left_Stick_Click.png"
+dest_files=["res://.godot/imported/Switch_Left_Stick_Click.png-7992de9526c87e19b6a04a21954e96af.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/guide/ui/renderers/controllers/switch/icons/Switch_Minus.png b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Minus.png
new file mode 100644
index 0000000..d32608e
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Minus.png differ
diff --git a/addons/guide/ui/renderers/controllers/switch/icons/Switch_Minus.png.import b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Minus.png.import
new file mode 100644
index 0000000..9a4233c
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Minus.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bdydqv6vi48ix"
+path="res://.godot/imported/Switch_Minus.png-b6cd3147393308196e49ab3a608d1c8a.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_Minus.png"
+dest_files=["res://.godot/imported/Switch_Minus.png-b6cd3147393308196e49ab3a608d1c8a.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/guide/ui/renderers/controllers/switch/icons/Switch_Plus.png b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Plus.png
new file mode 100644
index 0000000..f1b0dc4
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Plus.png differ
diff --git a/addons/guide/ui/renderers/controllers/switch/icons/Switch_Plus.png.import b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Plus.png.import
new file mode 100644
index 0000000..4d470fe
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Plus.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://j3wpcxyxsy2r"
+path="res://.godot/imported/Switch_Plus.png-75c2cf5b7056a47425c210b1f60dcdc2.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_Plus.png"
+dest_files=["res://.godot/imported/Switch_Plus.png-75c2cf5b7056a47425c210b1f60dcdc2.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/guide/ui/renderers/controllers/switch/icons/Switch_RB.png b/addons/guide/ui/renderers/controllers/switch/icons/Switch_RB.png
new file mode 100644
index 0000000..01f137a
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/switch/icons/Switch_RB.png differ
diff --git a/addons/guide/ui/renderers/controllers/switch/icons/Switch_RB.png.import b/addons/guide/ui/renderers/controllers/switch/icons/Switch_RB.png.import
new file mode 100644
index 0000000..1c7a578
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/switch/icons/Switch_RB.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://s2xm61tj0mfy"
+path="res://.godot/imported/Switch_RB.png-3d9fbc66dcc67aee9a5716cef6ece679.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_RB.png"
+dest_files=["res://.godot/imported/Switch_RB.png-3d9fbc66dcc67aee9a5716cef6ece679.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/guide/ui/renderers/controllers/switch/icons/Switch_RT.png b/addons/guide/ui/renderers/controllers/switch/icons/Switch_RT.png
new file mode 100644
index 0000000..6aef3cb
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/switch/icons/Switch_RT.png differ
diff --git a/addons/guide/ui/renderers/controllers/switch/icons/Switch_RT.png.import b/addons/guide/ui/renderers/controllers/switch/icons/Switch_RT.png.import
new file mode 100644
index 0000000..2109b91
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/switch/icons/Switch_RT.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cccvjq78xw3n4"
+path="res://.godot/imported/Switch_RT.png-521dbc557ae52cbe19429b25fbf68d18.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_RT.png"
+dest_files=["res://.godot/imported/Switch_RT.png-521dbc557ae52cbe19429b25fbf68d18.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/guide/ui/renderers/controllers/switch/icons/Switch_Right.png b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Right.png
new file mode 100644
index 0000000..f524c6c
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Right.png differ
diff --git a/addons/guide/ui/renderers/controllers/switch/icons/Switch_Right.png.import b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Right.png.import
new file mode 100644
index 0000000..094f135
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Right.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b3os8st4cai36"
+path="res://.godot/imported/Switch_Right.png-cd6972267e466e454282edcdee867c3a.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_Right.png"
+dest_files=["res://.godot/imported/Switch_Right.png-cd6972267e466e454282edcdee867c3a.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/guide/ui/renderers/controllers/switch/icons/Switch_Right_Stick.png b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Right_Stick.png
new file mode 100644
index 0000000..f2c605b
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Right_Stick.png differ
diff --git a/addons/guide/ui/renderers/controllers/switch/icons/Switch_Right_Stick.png.import b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Right_Stick.png.import
new file mode 100644
index 0000000..5b96fa4
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Right_Stick.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://d1jqxuup5llkb"
+path="res://.godot/imported/Switch_Right_Stick.png-9a091e8f8aab1b8517e0e63458ce9648.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_Right_Stick.png"
+dest_files=["res://.godot/imported/Switch_Right_Stick.png-9a091e8f8aab1b8517e0e63458ce9648.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/guide/ui/renderers/controllers/switch/icons/Switch_Right_Stick_Click.png b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Right_Stick_Click.png
new file mode 100644
index 0000000..eecd4e8
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Right_Stick_Click.png differ
diff --git a/addons/guide/ui/renderers/controllers/switch/icons/Switch_Right_Stick_Click.png.import b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Right_Stick_Click.png.import
new file mode 100644
index 0000000..d9492b0
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Right_Stick_Click.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://denpxaxemjpg3"
+path="res://.godot/imported/Switch_Right_Stick_Click.png-73f9b93cd8d68f031b93843441f5ac52.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_Right_Stick_Click.png"
+dest_files=["res://.godot/imported/Switch_Right_Stick_Click.png-73f9b93cd8d68f031b93843441f5ac52.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/guide/ui/renderers/controllers/switch/icons/Switch_Square.png b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Square.png
new file mode 100644
index 0000000..d0fd432
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Square.png differ
diff --git a/addons/guide/ui/renderers/controllers/switch/icons/Switch_Square.png.import b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Square.png.import
new file mode 100644
index 0000000..1da9b49
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Square.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cl0owijnhu5pc"
+path="res://.godot/imported/Switch_Square.png-e3ed4021db85fe9e3827c68afd31c3ac.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_Square.png"
+dest_files=["res://.godot/imported/Switch_Square.png-e3ed4021db85fe9e3827c68afd31c3ac.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/guide/ui/renderers/controllers/switch/icons/Switch_Up.png b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Up.png
new file mode 100644
index 0000000..352f890
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Up.png differ
diff --git a/addons/guide/ui/renderers/controllers/switch/icons/Switch_Up.png.import b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Up.png.import
new file mode 100644
index 0000000..e6fe688
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Up.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dxmy0dvee663b"
+path="res://.godot/imported/Switch_Up.png-d9db5e9a6fb52c3f8674741ac7a63c50.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_Up.png"
+dest_files=["res://.godot/imported/Switch_Up.png-d9db5e9a6fb52c3f8674741ac7a63c50.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/guide/ui/renderers/controllers/switch/icons/Switch_X.png b/addons/guide/ui/renderers/controllers/switch/icons/Switch_X.png
new file mode 100644
index 0000000..bdf86ba
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/switch/icons/Switch_X.png differ
diff --git a/addons/guide/ui/renderers/controllers/switch/icons/Switch_X.png.import b/addons/guide/ui/renderers/controllers/switch/icons/Switch_X.png.import
new file mode 100644
index 0000000..eeee651
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/switch/icons/Switch_X.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://7c6ie8ef23ox"
+path="res://.godot/imported/Switch_X.png-8ff15c2bb86e671b78d91e2041718b60.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_X.png"
+dest_files=["res://.godot/imported/Switch_X.png-8ff15c2bb86e671b78d91e2041718b60.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/guide/ui/renderers/controllers/switch/icons/Switch_Y.png b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Y.png
new file mode 100644
index 0000000..46ac216
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Y.png differ
diff --git a/addons/guide/ui/renderers/controllers/switch/icons/Switch_Y.png.import b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Y.png.import
new file mode 100644
index 0000000..0ace6f9
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/switch/icons/Switch_Y.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bru7lpc778npo"
+path="res://.godot/imported/Switch_Y.png-f47eae5d6991151df29244754a422398.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_Y.png"
+dest_files=["res://.godot/imported/Switch_Y.png-f47eae5d6991151df29244754a422398.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/guide/ui/renderers/controllers/switch/switch_controller_renderer.tscn b/addons/guide/ui/renderers/controllers/switch/switch_controller_renderer.tscn
new file mode 100644
index 0000000..98bc510
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/switch/switch_controller_renderer.tscn
@@ -0,0 +1,101 @@
+[gd_scene load_steps=21 format=3 uid="uid://dlhmh4o43l7pp"]
+
+[ext_resource type="PackedScene" uid="uid://bsaylcb5ixjxk" path="res://addons/guide/ui/renderers/controllers/controller_renderer.tscn" id="1_mxg2g"]
+[ext_resource type="Texture2D" uid="uid://bptn4jygg3p8q" path="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_B.png" id="2_hj4a5"]
+[ext_resource type="Texture2D" uid="uid://cl75ptbm7sfi5" path="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_A.png" id="3_0hjly"]
+[ext_resource type="Texture2D" uid="uid://bru7lpc778npo" path="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_Y.png" id="4_b8ldp"]
+[ext_resource type="Texture2D" uid="uid://7c6ie8ef23ox" path="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_X.png" id="5_qpo6l"]
+[ext_resource type="Texture2D" uid="uid://cha2jimmsyrsg" path="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_Left_Stick.png" id="6_o36fw"]
+[ext_resource type="Texture2D" uid="uid://by1vmleujtq1i" path="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_Left_Stick_Click.png" id="7_tn44v"]
+[ext_resource type="Texture2D" uid="uid://d1jqxuup5llkb" path="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_Right_Stick.png" id="8_gfspk"]
+[ext_resource type="Texture2D" uid="uid://denpxaxemjpg3" path="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_Right_Stick_Click.png" id="9_ktbic"]
+[ext_resource type="Texture2D" uid="uid://cb6gvej03avm3" path="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_LB.png" id="10_6uo88"]
+[ext_resource type="Texture2D" uid="uid://s2xm61tj0mfy" path="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_RB.png" id="11_xm8m7"]
+[ext_resource type="Texture2D" uid="uid://savy2mhybmun" path="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_LT.png" id="12_y225d"]
+[ext_resource type="Texture2D" uid="uid://cccvjq78xw3n4" path="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_RT.png" id="13_xpnov"]
+[ext_resource type="Texture2D" uid="uid://dxmy0dvee663b" path="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_Up.png" id="14_ujug8"]
+[ext_resource type="Texture2D" uid="uid://cyjwul8mif4s2" path="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_Left.png" id="15_2efb3"]
+[ext_resource type="Texture2D" uid="uid://b3os8st4cai36" path="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_Right.png" id="16_yqxw1"]
+[ext_resource type="Texture2D" uid="uid://qt8r1byskjmu" path="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_Down.png" id="17_gfqch"]
+[ext_resource type="Texture2D" uid="uid://j3wpcxyxsy2r" path="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_Plus.png" id="18_s5cn6"]
+[ext_resource type="Texture2D" uid="uid://cl0owijnhu5pc" path="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_Square.png" id="19_iegd6"]
+[ext_resource type="Texture2D" uid="uid://bdydqv6vi48ix" path="res://addons/guide/ui/renderers/controllers/switch/icons/Switch_Minus.png" id="20_kalbc"]
+
+[node name="ControllerRenderer" instance=ExtResource("1_mxg2g")]
+controller_name_matches = Array[String](["Nintendo Switch"])
+a_button = ExtResource("2_hj4a5")
+b_button = ExtResource("3_0hjly")
+x_button = ExtResource("4_b8ldp")
+y_button = ExtResource("5_qpo6l")
+left_stick = ExtResource("6_o36fw")
+left_stick_click = ExtResource("7_tn44v")
+right_stick = ExtResource("8_gfspk")
+right_stick_click = ExtResource("9_ktbic")
+left_bumper = ExtResource("10_6uo88")
+right_bumper = ExtResource("11_xm8m7")
+left_trigger = ExtResource("12_y225d")
+right_trigger = ExtResource("13_xpnov")
+dpad_up = ExtResource("14_ujug8")
+dpad_left = ExtResource("15_2efb3")
+dpad_right = ExtResource("16_yqxw1")
+dpad_down = ExtResource("17_gfqch")
+start = ExtResource("18_s5cn6")
+misc1 = ExtResource("19_iegd6")
+back = ExtResource("20_kalbc")
+
+[node name="AButton" parent="HBoxContainer/Controls" index="0"]
+texture = ExtResource("2_hj4a5")
+
+[node name="BButton" parent="HBoxContainer/Controls" index="1"]
+texture = ExtResource("3_0hjly")
+
+[node name="XButton" parent="HBoxContainer/Controls" index="2"]
+texture = ExtResource("4_b8ldp")
+
+[node name="YButton" parent="HBoxContainer/Controls" index="3"]
+texture = ExtResource("5_qpo6l")
+
+[node name="LeftStick" parent="HBoxContainer/Controls" index="4"]
+texture = ExtResource("6_o36fw")
+
+[node name="LeftStickClick" parent="HBoxContainer/Controls" index="5"]
+texture = ExtResource("7_tn44v")
+
+[node name="RightStick" parent="HBoxContainer/Controls" index="6"]
+texture = ExtResource("8_gfspk")
+
+[node name="RightStickClick" parent="HBoxContainer/Controls" index="7"]
+texture = ExtResource("9_ktbic")
+
+[node name="LeftBumper" parent="HBoxContainer/Controls" index="8"]
+texture = ExtResource("10_6uo88")
+
+[node name="RightBumper" parent="HBoxContainer/Controls" index="9"]
+texture = ExtResource("11_xm8m7")
+
+[node name="LeftTrigger" parent="HBoxContainer/Controls" index="10"]
+texture = ExtResource("12_y225d")
+
+[node name="RightTrigger" parent="HBoxContainer/Controls" index="11"]
+texture = ExtResource("13_xpnov")
+
+[node name="DpadUp" parent="HBoxContainer/Controls" index="12"]
+texture = ExtResource("14_ujug8")
+
+[node name="DpadLeft" parent="HBoxContainer/Controls" index="13"]
+texture = ExtResource("15_2efb3")
+
+[node name="DpadRight" parent="HBoxContainer/Controls" index="14"]
+texture = ExtResource("16_yqxw1")
+
+[node name="DpadDown" parent="HBoxContainer/Controls" index="15"]
+texture = ExtResource("17_gfqch")
+
+[node name="Start" parent="HBoxContainer/Controls" index="16"]
+texture = ExtResource("18_s5cn6")
+
+[node name="Misc1" parent="HBoxContainer/Controls" index="17"]
+texture = ExtResource("19_iegd6")
+
+[node name="Back" parent="HBoxContainer/Controls" index="18"]
+texture = ExtResource("20_kalbc")
diff --git a/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_A.png b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_A.png
new file mode 100644
index 0000000..e22bb29
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_A.png differ
diff --git a/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_A.png.import b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_A.png.import
new file mode 100644
index 0000000..271ef8d
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_A.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cujvw3m7aghgh"
+path="res://.godot/imported/XboxSeriesX_A.png-6e6e4943fb226932d525ee72e98fb8ef.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_A.png"
+dest_files=["res://.godot/imported/XboxSeriesX_A.png-6e6e4943fb226932d525ee72e98fb8ef.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/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_B.png b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_B.png
new file mode 100644
index 0000000..9312c26
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_B.png differ
diff --git a/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_B.png.import b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_B.png.import
new file mode 100644
index 0000000..4eb3b2b
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_B.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://brgqhp87g40l5"
+path="res://.godot/imported/XboxSeriesX_B.png-78255eca5094e7bc4b681e33187791ff.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_B.png"
+dest_files=["res://.godot/imported/XboxSeriesX_B.png-78255eca5094e7bc4b681e33187791ff.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/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad.png b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad.png
new file mode 100644
index 0000000..d66bd81
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad.png differ
diff --git a/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad.png.import b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad.png.import
new file mode 100644
index 0000000..177a232
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://j5kp5tnwtysq"
+path="res://.godot/imported/XboxSeriesX_Dpad.png-4bf4aa5e92bab139204438b5ac2b303d.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad.png"
+dest_files=["res://.godot/imported/XboxSeriesX_Dpad.png-4bf4aa5e92bab139204438b5ac2b303d.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/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad_Down.png b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad_Down.png
new file mode 100644
index 0000000..93478ee
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad_Down.png differ
diff --git a/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad_Down.png.import b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad_Down.png.import
new file mode 100644
index 0000000..33030ff
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad_Down.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://d1fgf8ia7q4vo"
+path="res://.godot/imported/XboxSeriesX_Dpad_Down.png-04b87c5546afa2b93f9116f73733624b.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad_Down.png"
+dest_files=["res://.godot/imported/XboxSeriesX_Dpad_Down.png-04b87c5546afa2b93f9116f73733624b.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/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad_Left.png b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad_Left.png
new file mode 100644
index 0000000..e1e3dfd
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad_Left.png differ
diff --git a/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad_Left.png.import b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad_Left.png.import
new file mode 100644
index 0000000..4a746df
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad_Left.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bqtskbk6l8v7w"
+path="res://.godot/imported/XboxSeriesX_Dpad_Left.png-8d74fbe74c1e93968cbcbaa007feb182.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad_Left.png"
+dest_files=["res://.godot/imported/XboxSeriesX_Dpad_Left.png-8d74fbe74c1e93968cbcbaa007feb182.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/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad_Right.png b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad_Right.png
new file mode 100644
index 0000000..2cabaef
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad_Right.png differ
diff --git a/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad_Right.png.import b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad_Right.png.import
new file mode 100644
index 0000000..4dc009f
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad_Right.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://gsilbr1bntic"
+path="res://.godot/imported/XboxSeriesX_Dpad_Right.png-e6fc74a1f6107eff3516a80369cf5807.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad_Right.png"
+dest_files=["res://.godot/imported/XboxSeriesX_Dpad_Right.png-e6fc74a1f6107eff3516a80369cf5807.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/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad_Up.png b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad_Up.png
new file mode 100644
index 0000000..a466bad
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad_Up.png differ
diff --git a/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad_Up.png.import b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad_Up.png.import
new file mode 100644
index 0000000..a3ae1e8
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad_Up.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://boc4an5lj12lp"
+path="res://.godot/imported/XboxSeriesX_Dpad_Up.png-d339224a22eaf2e00e885d73c907a138.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad_Up.png"
+dest_files=["res://.godot/imported/XboxSeriesX_Dpad_Up.png-d339224a22eaf2e00e885d73c907a138.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/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_LB.png b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_LB.png
new file mode 100644
index 0000000..f6c414b
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_LB.png differ
diff --git a/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_LB.png.import b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_LB.png.import
new file mode 100644
index 0000000..99b6409
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_LB.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://uwqbgrkqv7qc"
+path="res://.godot/imported/XboxSeriesX_LB.png-50df36f6ecf745654f01aae04e5a8a67.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_LB.png"
+dest_files=["res://.godot/imported/XboxSeriesX_LB.png-50df36f6ecf745654f01aae04e5a8a67.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/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_LT.png b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_LT.png
new file mode 100644
index 0000000..526816c
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_LT.png differ
diff --git a/examples/sharp_corner_tapering/assets/tex_metal_taper_corner_left.png.import b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_LT.png.import
similarity index 65%
rename from examples/sharp_corner_tapering/assets/tex_metal_taper_corner_left.png.import
rename to addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_LT.png.import
index 5315f40..7b6737d 100644
--- a/examples/sharp_corner_tapering/assets/tex_metal_taper_corner_left.png.import
+++ b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_LT.png.import
@@ -2,16 +2,16 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://jc3g5qsmnpdd"
-path="res://.godot/imported/tex_metal_taper_corner_left.png-735fbf01b6fc0725b32a91fb24aae5d5.ctex"
+uid="uid://c1rw1hnlt3dlt"
+path="res://.godot/imported/XboxSeriesX_LT.png-4c80e87d7c8bd0e2d11025799541d6ae.ctex"
metadata={
"vram_texture": false
}
[deps]
-source_file="res://examples/sharp_corner_tapering/assets/tex_metal_taper_corner_left.png"
-dest_files=["res://.godot/imported/tex_metal_taper_corner_left.png-735fbf01b6fc0725b32a91fb24aae5d5.ctex"]
+source_file="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_LT.png"
+dest_files=["res://.godot/imported/XboxSeriesX_LT.png-4c80e87d7c8bd0e2d11025799541d6ae.ctex"]
[params]
diff --git a/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Left_Stick.png b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Left_Stick.png
new file mode 100644
index 0000000..de49dc6
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Left_Stick.png differ
diff --git a/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Left_Stick.png.import b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Left_Stick.png.import
new file mode 100644
index 0000000..66a1fbd
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Left_Stick.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://wol4p4f32lfr"
+path="res://.godot/imported/XboxSeriesX_Left_Stick.png-c81470e6e08de714ca5029e194422ef3.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Left_Stick.png"
+dest_files=["res://.godot/imported/XboxSeriesX_Left_Stick.png-c81470e6e08de714ca5029e194422ef3.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/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Left_Stick_Click.png b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Left_Stick_Click.png
new file mode 100644
index 0000000..ad0428f
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Left_Stick_Click.png differ
diff --git a/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Left_Stick_Click.png.import b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Left_Stick_Click.png.import
new file mode 100644
index 0000000..bc1a37b
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Left_Stick_Click.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c0xpc73ovry50"
+path="res://.godot/imported/XboxSeriesX_Left_Stick_Click.png-583c878cd8832ec16d9d309381418b44.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Left_Stick_Click.png"
+dest_files=["res://.godot/imported/XboxSeriesX_Left_Stick_Click.png-583c878cd8832ec16d9d309381418b44.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/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Menu.png b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Menu.png
new file mode 100644
index 0000000..190780e
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Menu.png differ
diff --git a/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Menu.png.import b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Menu.png.import
new file mode 100644
index 0000000..6ff0007
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Menu.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://y70m84f40jkk"
+path="res://.godot/imported/XboxSeriesX_Menu.png-b5260eb2af6d939947fd11289117ab1c.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Menu.png"
+dest_files=["res://.godot/imported/XboxSeriesX_Menu.png-b5260eb2af6d939947fd11289117ab1c.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/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_RB.png b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_RB.png
new file mode 100644
index 0000000..5dcfc6d
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_RB.png differ
diff --git a/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_RB.png.import b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_RB.png.import
new file mode 100644
index 0000000..44914e1
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_RB.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://5j0mm7ydxc5b"
+path="res://.godot/imported/XboxSeriesX_RB.png-8b5fe7ad52beb7dc8003e7331b18e18b.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_RB.png"
+dest_files=["res://.godot/imported/XboxSeriesX_RB.png-8b5fe7ad52beb7dc8003e7331b18e18b.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/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_RT.png b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_RT.png
new file mode 100644
index 0000000..8004286
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_RT.png differ
diff --git a/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_RT.png.import b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_RT.png.import
new file mode 100644
index 0000000..8f521dc
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_RT.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dvsukgwjjn78x"
+path="res://.godot/imported/XboxSeriesX_RT.png-e4f797c7b4ec150868ad67aaecd7e6bc.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_RT.png"
+dest_files=["res://.godot/imported/XboxSeriesX_RT.png-e4f797c7b4ec150868ad67aaecd7e6bc.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/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Right_Stick.png b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Right_Stick.png
new file mode 100644
index 0000000..866be1c
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Right_Stick.png differ
diff --git a/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Right_Stick.png.import b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Right_Stick.png.import
new file mode 100644
index 0000000..8da437a
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Right_Stick.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c1p7p5qpqxqfn"
+path="res://.godot/imported/XboxSeriesX_Right_Stick.png-9c5f7904babde376c469263f16a00eac.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Right_Stick.png"
+dest_files=["res://.godot/imported/XboxSeriesX_Right_Stick.png-9c5f7904babde376c469263f16a00eac.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/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Right_Stick_Click.png b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Right_Stick_Click.png
new file mode 100644
index 0000000..de08508
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Right_Stick_Click.png differ
diff --git a/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Right_Stick_Click.png.import b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Right_Stick_Click.png.import
new file mode 100644
index 0000000..cf66afe
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Right_Stick_Click.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://pmymadotp1l0"
+path="res://.godot/imported/XboxSeriesX_Right_Stick_Click.png-7cd7984c348fb4db2998472d7b950c6a.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Right_Stick_Click.png"
+dest_files=["res://.godot/imported/XboxSeriesX_Right_Stick_Click.png-7cd7984c348fb4db2998472d7b950c6a.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/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Share.png b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Share.png
new file mode 100644
index 0000000..66d9f95
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Share.png differ
diff --git a/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Share.png.import b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Share.png.import
new file mode 100644
index 0000000..cf906c6
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Share.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b8b2oomlnf5tt"
+path="res://.godot/imported/XboxSeriesX_Share.png-39f960085afed02792af365e2b9c52a6.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Share.png"
+dest_files=["res://.godot/imported/XboxSeriesX_Share.png-39f960085afed02792af365e2b9c52a6.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/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_View.png b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_View.png
new file mode 100644
index 0000000..066086a
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_View.png differ
diff --git a/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_View.png.import b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_View.png.import
new file mode 100644
index 0000000..b2f81ab
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_View.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b5wc83dex8igr"
+path="res://.godot/imported/XboxSeriesX_View.png-4584623e9f69c92b5f7aa502e1647455.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_View.png"
+dest_files=["res://.godot/imported/XboxSeriesX_View.png-4584623e9f69c92b5f7aa502e1647455.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/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_X.png b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_X.png
new file mode 100644
index 0000000..e944b3e
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_X.png differ
diff --git a/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_X.png.import b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_X.png.import
new file mode 100644
index 0000000..b461388
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_X.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dqnryo1s6qi45"
+path="res://.godot/imported/XboxSeriesX_X.png-5f1df66dbbce24929b4c3ab5307e19e3.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_X.png"
+dest_files=["res://.godot/imported/XboxSeriesX_X.png-5f1df66dbbce24929b4c3ab5307e19e3.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/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Y.png b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Y.png
new file mode 100644
index 0000000..cf4a997
Binary files /dev/null and b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Y.png differ
diff --git a/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Y.png.import b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Y.png.import
new file mode 100644
index 0000000..b0c08db
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Y.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bx76tv1exmv0p"
+path="res://.godot/imported/XboxSeriesX_Y.png-a7cca5057748346da0259b111678f72f.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Y.png"
+dest_files=["res://.godot/imported/XboxSeriesX_Y.png-a7cca5057748346da0259b111678f72f.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/guide/ui/renderers/controllers/xbox/xbox_controller_renderer.tscn b/addons/guide/ui/renderers/controllers/xbox/xbox_controller_renderer.tscn
new file mode 100644
index 0000000..d27adc9
--- /dev/null
+++ b/addons/guide/ui/renderers/controllers/xbox/xbox_controller_renderer.tscn
@@ -0,0 +1,101 @@
+[gd_scene load_steps=21 format=3 uid="uid://b0dr5w7b7spvo"]
+
+[ext_resource type="PackedScene" uid="uid://bsaylcb5ixjxk" path="res://addons/guide/ui/renderers/controllers/controller_renderer.tscn" id="1_5pcnq"]
+[ext_resource type="Texture2D" uid="uid://cujvw3m7aghgh" path="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_A.png" id="2_kd0r1"]
+[ext_resource type="Texture2D" uid="uid://brgqhp87g40l5" path="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_B.png" id="3_oebii"]
+[ext_resource type="Texture2D" uid="uid://dqnryo1s6qi45" path="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_X.png" id="4_dgucg"]
+[ext_resource type="Texture2D" uid="uid://bx76tv1exmv0p" path="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Y.png" id="5_nbb1e"]
+[ext_resource type="Texture2D" uid="uid://wol4p4f32lfr" path="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Left_Stick.png" id="6_wb84l"]
+[ext_resource type="Texture2D" uid="uid://c0xpc73ovry50" path="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Left_Stick_Click.png" id="7_en4bp"]
+[ext_resource type="Texture2D" uid="uid://c1p7p5qpqxqfn" path="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Right_Stick.png" id="8_3a0jg"]
+[ext_resource type="Texture2D" uid="uid://pmymadotp1l0" path="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Right_Stick_Click.png" id="9_vu674"]
+[ext_resource type="Texture2D" uid="uid://uwqbgrkqv7qc" path="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_LB.png" id="10_4dsgt"]
+[ext_resource type="Texture2D" uid="uid://5j0mm7ydxc5b" path="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_RB.png" id="11_3efc4"]
+[ext_resource type="Texture2D" uid="uid://c1rw1hnlt3dlt" path="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_LT.png" id="12_bylwu"]
+[ext_resource type="Texture2D" uid="uid://dvsukgwjjn78x" path="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_RT.png" id="13_viufm"]
+[ext_resource type="Texture2D" uid="uid://boc4an5lj12lp" path="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad_Up.png" id="14_2uufq"]
+[ext_resource type="Texture2D" uid="uid://bqtskbk6l8v7w" path="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad_Left.png" id="15_52cih"]
+[ext_resource type="Texture2D" uid="uid://gsilbr1bntic" path="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad_Right.png" id="16_j6krd"]
+[ext_resource type="Texture2D" uid="uid://d1fgf8ia7q4vo" path="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Dpad_Down.png" id="17_03a3w"]
+[ext_resource type="Texture2D" uid="uid://y70m84f40jkk" path="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Menu.png" id="18_qlpxc"]
+[ext_resource type="Texture2D" uid="uid://b8b2oomlnf5tt" path="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_Share.png" id="19_24yjp"]
+[ext_resource type="Texture2D" uid="uid://b5wc83dex8igr" path="res://addons/guide/ui/renderers/controllers/xbox/icons/XboxSeriesX_View.png" id="20_0bc5x"]
+
+[node name="ControllerRenderer" instance=ExtResource("1_5pcnq")]
+controller_name_matches = Array[String](["xbox", "xinput"])
+a_button = ExtResource("2_kd0r1")
+b_button = ExtResource("3_oebii")
+x_button = ExtResource("4_dgucg")
+y_button = ExtResource("5_nbb1e")
+left_stick = ExtResource("6_wb84l")
+left_stick_click = ExtResource("7_en4bp")
+right_stick = ExtResource("8_3a0jg")
+right_stick_click = ExtResource("9_vu674")
+left_bumper = ExtResource("10_4dsgt")
+right_bumper = ExtResource("11_3efc4")
+left_trigger = ExtResource("12_bylwu")
+right_trigger = ExtResource("13_viufm")
+dpad_up = ExtResource("14_2uufq")
+dpad_left = ExtResource("15_52cih")
+dpad_right = ExtResource("16_j6krd")
+dpad_down = ExtResource("17_03a3w")
+start = ExtResource("18_qlpxc")
+misc1 = ExtResource("19_24yjp")
+back = ExtResource("20_0bc5x")
+
+[node name="AButton" parent="HBoxContainer/Controls" index="0"]
+texture = ExtResource("2_kd0r1")
+
+[node name="BButton" parent="HBoxContainer/Controls" index="1"]
+texture = ExtResource("3_oebii")
+
+[node name="XButton" parent="HBoxContainer/Controls" index="2"]
+texture = ExtResource("4_dgucg")
+
+[node name="YButton" parent="HBoxContainer/Controls" index="3"]
+texture = ExtResource("5_nbb1e")
+
+[node name="LeftStick" parent="HBoxContainer/Controls" index="4"]
+texture = ExtResource("6_wb84l")
+
+[node name="LeftStickClick" parent="HBoxContainer/Controls" index="5"]
+texture = ExtResource("7_en4bp")
+
+[node name="RightStick" parent="HBoxContainer/Controls" index="6"]
+texture = ExtResource("8_3a0jg")
+
+[node name="RightStickClick" parent="HBoxContainer/Controls" index="7"]
+texture = ExtResource("9_vu674")
+
+[node name="LeftBumper" parent="HBoxContainer/Controls" index="8"]
+texture = ExtResource("10_4dsgt")
+
+[node name="RightBumper" parent="HBoxContainer/Controls" index="9"]
+texture = ExtResource("11_3efc4")
+
+[node name="LeftTrigger" parent="HBoxContainer/Controls" index="10"]
+texture = ExtResource("12_bylwu")
+
+[node name="RightTrigger" parent="HBoxContainer/Controls" index="11"]
+texture = ExtResource("13_viufm")
+
+[node name="DpadUp" parent="HBoxContainer/Controls" index="12"]
+texture = ExtResource("14_2uufq")
+
+[node name="DpadLeft" parent="HBoxContainer/Controls" index="13"]
+texture = ExtResource("15_52cih")
+
+[node name="DpadRight" parent="HBoxContainer/Controls" index="14"]
+texture = ExtResource("16_j6krd")
+
+[node name="DpadDown" parent="HBoxContainer/Controls" index="15"]
+texture = ExtResource("17_03a3w")
+
+[node name="Start" parent="HBoxContainer/Controls" index="16"]
+texture = ExtResource("18_qlpxc")
+
+[node name="Misc1" parent="HBoxContainer/Controls" index="17"]
+texture = ExtResource("19_24yjp")
+
+[node name="Back" parent="HBoxContainer/Controls" index="18"]
+texture = ExtResource("20_0bc5x")
diff --git a/addons/guide/ui/renderers/joy/icons/button_empty.png b/addons/guide/ui/renderers/joy/icons/button_empty.png
new file mode 100644
index 0000000..800bee0
Binary files /dev/null and b/addons/guide/ui/renderers/joy/icons/button_empty.png differ
diff --git a/addons/guide/ui/renderers/joy/icons/button_empty.png.import b/addons/guide/ui/renderers/joy/icons/button_empty.png.import
new file mode 100644
index 0000000..e2bc208
--- /dev/null
+++ b/addons/guide/ui/renderers/joy/icons/button_empty.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://veqjcwokdukw"
+path="res://.godot/imported/button_empty.png-123d9e156dcc9c2e5ff33e4ffbcd7884.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/joy/icons/button_empty.png"
+dest_files=["res://.godot/imported/button_empty.png-123d9e156dcc9c2e5ff33e4ffbcd7884.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/guide/ui/renderers/joy/icons/stick_empty.png b/addons/guide/ui/renderers/joy/icons/stick_empty.png
new file mode 100644
index 0000000..ae363d2
Binary files /dev/null and b/addons/guide/ui/renderers/joy/icons/stick_empty.png differ
diff --git a/addons/guide/ui/renderers/joy/icons/stick_empty.png.import b/addons/guide/ui/renderers/joy/icons/stick_empty.png.import
new file mode 100644
index 0000000..8d805df
--- /dev/null
+++ b/addons/guide/ui/renderers/joy/icons/stick_empty.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://do001o6aysaxo"
+path="res://.godot/imported/stick_empty.png-10fa96eb31f6946670d6998f8648c1db.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/joy/icons/stick_empty.png"
+dest_files=["res://.godot/imported/stick_empty.png-10fa96eb31f6946670d6998f8648c1db.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/guide/ui/renderers/joy/joy_renderer.gd b/addons/guide/ui/renderers/joy/joy_renderer.gd
new file mode 100644
index 0000000..92bdec1
--- /dev/null
+++ b/addons/guide/ui/renderers/joy/joy_renderer.gd
@@ -0,0 +1,69 @@
+@tool
+extends GUIDEIconRenderer
+
+@onready var _stick:Control = %Stick
+@onready var _button:Control = %Button
+@onready var _text:Control = %Text
+@onready var _directions:Control = %Directions
+@onready var _horizontal:Control = %Horizontal
+@onready var _vertical:Control = %Vertical
+
+
+
+func supports(input:GUIDEInput) -> bool:
+ return input is GUIDEInputJoyBase
+
+func render(input:GUIDEInput) -> void:
+ _stick.visible = false
+ _button.visible = false
+ _directions.visible = false
+ _horizontal.visible = false
+ _vertical.visible = false
+ _text.text = ""
+
+
+ if input is GUIDEInputJoyAxis1D:
+ _stick.visible = true
+ match input.axis:
+ JOY_AXIS_LEFT_X:
+ _directions.visible = true
+ _text.text = "1"
+ _horizontal.visible = true
+ JOY_AXIS_RIGHT_X:
+ _directions.visible = true
+ _text.text = "2"
+ _horizontal.visible = true
+ JOY_AXIS_LEFT_Y:
+ _directions.visible = true
+ _text.text = "1"
+ _vertical.visible = true
+ JOY_AXIS_RIGHT_Y:
+ _directions.visible = true
+ _text.text = "2"
+ _vertical.visible = true
+ JOY_AXIS_TRIGGER_LEFT:
+ _text.text = "3"
+ JOY_AXIS_TRIGGER_RIGHT:
+ _text.text = "4"
+
+
+
+ if input is GUIDEInputJoyAxis2D:
+ _stick.visible = true
+ match input.x:
+ JOY_AXIS_LEFT_X, JOY_AXIS_LEFT_Y:
+ _text.text = "1"
+ JOY_AXIS_RIGHT_X, JOY_AXIS_RIGHT_Y:
+ _text.text = "2"
+ _:
+ # well we don't know really what this is but what can we do.
+ _text.text = str(input.x + input.y)
+
+ if input is GUIDEInputJoyButton:
+ _button.visible = true
+ _text.text = str(input.button)
+
+ call("queue_sort")
+
+func cache_key(input:GUIDEInput) -> String:
+ return "a9ced629-de65-4c31-9de0-8e4cbf88a2e0" + input.to_string()
diff --git a/addons/guide/ui/renderers/joy/joy_renderer.tscn b/addons/guide/ui/renderers/joy/joy_renderer.tscn
new file mode 100644
index 0000000..a1dd98e
--- /dev/null
+++ b/addons/guide/ui/renderers/joy/joy_renderer.tscn
@@ -0,0 +1,63 @@
+[gd_scene load_steps=7 format=3 uid="uid://c6sqf8rur1wss"]
+
+[ext_resource type="Script" path="res://addons/guide/ui/renderers/joy/joy_renderer.gd" id="1_u7brn"]
+[ext_resource type="Texture2D" uid="uid://do001o6aysaxo" path="res://addons/guide/ui/renderers/joy/icons/stick_empty.png" id="2_23u2k"]
+[ext_resource type="Texture2D" uid="uid://veqjcwokdukw" path="res://addons/guide/ui/renderers/joy/icons/button_empty.png" id="3_7qfbp"]
+[ext_resource type="FontFile" uid="uid://cu8bvod6tnnwr" path="res://addons/guide/ui/renderers/keyboard/Lato-Black.ttf" id="4_otp86"]
+[ext_resource type="Texture2D" uid="uid://bmgxqbypegjxh" path="res://addons/guide/ui/renderers/textures/arrow_horizontal.svg" id="5_81uyo"]
+[ext_resource type="Texture2D" uid="uid://bu5nlug6uf03w" path="res://addons/guide/ui/renderers/textures/arrow_vertical.svg" id="6_syx8c"]
+
+[node name="JoyRenderer" type="MarginContainer"]
+offset_right = 100.0
+offset_bottom = 100.0
+size_flags_horizontal = 0
+script = ExtResource("1_u7brn")
+
+[node name="HBoxContainer" type="HBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="Controls" type="MarginContainer" parent="HBoxContainer"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(100, 100)
+layout_mode = 2
+
+[node name="Stick" type="TextureRect" parent="HBoxContainer/Controls"]
+unique_name_in_owner = true
+layout_mode = 2
+texture = ExtResource("2_23u2k")
+stretch_mode = 4
+
+[node name="Button" type="TextureRect" parent="HBoxContainer/Controls"]
+unique_name_in_owner = true
+layout_mode = 2
+texture = ExtResource("3_7qfbp")
+stretch_mode = 4
+
+[node name="Text" type="Label" parent="HBoxContainer/Controls"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 4
+theme_override_colors/font_color = Color(0.843137, 0.843137, 0.843137, 1)
+theme_override_fonts/font = ExtResource("4_otp86")
+theme_override_font_sizes/font_size = 50
+text = "1"
+horizontal_alignment = 1
+vertical_alignment = 1
+
+[node name="Directions" type="MarginContainer" parent="HBoxContainer"]
+unique_name_in_owner = true
+visible = false
+custom_minimum_size = Vector2(100, 100)
+layout_mode = 2
+
+[node name="Horizontal" type="TextureRect" parent="HBoxContainer/Directions"]
+unique_name_in_owner = true
+layout_mode = 2
+texture = ExtResource("5_81uyo")
+stretch_mode = 4
+
+[node name="Vertical" type="TextureRect" parent="HBoxContainer/Directions"]
+unique_name_in_owner = true
+layout_mode = 2
+texture = ExtResource("6_syx8c")
+stretch_mode = 4
diff --git a/addons/guide/ui/renderers/keyboard/Lato-Black.ttf b/addons/guide/ui/renderers/keyboard/Lato-Black.ttf
new file mode 100644
index 0000000..4340502
Binary files /dev/null and b/addons/guide/ui/renderers/keyboard/Lato-Black.ttf differ
diff --git a/addons/guide/ui/renderers/keyboard/Lato-Black.ttf.import b/addons/guide/ui/renderers/keyboard/Lato-Black.ttf.import
new file mode 100644
index 0000000..f2b96dd
--- /dev/null
+++ b/addons/guide/ui/renderers/keyboard/Lato-Black.ttf.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="font_data_dynamic"
+type="FontFile"
+uid="uid://cu8bvod6tnnwr"
+path="res://.godot/imported/Lato-Black.ttf-dd70fb4540a062e6fd25f6b3cb73f785.fontdata"
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/keyboard/Lato-Black.ttf"
+dest_files=["res://.godot/imported/Lato-Black.ttf-dd70fb4540a062e6fd25f6b3cb73f785.fontdata"]
+
+[params]
+
+Rendering=null
+antialiasing=1
+generate_mipmaps=false
+disable_embedded_bitmaps=true
+multichannel_signed_distance_field=false
+msdf_pixel_range=8
+msdf_size=48
+allow_system_fallback=true
+force_autohinter=false
+hinting=1
+subpixel_positioning=1
+oversampling=0.0
+Fallbacks=null
+fallbacks=[]
+Compress=null
+compress=true
+preload=[]
+language_support={}
+script_support={}
+opentype_features={}
diff --git a/addons/guide/ui/renderers/keyboard/icons/Blank_White_Normal.png b/addons/guide/ui/renderers/keyboard/icons/Blank_White_Normal.png
new file mode 100644
index 0000000..beee975
Binary files /dev/null and b/addons/guide/ui/renderers/keyboard/icons/Blank_White_Normal.png differ
diff --git a/addons/guide/ui/renderers/keyboard/icons/Blank_White_Normal.png.import b/addons/guide/ui/renderers/keyboard/icons/Blank_White_Normal.png.import
new file mode 100644
index 0000000..99d4b10
--- /dev/null
+++ b/addons/guide/ui/renderers/keyboard/icons/Blank_White_Normal.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b3x586os8uuwb"
+path="res://.godot/imported/Blank_White_Normal.png-71b5933a92dfdd83fd323e35be33b3b2.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/keyboard/icons/Blank_White_Normal.png"
+dest_files=["res://.godot/imported/Blank_White_Normal.png-71b5933a92dfdd83fd323e35be33b3b2.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/guide/ui/renderers/keyboard/key_renderer.gd b/addons/guide/ui/renderers/keyboard/key_renderer.gd
new file mode 100644
index 0000000..3facf61
--- /dev/null
+++ b/addons/guide/ui/renderers/keyboard/key_renderer.gd
@@ -0,0 +1,17 @@
+@tool
+extends GUIDEIconRenderer
+
+@onready var _label:Label = %Label
+
+func supports(input:GUIDEInput) -> bool:
+ return input is GUIDEInputKey
+
+func render(input:GUIDEInput) -> void:
+ var key:Key = input.key
+ var label_key:Key = DisplayServer.keyboard_get_label_from_physical(key)
+ _label.text = OS.get_keycode_string(label_key).strip_edges()
+ size = Vector2.ZERO
+ call("queue_sort")
+
+func cache_key(input:GUIDEInput) -> String:
+ return "ed6923d5-4115-44bd-b35e-2c4102ffc83e" + input.to_string()
diff --git a/addons/guide/ui/renderers/keyboard/key_renderer.tscn b/addons/guide/ui/renderers/keyboard/key_renderer.tscn
new file mode 100644
index 0000000..eacb5b9
--- /dev/null
+++ b/addons/guide/ui/renderers/keyboard/key_renderer.tscn
@@ -0,0 +1,36 @@
+[gd_scene load_steps=4 format=3 uid="uid://toty2e3yx26l"]
+
+[ext_resource type="Script" path="res://addons/guide/ui/renderers/keyboard/key_renderer.gd" id="1_tm2sd"]
+[ext_resource type="Texture2D" uid="uid://b3x586os8uuwb" path="res://addons/guide/ui/renderers/keyboard/icons/Blank_White_Normal.png" id="2_myc15"]
+[ext_resource type="FontFile" uid="uid://cu8bvod6tnnwr" path="res://addons/guide/ui/renderers/keyboard/Lato-Black.ttf" id="3_d3uds"]
+
+[node name="KeyRenderer" type="MarginContainer"]
+custom_minimum_size = Vector2(100, 100)
+offset_right = 267.0
+offset_bottom = 100.0
+size_flags_horizontal = 0
+size_flags_vertical = 0
+script = ExtResource("1_tm2sd")
+
+[node name="NinePatchRect" type="NinePatchRect" parent="."]
+layout_mode = 2
+texture = ExtResource("2_myc15")
+region_rect = Rect2(10, 10, 80, 80)
+patch_margin_left = 29
+patch_margin_top = 30
+patch_margin_right = 29
+patch_margin_bottom = 29
+
+[node name="MarginContainer" type="MarginContainer" parent="."]
+layout_mode = 2
+theme_override_constants/margin_left = 30
+theme_override_constants/margin_right = 30
+
+[node name="Label" type="Label" parent="MarginContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+theme_override_colors/font_color = Color(0.25098, 0.25098, 0.25098, 1)
+theme_override_fonts/font = ExtResource("3_d3uds")
+theme_override_font_sizes/font_size = 45
+text = "Long Long Long"
+horizontal_alignment = 1
diff --git a/addons/guide/ui/renderers/misc/action_renderer.gd b/addons/guide/ui/renderers/misc/action_renderer.gd
new file mode 100644
index 0000000..c39f8a6
--- /dev/null
+++ b/addons/guide/ui/renderers/misc/action_renderer.gd
@@ -0,0 +1,11 @@
+@tool
+extends GUIDEIconRenderer
+
+func supports(input:GUIDEInput) -> bool:
+ return input is GUIDEInputAction
+
+func render(input:GUIDEInput) -> void:
+ pass
+
+func cache_key(input:GUIDEInput) -> String:
+ return "0ecd6608-ba3c-4fc2-83f7-ad61736f1106" # we only have one output, so same cache key
diff --git a/addons/guide/ui/renderers/misc/action_renderer.tscn b/addons/guide/ui/renderers/misc/action_renderer.tscn
new file mode 100644
index 0000000..88cb452
--- /dev/null
+++ b/addons/guide/ui/renderers/misc/action_renderer.tscn
@@ -0,0 +1,16 @@
+[gd_scene load_steps=3 format=3 uid="uid://ortn6jb3wljf"]
+
+[ext_resource type="Script" path="res://addons/guide/ui/renderers/misc/action_renderer.gd" id="1_140q4"]
+[ext_resource type="Texture2D" uid="uid://diwkvjkss2ie" path="res://addons/guide/ui/renderers/textures/action.svg" id="2_uqvia"]
+
+[node name="ActionRenderer" type="MarginContainer"]
+offset_right = 512.0
+offset_bottom = 512.0
+size_flags_horizontal = 0
+script = ExtResource("1_140q4")
+
+[node name="Action" type="TextureRect" parent="."]
+unique_name_in_owner = true
+layout_mode = 2
+texture = ExtResource("2_uqvia")
+stretch_mode = 4
diff --git a/addons/guide/ui/renderers/misc/fallback_renderer.gd b/addons/guide/ui/renderers/misc/fallback_renderer.gd
new file mode 100644
index 0000000..4fa78e1
--- /dev/null
+++ b/addons/guide/ui/renderers/misc/fallback_renderer.gd
@@ -0,0 +1,11 @@
+@tool
+extends GUIDEIconRenderer
+
+func supports(input:GUIDEInput) -> bool:
+ return true
+
+func render(input:GUIDEInput) -> void:
+ pass
+
+func cache_key(input:GUIDEInput) -> String:
+ return "2e130e8b-d5b3-478c-af65-53415adfd6bb" # we only have one output, so same cache key
diff --git a/addons/guide/ui/renderers/misc/fallback_renderer.tscn b/addons/guide/ui/renderers/misc/fallback_renderer.tscn
new file mode 100644
index 0000000..106c8d8
--- /dev/null
+++ b/addons/guide/ui/renderers/misc/fallback_renderer.tscn
@@ -0,0 +1,20 @@
+[gd_scene load_steps=3 format=3 uid="uid://bqf4yoind3a82"]
+
+[ext_resource type="Script" path="res://addons/guide/ui/renderers/misc/fallback_renderer.gd" id="1_mgm3k"]
+[ext_resource type="FontFile" uid="uid://cu8bvod6tnnwr" path="res://addons/guide/ui/renderers/keyboard/Lato-Black.ttf" id="2_5hk8u"]
+
+[node name="FallbackRenderer" type="MarginContainer"]
+offset_right = 512.0
+offset_bottom = 512.0
+size_flags_horizontal = 0
+script = ExtResource("1_mgm3k")
+priority = 100
+
+[node name="Label" type="Label" parent="."]
+custom_minimum_size = Vector2(512, 512)
+layout_mode = 2
+theme_override_fonts/font = ExtResource("2_5hk8u")
+theme_override_font_sizes/font_size = 350
+text = "?"
+horizontal_alignment = 1
+vertical_alignment = 1
diff --git a/addons/guide/ui/renderers/mouse/icons/Mouse_Cursor.png b/addons/guide/ui/renderers/mouse/icons/Mouse_Cursor.png
new file mode 100644
index 0000000..8d52353
Binary files /dev/null and b/addons/guide/ui/renderers/mouse/icons/Mouse_Cursor.png differ
diff --git a/addons/guide/ui/renderers/mouse/icons/Mouse_Cursor.png.import b/addons/guide/ui/renderers/mouse/icons/Mouse_Cursor.png.import
new file mode 100644
index 0000000..21c5623
--- /dev/null
+++ b/addons/guide/ui/renderers/mouse/icons/Mouse_Cursor.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://ci7icm3q4l1sg"
+path="res://.godot/imported/Mouse_Cursor.png-d260e60db8690d81bb10201ace09e70f.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/mouse/icons/Mouse_Cursor.png"
+dest_files=["res://.godot/imported/Mouse_Cursor.png-d260e60db8690d81bb10201ace09e70f.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/guide/ui/renderers/mouse/icons/Mouse_Left_Key_Light.png b/addons/guide/ui/renderers/mouse/icons/Mouse_Left_Key_Light.png
new file mode 100644
index 0000000..22983fa
Binary files /dev/null and b/addons/guide/ui/renderers/mouse/icons/Mouse_Left_Key_Light.png differ
diff --git a/addons/guide/ui/renderers/mouse/icons/Mouse_Left_Key_Light.png.import b/addons/guide/ui/renderers/mouse/icons/Mouse_Left_Key_Light.png.import
new file mode 100644
index 0000000..e6fb06e
--- /dev/null
+++ b/addons/guide/ui/renderers/mouse/icons/Mouse_Left_Key_Light.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://vvgpheda22ew"
+path="res://.godot/imported/Mouse_Left_Key_Light.png-8c9c47fe23d850a53ee5b259032ff1e7.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/mouse/icons/Mouse_Left_Key_Light.png"
+dest_files=["res://.godot/imported/Mouse_Left_Key_Light.png-8c9c47fe23d850a53ee5b259032ff1e7.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/guide/ui/renderers/mouse/icons/Mouse_Middle_Key_Light.png b/addons/guide/ui/renderers/mouse/icons/Mouse_Middle_Key_Light.png
new file mode 100644
index 0000000..d3298c1
Binary files /dev/null and b/addons/guide/ui/renderers/mouse/icons/Mouse_Middle_Key_Light.png differ
diff --git a/examples/sharp_corner_tapering/assets/tex_metal_corner_outer.png.import b/addons/guide/ui/renderers/mouse/icons/Mouse_Middle_Key_Light.png.import
similarity index 63%
rename from examples/sharp_corner_tapering/assets/tex_metal_corner_outer.png.import
rename to addons/guide/ui/renderers/mouse/icons/Mouse_Middle_Key_Light.png.import
index 10587db..74951c7 100644
--- a/examples/sharp_corner_tapering/assets/tex_metal_corner_outer.png.import
+++ b/addons/guide/ui/renderers/mouse/icons/Mouse_Middle_Key_Light.png.import
@@ -2,16 +2,16 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://bc34cuc50as8f"
-path="res://.godot/imported/tex_metal_corner_outer.png-6ea6aecab21ef2cdfa9c20ae8389ee06.ctex"
+uid="uid://bmj244x0jn7v2"
+path="res://.godot/imported/Mouse_Middle_Key_Light.png-d1b621d65964bf35e881bf5a749d0470.ctex"
metadata={
"vram_texture": false
}
[deps]
-source_file="res://examples/sharp_corner_tapering/assets/tex_metal_corner_outer.png"
-dest_files=["res://.godot/imported/tex_metal_corner_outer.png-6ea6aecab21ef2cdfa9c20ae8389ee06.ctex"]
+source_file="res://addons/guide/ui/renderers/mouse/icons/Mouse_Middle_Key_Light.png"
+dest_files=["res://.godot/imported/Mouse_Middle_Key_Light.png-d1b621d65964bf35e881bf5a749d0470.ctex"]
[params]
diff --git a/addons/guide/ui/renderers/mouse/icons/Mouse_Right_Key_Light.png b/addons/guide/ui/renderers/mouse/icons/Mouse_Right_Key_Light.png
new file mode 100644
index 0000000..8106df7
Binary files /dev/null and b/addons/guide/ui/renderers/mouse/icons/Mouse_Right_Key_Light.png differ
diff --git a/addons/guide/ui/renderers/mouse/icons/Mouse_Right_Key_Light.png.import b/addons/guide/ui/renderers/mouse/icons/Mouse_Right_Key_Light.png.import
new file mode 100644
index 0000000..f44ea7f
--- /dev/null
+++ b/addons/guide/ui/renderers/mouse/icons/Mouse_Right_Key_Light.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b8bsyguf4qw6f"
+path="res://.godot/imported/Mouse_Right_Key_Light.png-f1130bc98a1b33064099609790f7efff.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/mouse/icons/Mouse_Right_Key_Light.png"
+dest_files=["res://.godot/imported/Mouse_Right_Key_Light.png-f1130bc98a1b33064099609790f7efff.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/guide/ui/renderers/mouse/icons/Mouse_Side_Key_1_Light.png b/addons/guide/ui/renderers/mouse/icons/Mouse_Side_Key_1_Light.png
new file mode 100644
index 0000000..2011ef0
Binary files /dev/null and b/addons/guide/ui/renderers/mouse/icons/Mouse_Side_Key_1_Light.png differ
diff --git a/addons/guide/ui/renderers/mouse/icons/Mouse_Side_Key_1_Light.png.import b/addons/guide/ui/renderers/mouse/icons/Mouse_Side_Key_1_Light.png.import
new file mode 100644
index 0000000..8bf18da
--- /dev/null
+++ b/addons/guide/ui/renderers/mouse/icons/Mouse_Side_Key_1_Light.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bqxly0g8pftxa"
+path="res://.godot/imported/Mouse_Side_Key_1_Light.png-1e0810994bec8c12ac686f4c33006236.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/mouse/icons/Mouse_Side_Key_1_Light.png"
+dest_files=["res://.godot/imported/Mouse_Side_Key_1_Light.png-1e0810994bec8c12ac686f4c33006236.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/guide/ui/renderers/mouse/icons/Mouse_Side_Key_2_Light.png b/addons/guide/ui/renderers/mouse/icons/Mouse_Side_Key_2_Light.png
new file mode 100644
index 0000000..a2fd2a0
Binary files /dev/null and b/addons/guide/ui/renderers/mouse/icons/Mouse_Side_Key_2_Light.png differ
diff --git a/addons/guide/ui/renderers/mouse/icons/Mouse_Side_Key_2_Light.png.import b/addons/guide/ui/renderers/mouse/icons/Mouse_Side_Key_2_Light.png.import
new file mode 100644
index 0000000..65b8da2
--- /dev/null
+++ b/addons/guide/ui/renderers/mouse/icons/Mouse_Side_Key_2_Light.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://je8rm7jk2nxd"
+path="res://.godot/imported/Mouse_Side_Key_2_Light.png-3a47028a83b486166e55c8b465b98934.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/mouse/icons/Mouse_Side_Key_2_Light.png"
+dest_files=["res://.godot/imported/Mouse_Side_Key_2_Light.png-3a47028a83b486166e55c8b465b98934.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/guide/ui/renderers/mouse/icons/Mouse_Simple_Key_Light.png b/addons/guide/ui/renderers/mouse/icons/Mouse_Simple_Key_Light.png
new file mode 100644
index 0000000..7891ea4
Binary files /dev/null and b/addons/guide/ui/renderers/mouse/icons/Mouse_Simple_Key_Light.png differ
diff --git a/addons/guide/ui/renderers/mouse/icons/Mouse_Simple_Key_Light.png.import b/addons/guide/ui/renderers/mouse/icons/Mouse_Simple_Key_Light.png.import
new file mode 100644
index 0000000..4799ca4
--- /dev/null
+++ b/addons/guide/ui/renderers/mouse/icons/Mouse_Simple_Key_Light.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b3uxk5agbpmab"
+path="res://.godot/imported/Mouse_Simple_Key_Light.png-0d9fe3a005ffb8a731da9e9c204556cb.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/mouse/icons/Mouse_Simple_Key_Light.png"
+dest_files=["res://.godot/imported/Mouse_Simple_Key_Light.png-0d9fe3a005ffb8a731da9e9c204556cb.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/guide/ui/renderers/mouse/mouse_renderer.gd b/addons/guide/ui/renderers/mouse/mouse_renderer.gd
new file mode 100644
index 0000000..2671a99
--- /dev/null
+++ b/addons/guide/ui/renderers/mouse/mouse_renderer.gd
@@ -0,0 +1,87 @@
+@tool
+extends GUIDEIconRenderer
+
+@onready var _controls:Control = %Controls
+@onready var _mouse_left:Control = %MouseLeft
+@onready var _mouse_right:Control = %MouseRight
+@onready var _mouse_middle:Control = %MouseMiddle
+@onready var _mouse_side_a:Control = %MouseSideA
+@onready var _mouse_side_b:Control = %MouseSideB
+@onready var _mouse_blank:Control = %MouseBlank
+@onready var _mouse_cursor:Control = %MouseCursor
+
+
+@onready var _directions:Control = %Directions
+@onready var _left:Control = %Left
+@onready var _right:Control = %Right
+@onready var _up:Control = %Up
+@onready var _down:Control = %Down
+@onready var _horizontal:Control = %Horizontal
+@onready var _vertical:Control = %Vertical
+
+
+
+func supports(input:GUIDEInput) -> bool:
+ return input is GUIDEInputMouseButton or \
+ input is GUIDEInputMouseAxis1D or \
+ input is GUIDEInputMouseAxis2D or \
+ input is GUIDEInputMousePosition
+
+
+func render(input:GUIDEInput) -> void:
+ for child in _controls.get_children():
+ child.visible = false
+ for child in _directions.get_children():
+ child.visible = false
+
+ _directions.visible = false
+
+ if input is GUIDEInputMouseButton:
+ match input.button:
+ MOUSE_BUTTON_LEFT:
+ _mouse_left.visible = true
+ MOUSE_BUTTON_RIGHT:
+ _mouse_right.visible = true
+ MOUSE_BUTTON_MIDDLE:
+ _mouse_middle.visible = true
+ MOUSE_BUTTON_WHEEL_UP:
+ _directions.visible = true
+ _up.visible = true
+ _mouse_middle.visible = true
+ MOUSE_BUTTON_WHEEL_DOWN:
+ _directions.visible = true
+ _down.visible = true
+ _mouse_middle.visible = true
+ MOUSE_BUTTON_WHEEL_LEFT:
+ _directions.visible = true
+ _left.visible = true
+ _mouse_middle.visible = true
+ MOUSE_BUTTON_WHEEL_RIGHT:
+ _directions.visible = true
+ _right.visible = true
+ _mouse_middle.visible = true
+ MOUSE_BUTTON_XBUTTON1:
+ _mouse_side_a.visible = true
+ MOUSE_BUTTON_XBUTTON2:
+ _mouse_side_b.visible = true
+
+ if input is GUIDEInputMouseAxis1D:
+ if input.axis == GUIDEInputMouseAxis1D.GUIDEInputMouseAxis.X:
+ _mouse_blank.visible = true
+ _directions.visible = true
+ _horizontal.visible = true
+ else:
+ _mouse_blank.visible = true
+ _directions.visible = true
+ _vertical.visible = true
+
+ if input is GUIDEInputMouseAxis2D:
+ _mouse_blank.visible = true
+
+ if input is GUIDEInputMousePosition:
+ _mouse_cursor.visible = true
+
+ call("queue_sort")
+
+func cache_key(input:GUIDEInput) -> String:
+ return "7e27520a-b6d8-4451-858d-e94330c82e85" + input.to_string()
diff --git a/addons/guide/ui/renderers/mouse/mouse_renderer.tscn b/addons/guide/ui/renderers/mouse/mouse_renderer.tscn
new file mode 100644
index 0000000..7e51e7d
--- /dev/null
+++ b/addons/guide/ui/renderers/mouse/mouse_renderer.tscn
@@ -0,0 +1,124 @@
+[gd_scene load_steps=15 format=3 uid="uid://bfl6dbw21xqs1"]
+
+[ext_resource type="Script" path="res://addons/guide/ui/renderers/mouse/mouse_renderer.gd" id="1_amutf"]
+[ext_resource type="Texture2D" uid="uid://vvgpheda22ew" path="res://addons/guide/ui/renderers/mouse/icons/Mouse_Left_Key_Light.png" id="2_6vk7n"]
+[ext_resource type="Texture2D" uid="uid://b8bsyguf4qw6f" path="res://addons/guide/ui/renderers/mouse/icons/Mouse_Right_Key_Light.png" id="3_aaqrj"]
+[ext_resource type="Texture2D" uid="uid://bmj244x0jn7v2" path="res://addons/guide/ui/renderers/mouse/icons/Mouse_Middle_Key_Light.png" id="4_gprek"]
+[ext_resource type="Texture2D" uid="uid://bqxly0g8pftxa" path="res://addons/guide/ui/renderers/mouse/icons/Mouse_Side_Key_1_Light.png" id="6_adi64"]
+[ext_resource type="Texture2D" uid="uid://1swh072gtbb4" path="res://addons/guide/ui/renderers/textures/arrow_left.svg" id="6_fo4h0"]
+[ext_resource type="Texture2D" uid="uid://je8rm7jk2nxd" path="res://addons/guide/ui/renderers/mouse/icons/Mouse_Side_Key_2_Light.png" id="7_bbgo7"]
+[ext_resource type="Texture2D" uid="uid://cjvs04qsrj8ap" path="res://addons/guide/ui/renderers/textures/arrow_right.svg" id="7_t242p"]
+[ext_resource type="Texture2D" uid="uid://ni6lsbx1d2hf" path="res://addons/guide/ui/renderers/textures/arrow_up.svg" id="8_a7prs"]
+[ext_resource type="Texture2D" uid="uid://ci7icm3q4l1sg" path="res://addons/guide/ui/renderers/mouse/icons/Mouse_Cursor.png" id="8_g4vq6"]
+[ext_resource type="Texture2D" uid="uid://oq2vvwgbdsh7" path="res://addons/guide/ui/renderers/textures/arrow_down.svg" id="9_dfyjd"]
+[ext_resource type="Texture2D" uid="uid://bmgxqbypegjxh" path="res://addons/guide/ui/renderers/textures/arrow_horizontal.svg" id="12_xdqh7"]
+[ext_resource type="Texture2D" uid="uid://b3uxk5agbpmab" path="res://addons/guide/ui/renderers/mouse/icons/Mouse_Simple_Key_Light.png" id="13_1bw2l"]
+[ext_resource type="Texture2D" uid="uid://bu5nlug6uf03w" path="res://addons/guide/ui/renderers/textures/arrow_vertical.svg" id="13_yoogt"]
+
+[node name="MouseRenderer" type="MarginContainer"]
+offset_right = 100.0
+offset_bottom = 100.0
+size_flags_horizontal = 0
+script = ExtResource("1_amutf")
+
+[node name="HBoxContainer" type="HBoxContainer" parent="."]
+layout_mode = 2
+size_flags_horizontal = 0
+
+[node name="Controls" type="MarginContainer" parent="HBoxContainer"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(100, 100)
+layout_mode = 2
+
+[node name="MouseBlank" type="TextureRect" parent="HBoxContainer/Controls"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+texture = ExtResource("13_1bw2l")
+stretch_mode = 4
+
+[node name="MouseLeft" type="TextureRect" parent="HBoxContainer/Controls"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+texture = ExtResource("2_6vk7n")
+stretch_mode = 4
+
+[node name="MouseRight" type="TextureRect" parent="HBoxContainer/Controls"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+texture = ExtResource("3_aaqrj")
+stretch_mode = 4
+
+[node name="MouseMiddle" type="TextureRect" parent="HBoxContainer/Controls"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+texture = ExtResource("4_gprek")
+stretch_mode = 4
+
+[node name="MouseSideA" type="TextureRect" parent="HBoxContainer/Controls"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+texture = ExtResource("6_adi64")
+stretch_mode = 4
+
+[node name="MouseSideB" type="TextureRect" parent="HBoxContainer/Controls"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+texture = ExtResource("7_bbgo7")
+stretch_mode = 4
+
+[node name="MouseCursor" type="TextureRect" parent="HBoxContainer/Controls"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+texture = ExtResource("8_g4vq6")
+stretch_mode = 4
+
+[node name="Directions" type="MarginContainer" parent="HBoxContainer"]
+unique_name_in_owner = true
+visible = false
+custom_minimum_size = Vector2(100, 100)
+layout_mode = 2
+
+[node name="Left" type="TextureRect" parent="HBoxContainer/Directions"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+texture = ExtResource("6_fo4h0")
+stretch_mode = 4
+
+[node name="Right" type="TextureRect" parent="HBoxContainer/Directions"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+texture = ExtResource("7_t242p")
+stretch_mode = 4
+
+[node name="Up" type="TextureRect" parent="HBoxContainer/Directions"]
+unique_name_in_owner = true
+layout_mode = 2
+texture = ExtResource("8_a7prs")
+stretch_mode = 4
+
+[node name="Down" type="TextureRect" parent="HBoxContainer/Directions"]
+unique_name_in_owner = true
+layout_mode = 2
+texture = ExtResource("9_dfyjd")
+stretch_mode = 4
+
+[node name="Horizontal" type="TextureRect" parent="HBoxContainer/Directions"]
+unique_name_in_owner = true
+layout_mode = 2
+texture = ExtResource("12_xdqh7")
+stretch_mode = 4
+
+[node name="Vertical" type="TextureRect" parent="HBoxContainer/Directions"]
+unique_name_in_owner = true
+layout_mode = 2
+texture = ExtResource("13_yoogt")
+stretch_mode = 4
diff --git a/addons/guide/ui/renderers/textures/action.svg b/addons/guide/ui/renderers/textures/action.svg
new file mode 100644
index 0000000..1da724c
--- /dev/null
+++ b/addons/guide/ui/renderers/textures/action.svg
@@ -0,0 +1,10 @@
+
+
+
diff --git a/addons/guide/ui/renderers/textures/action.svg.import b/addons/guide/ui/renderers/textures/action.svg.import
new file mode 100644
index 0000000..7c0aa8d
--- /dev/null
+++ b/addons/guide/ui/renderers/textures/action.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://diwkvjkss2ie"
+path="res://.godot/imported/action.svg-6100da2ab8ea5d289c6e91ccdfb53aca.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/textures/action.svg"
+dest_files=["res://.godot/imported/action.svg-6100da2ab8ea5d289c6e91ccdfb53aca.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/addons/guide/ui/renderers/textures/arrow_all_directions.svg b/addons/guide/ui/renderers/textures/arrow_all_directions.svg
new file mode 100644
index 0000000..c0053b6
--- /dev/null
+++ b/addons/guide/ui/renderers/textures/arrow_all_directions.svg
@@ -0,0 +1,8 @@
+
+
+
diff --git a/addons/guide/ui/renderers/textures/arrow_all_directions.svg.import b/addons/guide/ui/renderers/textures/arrow_all_directions.svg.import
new file mode 100644
index 0000000..20bd950
--- /dev/null
+++ b/addons/guide/ui/renderers/textures/arrow_all_directions.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dwb1h8sfsccyy"
+path="res://.godot/imported/arrow_all_directions.svg-c87a4938e66e69435ad57c677b38771f.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/textures/arrow_all_directions.svg"
+dest_files=["res://.godot/imported/arrow_all_directions.svg-c87a4938e66e69435ad57c677b38771f.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/addons/guide/ui/renderers/textures/arrow_down.svg b/addons/guide/ui/renderers/textures/arrow_down.svg
new file mode 100644
index 0000000..b1a193a
--- /dev/null
+++ b/addons/guide/ui/renderers/textures/arrow_down.svg
@@ -0,0 +1,8 @@
+
+
+
diff --git a/addons/guide/ui/renderers/textures/arrow_down.svg.import b/addons/guide/ui/renderers/textures/arrow_down.svg.import
new file mode 100644
index 0000000..f624ed8
--- /dev/null
+++ b/addons/guide/ui/renderers/textures/arrow_down.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://oq2vvwgbdsh7"
+path="res://.godot/imported/arrow_down.svg-88a3b47c68c37638cef21944ad9cda50.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/textures/arrow_down.svg"
+dest_files=["res://.godot/imported/arrow_down.svg-88a3b47c68c37638cef21944ad9cda50.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/addons/guide/ui/renderers/textures/arrow_horizontal.svg b/addons/guide/ui/renderers/textures/arrow_horizontal.svg
new file mode 100644
index 0000000..ae0146f
--- /dev/null
+++ b/addons/guide/ui/renderers/textures/arrow_horizontal.svg
@@ -0,0 +1,8 @@
+
+
+
diff --git a/addons/guide/ui/renderers/textures/arrow_horizontal.svg.import b/addons/guide/ui/renderers/textures/arrow_horizontal.svg.import
new file mode 100644
index 0000000..8975345
--- /dev/null
+++ b/addons/guide/ui/renderers/textures/arrow_horizontal.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bmgxqbypegjxh"
+path="res://.godot/imported/arrow_horizontal.svg-5fd469f78a3e46cba20723a7b243bca1.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/textures/arrow_horizontal.svg"
+dest_files=["res://.godot/imported/arrow_horizontal.svg-5fd469f78a3e46cba20723a7b243bca1.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/addons/guide/ui/renderers/textures/arrow_left.svg b/addons/guide/ui/renderers/textures/arrow_left.svg
new file mode 100644
index 0000000..9f94969
--- /dev/null
+++ b/addons/guide/ui/renderers/textures/arrow_left.svg
@@ -0,0 +1,8 @@
+
+
+
diff --git a/addons/guide/ui/renderers/textures/arrow_left.svg.import b/addons/guide/ui/renderers/textures/arrow_left.svg.import
new file mode 100644
index 0000000..2b22128
--- /dev/null
+++ b/addons/guide/ui/renderers/textures/arrow_left.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://1swh072gtbb4"
+path="res://.godot/imported/arrow_left.svg-2a189e6eec3713a64220cf9427e1f45c.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/textures/arrow_left.svg"
+dest_files=["res://.godot/imported/arrow_left.svg-2a189e6eec3713a64220cf9427e1f45c.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/addons/guide/ui/renderers/textures/arrow_right.svg b/addons/guide/ui/renderers/textures/arrow_right.svg
new file mode 100644
index 0000000..7516511
--- /dev/null
+++ b/addons/guide/ui/renderers/textures/arrow_right.svg
@@ -0,0 +1,8 @@
+
+
+
diff --git a/addons/guide/ui/renderers/textures/arrow_right.svg.import b/addons/guide/ui/renderers/textures/arrow_right.svg.import
new file mode 100644
index 0000000..2b784e3
--- /dev/null
+++ b/addons/guide/ui/renderers/textures/arrow_right.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cjvs04qsrj8ap"
+path="res://.godot/imported/arrow_right.svg-83b2fe427227f253ed212a8b1c56acb4.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/textures/arrow_right.svg"
+dest_files=["res://.godot/imported/arrow_right.svg-83b2fe427227f253ed212a8b1c56acb4.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/addons/guide/ui/renderers/textures/arrow_up.svg b/addons/guide/ui/renderers/textures/arrow_up.svg
new file mode 100644
index 0000000..8b37520
--- /dev/null
+++ b/addons/guide/ui/renderers/textures/arrow_up.svg
@@ -0,0 +1,8 @@
+
+
+
diff --git a/addons/guide/ui/renderers/textures/arrow_up.svg.import b/addons/guide/ui/renderers/textures/arrow_up.svg.import
new file mode 100644
index 0000000..83e63ee
--- /dev/null
+++ b/addons/guide/ui/renderers/textures/arrow_up.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://ni6lsbx1d2hf"
+path="res://.godot/imported/arrow_up.svg-56e16fd95d307eb9666c8ac4e78e2b97.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/textures/arrow_up.svg"
+dest_files=["res://.godot/imported/arrow_up.svg-56e16fd95d307eb9666c8ac4e78e2b97.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/addons/guide/ui/renderers/textures/arrow_vertical.svg b/addons/guide/ui/renderers/textures/arrow_vertical.svg
new file mode 100644
index 0000000..2bf3493
--- /dev/null
+++ b/addons/guide/ui/renderers/textures/arrow_vertical.svg
@@ -0,0 +1,8 @@
+
+
+
diff --git a/addons/guide/ui/renderers/textures/arrow_vertical.svg.import b/addons/guide/ui/renderers/textures/arrow_vertical.svg.import
new file mode 100644
index 0000000..7d87170
--- /dev/null
+++ b/addons/guide/ui/renderers/textures/arrow_vertical.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bu5nlug6uf03w"
+path="res://.godot/imported/arrow_vertical.svg-17983361d36ac9313b8d80f7240cf6aa.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/textures/arrow_vertical.svg"
+dest_files=["res://.godot/imported/arrow_vertical.svg-17983361d36ac9313b8d80f7240cf6aa.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/addons/guide/ui/renderers/touch/icons/touch_1_finger.png b/addons/guide/ui/renderers/touch/icons/touch_1_finger.png
new file mode 100644
index 0000000..86ef0be
Binary files /dev/null and b/addons/guide/ui/renderers/touch/icons/touch_1_finger.png differ
diff --git a/addons/guide/ui/renderers/touch/icons/touch_1_finger.png.import b/addons/guide/ui/renderers/touch/icons/touch_1_finger.png.import
new file mode 100644
index 0000000..15f71ab
--- /dev/null
+++ b/addons/guide/ui/renderers/touch/icons/touch_1_finger.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c5nwnp5cjny7m"
+path="res://.godot/imported/touch_1_finger.png-c21ed1a6c694f6c9460ef451efcf36e2.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/touch/icons/touch_1_finger.png"
+dest_files=["res://.godot/imported/touch_1_finger.png-c21ed1a6c694f6c9460ef451efcf36e2.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/guide/ui/renderers/touch/icons/touch_2_fingers.png b/addons/guide/ui/renderers/touch/icons/touch_2_fingers.png
new file mode 100644
index 0000000..354ff8d
Binary files /dev/null and b/addons/guide/ui/renderers/touch/icons/touch_2_fingers.png differ
diff --git a/addons/guide/ui/renderers/touch/icons/touch_2_fingers.png.import b/addons/guide/ui/renderers/touch/icons/touch_2_fingers.png.import
new file mode 100644
index 0000000..5e7dfd6
--- /dev/null
+++ b/addons/guide/ui/renderers/touch/icons/touch_2_fingers.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bllhe78a1yo6"
+path="res://.godot/imported/touch_2_fingers.png-f5e5340b938b807b9249837228cb3c96.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/touch/icons/touch_2_fingers.png"
+dest_files=["res://.godot/imported/touch_2_fingers.png-f5e5340b938b807b9249837228cb3c96.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/guide/ui/renderers/touch/icons/touch_3_fingers.png b/addons/guide/ui/renderers/touch/icons/touch_3_fingers.png
new file mode 100644
index 0000000..09a07d9
Binary files /dev/null and b/addons/guide/ui/renderers/touch/icons/touch_3_fingers.png differ
diff --git a/addons/guide/ui/renderers/touch/icons/touch_3_fingers.png.import b/addons/guide/ui/renderers/touch/icons/touch_3_fingers.png.import
new file mode 100644
index 0000000..180b468
--- /dev/null
+++ b/addons/guide/ui/renderers/touch/icons/touch_3_fingers.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bwhqf2nmm5q1w"
+path="res://.godot/imported/touch_3_fingers.png-1b0a5171a90b9f0d4adf18b2d204fed6.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/touch/icons/touch_3_fingers.png"
+dest_files=["res://.godot/imported/touch_3_fingers.png-1b0a5171a90b9f0d4adf18b2d204fed6.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/guide/ui/renderers/touch/icons/touch_4_fingers.png b/addons/guide/ui/renderers/touch/icons/touch_4_fingers.png
new file mode 100644
index 0000000..288c40c
Binary files /dev/null and b/addons/guide/ui/renderers/touch/icons/touch_4_fingers.png differ
diff --git a/addons/guide/ui/renderers/touch/icons/touch_4_fingers.png.import b/addons/guide/ui/renderers/touch/icons/touch_4_fingers.png.import
new file mode 100644
index 0000000..02e6cd4
--- /dev/null
+++ b/addons/guide/ui/renderers/touch/icons/touch_4_fingers.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cjw5m42gufghr"
+path="res://.godot/imported/touch_4_fingers.png-feb0257c01dc7e2234eb7bd7feabd57d.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/touch/icons/touch_4_fingers.png"
+dest_files=["res://.godot/imported/touch_4_fingers.png-feb0257c01dc7e2234eb7bd7feabd57d.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/guide/ui/renderers/touch/icons/touch_rotate.png b/addons/guide/ui/renderers/touch/icons/touch_rotate.png
new file mode 100644
index 0000000..6c1af5b
Binary files /dev/null and b/addons/guide/ui/renderers/touch/icons/touch_rotate.png differ
diff --git a/addons/guide/ui/renderers/touch/icons/touch_rotate.png.import b/addons/guide/ui/renderers/touch/icons/touch_rotate.png.import
new file mode 100644
index 0000000..0975cd0
--- /dev/null
+++ b/addons/guide/ui/renderers/touch/icons/touch_rotate.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bxj4t5vjx7o3w"
+path="res://.godot/imported/touch_rotate.png-22fc9d2b74759b3c29981107fa4d935b.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/touch/icons/touch_rotate.png"
+dest_files=["res://.godot/imported/touch_rotate.png-22fc9d2b74759b3c29981107fa4d935b.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/guide/ui/renderers/touch/icons/touch_zoom.png b/addons/guide/ui/renderers/touch/icons/touch_zoom.png
new file mode 100644
index 0000000..37e0cb8
Binary files /dev/null and b/addons/guide/ui/renderers/touch/icons/touch_zoom.png differ
diff --git a/addons/guide/ui/renderers/touch/icons/touch_zoom.png.import b/addons/guide/ui/renderers/touch/icons/touch_zoom.png.import
new file mode 100644
index 0000000..73bc5e3
--- /dev/null
+++ b/addons/guide/ui/renderers/touch/icons/touch_zoom.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cutplj0nhphk"
+path="res://.godot/imported/touch_zoom.png-6fb619cb5fcce5feede2a4a5f40428ee.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/guide/ui/renderers/touch/icons/touch_zoom.png"
+dest_files=["res://.godot/imported/touch_zoom.png-6fb619cb5fcce5feede2a4a5f40428ee.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/guide/ui/renderers/touch/touch_renderer.gd b/addons/guide/ui/renderers/touch/touch_renderer.gd
new file mode 100644
index 0000000..ad2284e
--- /dev/null
+++ b/addons/guide/ui/renderers/touch/touch_renderer.gd
@@ -0,0 +1,73 @@
+@tool
+extends GUIDEIconRenderer
+
+const GUIDEInputTouchBase = preload("../../../inputs/guide_input_touch_base.gd")
+
+@onready var _controls:Control = %Controls
+@onready var _1_finger:Control = %T1Finger
+@onready var _2_finger:Control = %T2Fingers
+@onready var _3_finger:Control = %T3Fingers
+@onready var _4_finger:Control = %T4Fingers
+@onready var _rotate:Control = %Rotate
+@onready var _zoom:Control = %Zoom
+
+
+@onready var _directions:Control = %Directions
+@onready var _horizontal:Control = %Horizontal
+@onready var _vertical:Control = %Vertical
+@onready var _axis2d:Control = %Axis2D
+
+
+
+func supports(input:GUIDEInput) -> bool:
+ return input is GUIDEInputTouchAxis1D or \
+ input is GUIDEInputTouchAxis2D or \
+ input is GUIDEInputTouchPosition or \
+ input is GUIDEInputTouchAngle or \
+ input is GUIDEInputTouchDistance
+
+
+
+func render(input:GUIDEInput) -> void:
+ for child in _controls.get_children():
+ child.visible = false
+ for child in _directions.get_children():
+ child.visible = false
+
+ _directions.visible = false
+
+ if input is GUIDEInputTouchBase:
+ match input.finger_count:
+ 2:
+ _2_finger.visible = true
+ 3:
+ _3_finger.visible = true
+ 4:
+ _4_finger.visible = true
+ _:
+ # we have no icons for more than 4 fingers, so everything else gets
+ # the 1 finger icon
+ _1_finger.visible = true
+
+ if input is GUIDEInputTouchAxis2D:
+ _directions.visible = true
+ _axis2d.visible = true
+
+ if input is GUIDEInputTouchAxis1D:
+ _directions.visible = true
+ match input.axis:
+ GUIDEInputTouchAxis1D.GUIDEInputTouchAxis.X:
+ _horizontal.visible = true
+ GUIDEInputTouchAxis1D.GUIDEInputTouchAxis.X:
+ _vertical.visible = true
+
+ if input is GUIDEInputTouchDistance:
+ _zoom.visible = true
+
+ if input is GUIDEInputTouchAngle:
+ _rotate.visible = true
+
+ call("queue_sort")
+
+func cache_key(input:GUIDEInput) -> String:
+ return "1f4c5082-d419-465f-aba8-f889caaff335" + input.to_string()
diff --git a/addons/guide/ui/renderers/touch/touch_renderer.tscn b/addons/guide/ui/renderers/touch/touch_renderer.tscn
new file mode 100644
index 0000000..b54a0e8
--- /dev/null
+++ b/addons/guide/ui/renderers/touch/touch_renderer.tscn
@@ -0,0 +1,93 @@
+[gd_scene load_steps=11 format=3 uid="uid://ykuou1deo5ub"]
+
+[ext_resource type="Script" path="res://addons/guide/ui/renderers/touch/touch_renderer.gd" id="1_heixj"]
+[ext_resource type="Texture2D" uid="uid://c5nwnp5cjny7m" path="res://addons/guide/ui/renderers/touch/icons/touch_1_finger.png" id="2_4mplc"]
+[ext_resource type="Texture2D" uid="uid://bllhe78a1yo6" path="res://addons/guide/ui/renderers/touch/icons/touch_2_fingers.png" id="3_0dcqu"]
+[ext_resource type="Texture2D" uid="uid://bwhqf2nmm5q1w" path="res://addons/guide/ui/renderers/touch/icons/touch_3_fingers.png" id="4_mgq0v"]
+[ext_resource type="Texture2D" uid="uid://cjw5m42gufghr" path="res://addons/guide/ui/renderers/touch/icons/touch_4_fingers.png" id="5_qoicp"]
+[ext_resource type="Texture2D" uid="uid://bxj4t5vjx7o3w" path="res://addons/guide/ui/renderers/touch/icons/touch_rotate.png" id="6_4jaqi"]
+[ext_resource type="Texture2D" uid="uid://cutplj0nhphk" path="res://addons/guide/ui/renderers/touch/icons/touch_zoom.png" id="7_gvy7l"]
+[ext_resource type="Texture2D" uid="uid://bmgxqbypegjxh" path="res://addons/guide/ui/renderers/textures/arrow_horizontal.svg" id="8_m5om5"]
+[ext_resource type="Texture2D" uid="uid://dwb1h8sfsccyy" path="res://addons/guide/ui/renderers/textures/arrow_all_directions.svg" id="9_5gr2q"]
+[ext_resource type="Texture2D" uid="uid://bu5nlug6uf03w" path="res://addons/guide/ui/renderers/textures/arrow_vertical.svg" id="10_4vghq"]
+
+[node name="TouchRenderer" type="MarginContainer"]
+offset_right = 100.0
+offset_bottom = 100.0
+size_flags_horizontal = 0
+script = ExtResource("1_heixj")
+
+[node name="HBoxContainer" type="HBoxContainer" parent="."]
+layout_mode = 2
+size_flags_horizontal = 0
+
+[node name="Controls" type="MarginContainer" parent="HBoxContainer"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(100, 100)
+layout_mode = 2
+
+[node name="T1Finger" type="TextureRect" parent="HBoxContainer/Controls"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+texture = ExtResource("2_4mplc")
+stretch_mode = 4
+
+[node name="T2Fingers" type="TextureRect" parent="HBoxContainer/Controls"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+texture = ExtResource("3_0dcqu")
+stretch_mode = 4
+
+[node name="T3Fingers" type="TextureRect" parent="HBoxContainer/Controls"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+texture = ExtResource("4_mgq0v")
+stretch_mode = 4
+
+[node name="T4Fingers" type="TextureRect" parent="HBoxContainer/Controls"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+texture = ExtResource("5_qoicp")
+stretch_mode = 4
+
+[node name="Rotate" type="TextureRect" parent="HBoxContainer/Controls"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+texture = ExtResource("6_4jaqi")
+stretch_mode = 4
+
+[node name="Zoom" type="TextureRect" parent="HBoxContainer/Controls"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+texture = ExtResource("7_gvy7l")
+stretch_mode = 4
+
+[node name="Directions" type="MarginContainer" parent="HBoxContainer"]
+unique_name_in_owner = true
+visible = false
+custom_minimum_size = Vector2(100, 100)
+layout_mode = 2
+
+[node name="Horizontal" type="TextureRect" parent="HBoxContainer/Directions"]
+unique_name_in_owner = true
+layout_mode = 2
+texture = ExtResource("8_m5om5")
+stretch_mode = 4
+
+[node name="Vertical" type="TextureRect" parent="HBoxContainer/Directions"]
+unique_name_in_owner = true
+layout_mode = 2
+texture = ExtResource("10_4vghq")
+stretch_mode = 4
+
+[node name="Axis2D" type="TextureRect" parent="HBoxContainer/Directions"]
+unique_name_in_owner = true
+layout_mode = 2
+texture = ExtResource("9_5gr2q")
+stretch_mode = 4
diff --git a/addons/guide/ui/text_providers/controllers/controller_text_provider.gd b/addons/guide/ui/text_providers/controllers/controller_text_provider.gd
new file mode 100644
index 0000000..3a569ee
--- /dev/null
+++ b/addons/guide/ui/text_providers/controllers/controller_text_provider.gd
@@ -0,0 +1,115 @@
+extends GUIDETextProvider
+
+func _init():
+ priority = -1
+
+func _controller_names() -> Array[String]:
+ return []
+
+func _a_button_name() -> String:
+ return "A"
+
+func _b_button_name() -> String:
+ return "B"
+
+func _x_button_name() -> String:
+ return "X"
+
+func _y_button_name() -> String:
+ return "Y"
+
+func _left_bumper_name() -> String:
+ return "LB"
+
+func _right_bumper_name() -> String:
+ return "RB"
+
+func _left_trigger_name() -> String:
+ return "LT"
+
+func _right_trigger_name() -> String:
+ return "RT"
+
+func _back_button_name() -> String:
+ return "Back"
+
+func _misc_1_button_name() -> String:
+ return "Misc 1"
+
+func _start_button_name() -> String:
+ return "Start"
+
+
+func supports(input:GUIDEInput) -> bool:
+ var controller_name = GUIDEInputFormatter._joy_name_for_input(input)
+ if controller_name == "":
+ return false
+
+ var haystack = controller_name.to_lower()
+ for needle in _controller_names():
+ if haystack.contains(needle.to_lower()):
+ return true
+
+ return false
+
+func _format(input:String) -> String:
+ return "[%s]" % [input]
+
+
+func get_text(input:GUIDEInput) -> String:
+ if input is GUIDEInputJoyAxis1D:
+ match input.axis:
+ JOY_AXIS_LEFT_X:
+ return _format(tr("Left Stick Horizontal"))
+ JOY_AXIS_LEFT_Y:
+ return _format(tr("Left Stick Vertical"))
+ JOY_AXIS_RIGHT_X:
+ return _format(tr("Right Stick Horizontal"))
+ JOY_AXIS_RIGHT_Y:
+ return _format(tr("Right Stick Vertical"))
+ JOY_AXIS_TRIGGER_LEFT:
+ return _format(tr(_left_trigger_name()))
+ JOY_AXIS_TRIGGER_RIGHT:
+ return _format(tr(_right_trigger_name()))
+
+ if input is GUIDEInputJoyAxis2D:
+ match input.x:
+ JOY_AXIS_LEFT_X, JOY_AXIS_LEFT_Y:
+ return _format(tr("Left Stick"))
+ JOY_AXIS_RIGHT_X, JOY_AXIS_RIGHT_Y:
+ return _format(tr("Right Stick"))
+
+ if input is GUIDEInputJoyButton:
+ match input.button:
+ JOY_BUTTON_A:
+ return _format(tr(_a_button_name()))
+ JOY_BUTTON_B:
+ return _format(tr(_b_button_name()))
+ JOY_BUTTON_X:
+ return _format(tr(_x_button_name()))
+ JOY_BUTTON_Y:
+ return _format(tr(_y_button_name()))
+ JOY_BUTTON_DPAD_LEFT:
+ return _format(tr("DPAD Left"))
+ JOY_BUTTON_DPAD_RIGHT:
+ return _format(tr("DPAD Right"))
+ JOY_BUTTON_DPAD_UP:
+ return _format(tr("DPAD Up"))
+ JOY_BUTTON_DPAD_DOWN:
+ return _format(tr("DPAD Down"))
+ JOY_BUTTON_LEFT_SHOULDER:
+ return _format(tr(_left_bumper_name()))
+ JOY_BUTTON_RIGHT_SHOULDER:
+ return _format(tr(_right_bumper_name()))
+ JOY_BUTTON_LEFT_STICK:
+ return _format(tr("Left Stick"))
+ JOY_BUTTON_RIGHT_STICK:
+ return _format(tr("Right Stick"))
+ JOY_BUTTON_BACK:
+ return _format(tr(_back_button_name()))
+ JOY_BUTTON_MISC1:
+ return _format(tr(_misc_1_button_name()))
+ JOY_BUTTON_START:
+ return _format(tr(_start_button_name()))
+
+ return _format("??")
diff --git a/addons/guide/ui/text_providers/controllers/playstation/playstation_controller_text_provider.gd b/addons/guide/ui/text_providers/controllers/playstation/playstation_controller_text_provider.gd
new file mode 100644
index 0000000..786f145
--- /dev/null
+++ b/addons/guide/ui/text_providers/controllers/playstation/playstation_controller_text_provider.gd
@@ -0,0 +1,37 @@
+extends "res://addons/guide/ui/text_providers/controllers/controller_text_provider.gd"
+
+func _controller_names() -> Array[String]:
+ return ["DualSense", "DualShock", "Playstation", "PS3", "PS4", "PS5"]
+
+func _a_button_name() -> String:
+ return "Cross"
+
+func _b_button_name() -> String:
+ return "Circle"
+
+func _x_button_name() -> String:
+ return "Square"
+
+func _y_button_name() -> String:
+ return "Triangle"
+
+func _left_bumper_name() -> String:
+ return "L1"
+
+func _right_bumper_name() -> String:
+ return "R1"
+
+func _left_trigger_name() -> String:
+ return "L2"
+
+func _right_trigger_name() -> String:
+ return "R2"
+
+func _back_button_name() -> String:
+ return "Share"
+
+func _misc_1_button_name() -> String:
+ return "Microphone"
+
+func _start_button_name() -> String:
+ return "Options"
diff --git a/addons/guide/ui/text_providers/controllers/switch/switch_controller_text_provider.gd b/addons/guide/ui/text_providers/controllers/switch/switch_controller_text_provider.gd
new file mode 100644
index 0000000..97fbe5d
--- /dev/null
+++ b/addons/guide/ui/text_providers/controllers/switch/switch_controller_text_provider.gd
@@ -0,0 +1,37 @@
+extends "res://addons/guide/ui/text_providers/controllers/controller_text_provider.gd"
+
+func _controller_names() -> Array[String]:
+ return ["Nintendo Switch"]
+
+func _a_button_name() -> String:
+ return "B"
+
+func _b_button_name() -> String:
+ return "A"
+
+func _x_button_name() -> String:
+ return "Y"
+
+func _y_button_name() -> String:
+ return "X"
+
+func _left_bumper_name() -> String:
+ return "L"
+
+func _right_bumper_name() -> String:
+ return "R"
+
+func _left_trigger_name() -> String:
+ return "ZL"
+
+func _right_trigger_name() -> String:
+ return "ZR"
+
+func _back_button_name() -> String:
+ return "-"
+
+func _misc_1_button_name() -> String:
+ return "Square"
+
+func _start_button_name() -> String:
+ return "+"
diff --git a/addons/guide/ui/text_providers/controllers/xbox/xbox_controller_text_provider.gd b/addons/guide/ui/text_providers/controllers/xbox/xbox_controller_text_provider.gd
new file mode 100644
index 0000000..1d044d7
--- /dev/null
+++ b/addons/guide/ui/text_providers/controllers/xbox/xbox_controller_text_provider.gd
@@ -0,0 +1,37 @@
+extends "res://addons/guide/ui/text_providers/controllers/controller_text_provider.gd"
+
+func _controller_names() -> Array[String]:
+ return ["XInput", "XBox"]
+
+func _a_button_name() -> String:
+ return "A"
+
+func _b_button_name() -> String:
+ return "B"
+
+func _x_button_name() -> String:
+ return "X"
+
+func _y_button_name() -> String:
+ return "Y"
+
+func _left_bumper_name() -> String:
+ return "LB"
+
+func _right_bumper_name() -> String:
+ return "RB"
+
+func _left_trigger_name() -> String:
+ return "LT"
+
+func _right_trigger_name() -> String:
+ return "RT"
+
+func _back_button_name() -> String:
+ return "View"
+
+func _misc_1_button_name() -> String:
+ return "Share"
+
+func _start_button_name() -> String:
+ return "Menu"
diff --git a/addons/guide/ui/text_providers/default_text_provider.gd b/addons/guide/ui/text_providers/default_text_provider.gd
new file mode 100644
index 0000000..c9ce6af
--- /dev/null
+++ b/addons/guide/ui/text_providers/default_text_provider.gd
@@ -0,0 +1,141 @@
+extends GUIDETextProvider
+
+var _is_on_desktop:bool = false
+
+func _init():
+ priority = 0
+ _is_on_desktop = OS.has_feature("linuxbsd") or OS.has_feature("macos") or OS.has_feature("windows")
+
+func supports(input:GUIDEInput) -> bool:
+ return true
+
+
+func _format(input:String) -> String:
+ return "[%s]" % [input]
+
+
+func get_text(input:GUIDEInput) -> String:
+ if input is GUIDEInputKey:
+ var result:PackedStringArray = []
+ if input.control:
+ var ctrl = GUIDEInputKey.new()
+ ctrl.key = KEY_CTRL
+ result.append(get_text(ctrl))
+ if input.alt:
+ var alt = GUIDEInputKey.new()
+ alt.key = KEY_ALT
+ result.append(get_text(alt))
+ if input.shift:
+ var shift = GUIDEInputKey.new()
+ shift.key = KEY_SHIFT
+ result.append(get_text(shift))
+ if input.meta:
+ var meta = GUIDEInputKey.new()
+ meta.key = KEY_META
+ result.append(get_text(meta))
+
+ var the_key = input.key
+
+ # if we are on desktop, translate the physical keycode into the actual label
+ # this is not supported on mobile, so we have to check
+ if _is_on_desktop:
+ the_key = DisplayServer.keyboard_get_label_from_physical(input.key)
+
+
+ result.append(_format(OS.get_keycode_string(the_key)))
+ return "+".join(result)
+
+ if input is GUIDEInputMouseAxis1D:
+ match input.axis:
+ GUIDEInputMouseAxis1D.GUIDEInputMouseAxis.X:
+ return _format(tr("Mouse Left/Right"))
+ GUIDEInputMouseAxis1D.GUIDEInputMouseAxis.Y:
+ return _format(tr("Mouse Up/Down"))
+
+ if input is GUIDEInputMouseAxis2D:
+ return _format(tr("Mouse"))
+
+ if input is GUIDEInputMouseButton:
+ match input.button:
+ MOUSE_BUTTON_LEFT:
+ return _format(tr("Left Mouse Button"))
+ MOUSE_BUTTON_RIGHT:
+ return _format(tr("Right Mouse Button"))
+ MOUSE_BUTTON_MIDDLE:
+ return _format(tr("Middle Mouse Button"))
+ MOUSE_BUTTON_WHEEL_UP:
+ return _format(tr("Mouse Wheel Up"))
+ MOUSE_BUTTON_WHEEL_DOWN:
+ return _format(tr("Mouse Wheel Down"))
+ MOUSE_BUTTON_WHEEL_LEFT:
+ return _format(tr("Mouse Wheel Left"))
+ MOUSE_BUTTON_WHEEL_RIGHT:
+ return _format(tr("Mouse Wheel Right"))
+ MOUSE_BUTTON_XBUTTON1:
+ return _format(tr("Mouse Side 1"))
+ MOUSE_BUTTON_XBUTTON2:
+ return _format(tr("Mouse Side 2"))
+
+ if input is GUIDEInputJoyAxis1D:
+ match input.axis:
+ JOY_AXIS_LEFT_X:
+ return _format(tr("Stick 1 Horizontal"))
+ JOY_AXIS_LEFT_Y:
+ return _format(tr("Stick 1 Vertical"))
+ JOY_AXIS_RIGHT_X:
+ return _format(tr("Stick 2 Horizontal"))
+ JOY_AXIS_RIGHT_Y:
+ return _format(tr("Stick 2 Vertical"))
+ JOY_AXIS_TRIGGER_LEFT:
+ return _format(tr("Axis 3"))
+ JOY_AXIS_TRIGGER_RIGHT:
+ return _format(tr("Axis 4"))
+
+ if input is GUIDEInputJoyAxis2D:
+ match input.x:
+ JOY_AXIS_LEFT_X, JOY_AXIS_LEFT_Y:
+ return _format(tr("Stick 1"))
+ JOY_AXIS_RIGHT_X, JOY_AXIS_RIGHT_Y:
+ return _format(tr("Stick 2"))
+
+ if input is GUIDEInputJoyButton:
+ return _format(tr("Joy %s") % [input.button])
+
+
+ if input is GUIDEInputAction:
+ return _format(tr("Action %s") % ["?" if input.action == null else input.action._editor_name()])
+
+ if input is GUIDEInputAny:
+ var parts:Array[String] = []
+ if input.joy:
+ parts.append(tr("Joy Button"))
+ if input.mouse:
+ parts.append(tr("Mouse Button"))
+ if input.keyboard:
+ parts.append(tr("Key"))
+
+ return _format(tr("Any %s") % [ "/".join(parts) ] )
+
+ if input is GUIDEInputMousePosition:
+ return _format(tr("Mouse Position"))
+
+ if input is GUIDEInputTouchPosition:
+ return _format(tr("Touch Position %s") % [input.finger_index if input.finger_index >= 0 else "Average"])
+
+ if input is GUIDEInputTouchAngle:
+ return _format(tr("Touch Angle"))
+
+ if input is GUIDEInputTouchDistance:
+ return _format(tr("Touch Distance"))
+
+ if input is GUIDEInputTouchAxis1D:
+ match input.axis:
+ GUIDEInputTouchAxis1D.GUIDEInputTouchAxis.X:
+ _format(tr("Touch Left/Right %s") % [input.finger_index if input.finger_index >= 0 else "Average"])
+ GUIDEInputTouchAxis1D.GUIDEInputTouchAxis.Y:
+ _format(tr("Touch Up/Down %s") % [input.finger_index if input.finger_index >= 0 else "Average"])
+
+ if input is GUIDEInputTouchAxis2D:
+ return _format(tr("Touch Axis 2D %s") % [input.finger_index if input.finger_index >= 0 else "Average"])
+
+ return _format("??")
diff --git a/examples/edge_shader/edge_shader.gdshader b/examples/edge_shader/edge_shader.gdshader
deleted file mode 100644
index a2def85..0000000
--- a/examples/edge_shader/edge_shader.gdshader
+++ /dev/null
@@ -1,16 +0,0 @@
-shader_type canvas_item;
-/**
- * This shader fixes normal textures looking wrong when used with SmartShape2D.
- * See: https://github.com/SirRamEsq/SmartShape2D/blob/master/addons/rmsmartshape/documentation/Normals.md
- */
-
-varying mat2 NORMAL_MATRIX;
-
-void vertex() {
- NORMAL_MATRIX = mat2(COLOR.rg, COLOR.ba) * 2.0 - mat2(vec2(1.0), vec2(1.0));
-}
-
-void fragment() {
- NORMAL.xy = NORMAL_MATRIX * NORMAL.xy;
- COLOR = texture(TEXTURE, UV);
-}
diff --git a/examples/edge_shader/material_edge_shader.tres b/examples/edge_shader/material_edge_shader.tres
deleted file mode 100644
index 0bda5c4..0000000
--- a/examples/edge_shader/material_edge_shader.tres
+++ /dev/null
@@ -1,6 +0,0 @@
-[gd_resource type="ShaderMaterial" load_steps=2 format=3 uid="uid://dpec5hjpflmri"]
-
-[ext_resource type="Shader" path="res://examples/edge_shader/edge_shader.gdshader" id="1_akl0v"]
-
-[resource]
-shader = ExtResource("1_akl0v")
diff --git a/examples/sharp_corner_tapering/assets/mat_edge_metal.tres b/examples/sharp_corner_tapering/assets/mat_edge_metal.tres
deleted file mode 100644
index aacc59b..0000000
--- a/examples/sharp_corner_tapering/assets/mat_edge_metal.tres
+++ /dev/null
@@ -1,22 +0,0 @@
-[gd_resource type="Resource" script_class="SS2D_Material_Edge" load_steps=7 format=3 uid="uid://c8q2nrxnbopbb"]
-
-[ext_resource type="Script" path="res://addons/rmsmartshape/materials/edge_material.gd" id="1_lbfns"]
-[ext_resource type="Texture2D" uid="uid://b3aqa3bj1osvp" path="res://examples/sharp_corner_tapering/assets/tex_metal_edge.png" id="2_bmjh3"]
-[ext_resource type="Texture2D" uid="uid://bobwi3r6aiiqg" path="res://examples/sharp_corner_tapering/assets/tex_metal_corner_inner.png" id="3_n1i1s"]
-[ext_resource type="Texture2D" uid="uid://bc34cuc50as8f" path="res://examples/sharp_corner_tapering/assets/tex_metal_corner_outer.png" id="4_ml2u7"]
-[ext_resource type="Texture2D" uid="uid://jc3g5qsmnpdd" path="res://examples/sharp_corner_tapering/assets/tex_metal_taper_corner_left.png" id="5_244mn"]
-[ext_resource type="Texture2D" uid="uid://cdnfaf3bslk38" path="res://examples/sharp_corner_tapering/assets/tex_metal_taper_corner_right.png" id="6_1c2na"]
-
-[resource]
-script = ExtResource("1_lbfns")
-textures = Array[Texture2D]([ExtResource("2_bmjh3")])
-textures_corner_outer = Array[Texture2D]([ExtResource("4_ml2u7")])
-textures_corner_inner = Array[Texture2D]([ExtResource("3_n1i1s")])
-textures_taper_left = Array[Texture2D]([])
-textures_taper_right = Array[Texture2D]([])
-textures_taper_corner_left = Array[Texture2D]([ExtResource("5_244mn")])
-textures_taper_corner_right = Array[Texture2D]([ExtResource("6_1c2na")])
-randomize_texture = false
-use_corner_texture = true
-use_taper_texture = false
-fit_mode = 0
diff --git a/examples/sharp_corner_tapering/assets/tex_metal_corner_inner.png b/examples/sharp_corner_tapering/assets/tex_metal_corner_inner.png
deleted file mode 100644
index 6adcf60..0000000
Binary files a/examples/sharp_corner_tapering/assets/tex_metal_corner_inner.png and /dev/null differ
diff --git a/examples/sharp_corner_tapering/assets/tex_metal_corner_outer.png b/examples/sharp_corner_tapering/assets/tex_metal_corner_outer.png
deleted file mode 100644
index 1dcc88a..0000000
Binary files a/examples/sharp_corner_tapering/assets/tex_metal_corner_outer.png and /dev/null differ
diff --git a/examples/sharp_corner_tapering/assets/tex_metal_edge.png b/examples/sharp_corner_tapering/assets/tex_metal_edge.png
deleted file mode 100644
index cab72a4..0000000
Binary files a/examples/sharp_corner_tapering/assets/tex_metal_edge.png and /dev/null differ
diff --git a/examples/sharp_corner_tapering/assets/tex_metal_fill.png b/examples/sharp_corner_tapering/assets/tex_metal_fill.png
deleted file mode 100644
index d8a5619..0000000
Binary files a/examples/sharp_corner_tapering/assets/tex_metal_fill.png and /dev/null differ
diff --git a/examples/sharp_corner_tapering/assets/tex_metal_taper_corner_left.png b/examples/sharp_corner_tapering/assets/tex_metal_taper_corner_left.png
deleted file mode 100644
index 17ab69d..0000000
Binary files a/examples/sharp_corner_tapering/assets/tex_metal_taper_corner_left.png and /dev/null differ
diff --git a/examples/sharp_corner_tapering/assets/tex_metal_taper_corner_right.png b/examples/sharp_corner_tapering/assets/tex_metal_taper_corner_right.png
deleted file mode 100644
index 3d06df9..0000000
Binary files a/examples/sharp_corner_tapering/assets/tex_metal_taper_corner_right.png and /dev/null differ
diff --git a/examples/sharp_corner_tapering/sharp_corner_tapering.tscn b/examples/sharp_corner_tapering/sharp_corner_tapering.tscn
deleted file mode 100644
index ecbfeb0..0000000
--- a/examples/sharp_corner_tapering/sharp_corner_tapering.tscn
+++ /dev/null
@@ -1,315 +0,0 @@
-[gd_scene load_steps=46 format=3 uid="uid://cl71flny1scun"]
-
-[ext_resource type="Script" path="res://addons/rmsmartshape/shapes/shape.gd" id="1_5hlge"]
-[ext_resource type="Script" path="res://addons/rmsmartshape/vertex_properties.gd" id="2_yp1d2"]
-[ext_resource type="Script" path="res://addons/rmsmartshape/shapes/point.gd" id="3_edpr6"]
-[ext_resource type="Script" path="res://addons/rmsmartshape/shapes/point_array.gd" id="4_udfgv"]
-[ext_resource type="Texture2D" uid="uid://bj658oli0klj3" path="res://examples/sharp_corner_tapering/assets/tex_metal_fill.png" id="6_ry0x0"]
-[ext_resource type="Script" path="res://addons/rmsmartshape/materials/shape_material.gd" id="7_ncdby"]
-[ext_resource type="Resource" uid="uid://c8q2nrxnbopbb" path="res://examples/sharp_corner_tapering/assets/mat_edge_metal.tres" id="8_uwntg"]
-[ext_resource type="Script" path="res://addons/rmsmartshape/normal_range.gd" id="9_tqu3h"]
-[ext_resource type="Script" path="res://addons/rmsmartshape/materials/edge_material.gd" id="10_55l7o"]
-[ext_resource type="Script" path="res://addons/rmsmartshape/materials/edge_material_metadata.gd" id="10_umoay"]
-[ext_resource type="Texture2D" uid="uid://b3aqa3bj1osvp" path="res://examples/sharp_corner_tapering/assets/tex_metal_edge.png" id="11_jc4po"]
-[ext_resource type="Texture2D" uid="uid://bobwi3r6aiiqg" path="res://examples/sharp_corner_tapering/assets/tex_metal_corner_inner.png" id="12_va43u"]
-[ext_resource type="Texture2D" uid="uid://bc34cuc50as8f" path="res://examples/sharp_corner_tapering/assets/tex_metal_corner_outer.png" id="13_f7yxr"]
-
-[sub_resource type="Resource" id="Resource_2r7x3"]
-script = ExtResource("2_yp1d2")
-texture_idx = 0
-flip = false
-width = 1.0
-
-[sub_resource type="Resource" id="Resource_vpbfx"]
-script = ExtResource("3_edpr6")
-position = Vector2(83.365, 45.3257)
-point_in = Vector2(0, 0)
-point_out = Vector2(0, 0)
-properties = SubResource("Resource_2r7x3")
-
-[sub_resource type="Resource" id="Resource_a0g7n"]
-script = ExtResource("2_yp1d2")
-texture_idx = 0
-flip = false
-width = 1.0
-
-[sub_resource type="Resource" id="Resource_w5cbt"]
-script = ExtResource("3_edpr6")
-position = Vector2(263.601, 615.758)
-point_in = Vector2(0, 0)
-point_out = Vector2(0, 0)
-properties = SubResource("Resource_a0g7n")
-
-[sub_resource type="Resource" id="Resource_h14mo"]
-script = ExtResource("2_yp1d2")
-texture_idx = -4
-flip = false
-width = 1.0
-
-[sub_resource type="Resource" id="Resource_j5l1j"]
-script = ExtResource("3_edpr6")
-position = Vector2(1026.77, 577.75)
-point_in = Vector2(0, 0)
-point_out = Vector2(0, 0)
-properties = SubResource("Resource_h14mo")
-
-[sub_resource type="Resource" id="Resource_ukuig"]
-script = ExtResource("2_yp1d2")
-texture_idx = 0
-flip = false
-width = 1.0
-
-[sub_resource type="Resource" id="Resource_324cb"]
-script = ExtResource("3_edpr6")
-position = Vector2(731.167, 13.1754)
-point_in = Vector2(0, 0)
-point_out = Vector2(0, 0)
-properties = SubResource("Resource_ukuig")
-
-[sub_resource type="Resource" id="Resource_wasbm"]
-script = ExtResource("2_yp1d2")
-texture_idx = 0
-flip = false
-width = 1.0
-
-[sub_resource type="Resource" id="Resource_p3ja8"]
-script = ExtResource("3_edpr6")
-position = Vector2(83.365, 45.3257)
-point_in = Vector2(0, 0)
-point_out = Vector2(0, 0)
-properties = SubResource("Resource_wasbm")
-
-[sub_resource type="Resource" id="Resource_1fh62"]
-script = ExtResource("2_yp1d2")
-texture_idx = 0
-flip = false
-width = 1.0
-
-[sub_resource type="Resource" id="Resource_qjb65"]
-script = ExtResource("3_edpr6")
-position = Vector2(742.381, 447.381)
-point_in = Vector2(0, 0)
-point_out = Vector2(0, 0)
-properties = SubResource("Resource_1fh62")
-
-[sub_resource type="Resource" id="Resource_o8bfq"]
-script = ExtResource("2_yp1d2")
-texture_idx = 0
-flip = false
-width = 1.0
-
-[sub_resource type="Resource" id="Resource_w1ohc"]
-script = ExtResource("3_edpr6")
-position = Vector2(641.411, 322.256)
-point_in = Vector2(0, 0)
-point_out = Vector2(0, 0)
-properties = SubResource("Resource_o8bfq")
-
-[sub_resource type="Resource" id="Resource_tp3up"]
-script = ExtResource("2_yp1d2")
-texture_idx = 0
-flip = false
-width = 1.0
-
-[sub_resource type="Resource" id="Resource_tlf23"]
-script = ExtResource("3_edpr6")
-position = Vector2(1133.09, 291.15)
-point_in = Vector2(0, 0)
-point_out = Vector2(0, 0)
-properties = SubResource("Resource_tp3up")
-
-[sub_resource type="Resource" id="Resource_6u83i"]
-script = ExtResource("2_yp1d2")
-texture_idx = 0
-flip = false
-width = 1.0
-
-[sub_resource type="Resource" id="Resource_86kx1"]
-script = ExtResource("3_edpr6")
-position = Vector2(546.956, 94.6283)
-point_in = Vector2(0, 0)
-point_out = Vector2(0, 0)
-properties = SubResource("Resource_6u83i")
-
-[sub_resource type="Resource" id="Resource_kysxg"]
-script = ExtResource("2_yp1d2")
-texture_idx = 0
-flip = false
-width = 1.0
-
-[sub_resource type="Resource" id="Resource_eqyck"]
-script = ExtResource("3_edpr6")
-position = Vector2(293.278, 51.6277)
-point_in = Vector2(0, 0)
-point_out = Vector2(0, 0)
-properties = SubResource("Resource_kysxg")
-
-[sub_resource type="Resource" id="Resource_egpc8"]
-script = ExtResource("2_yp1d2")
-texture_idx = 0
-flip = false
-width = 1.0
-
-[sub_resource type="Resource" id="Resource_1wyqg"]
-script = ExtResource("3_edpr6")
-position = Vector2(871.022, 351.231)
-point_in = Vector2(0, 0)
-point_out = Vector2(0, 0)
-properties = SubResource("Resource_egpc8")
-
-[sub_resource type="Resource" id="Resource_baip5"]
-script = ExtResource("2_yp1d2")
-texture_idx = 0
-flip = false
-width = 1.0
-
-[sub_resource type="Resource" id="Resource_2owaw"]
-script = ExtResource("3_edpr6")
-position = Vector2(239.115, 334.66)
-point_in = Vector2(0, 0)
-point_out = Vector2(0, 0)
-properties = SubResource("Resource_baip5")
-
-[sub_resource type="Resource" id="Resource_o2w6y"]
-script = ExtResource("4_udfgv")
-_points = {
-0: SubResource("Resource_vpbfx"),
-1: SubResource("Resource_w5cbt"),
-2: SubResource("Resource_j5l1j"),
-3: SubResource("Resource_324cb"),
-4: SubResource("Resource_p3ja8"),
-6: SubResource("Resource_qjb65"),
-8: SubResource("Resource_w1ohc"),
-9: SubResource("Resource_tlf23"),
-10: SubResource("Resource_86kx1"),
-11: SubResource("Resource_eqyck"),
-12: SubResource("Resource_1wyqg"),
-13: SubResource("Resource_2owaw")
-}
-_point_order = PackedInt32Array(4, 11, 8, 10, 3, 12, 9, 2, 6, 1, 13, 0)
-_constraints = {
-Vector2i(0, 4): 15
-}
-_next_key = 14
-_material_overrides = {}
-tessellation_stages = 5
-tessellation_tolerance = 4.0
-curve_bake_interval = 20.0
-
-[sub_resource type="Resource" id="Resource_42qh7"]
-script = ExtResource("9_tqu3h")
-begin = 0.0
-distance = 360.0
-edgeRendering = Vector2(0, 0)
-
-[sub_resource type="Resource" id="Resource_t7ed3"]
-script = ExtResource("10_umoay")
-edge_material = ExtResource("8_uwntg")
-normal_range = SubResource("Resource_42qh7")
-weld = true
-taper_sharp_corners = true
-render = true
-z_index = 0
-z_as_relative = true
-offset = 0.0
-
-[sub_resource type="Resource" id="Resource_07ymf"]
-script = ExtResource("7_ncdby")
-_edge_meta_materials = Array[ExtResource("10_umoay")]([SubResource("Resource_t7ed3")])
-fill_textures = Array[Texture2D]([ExtResource("6_ry0x0")])
-fill_texture_z_index = -10
-fill_texture_show_behind_parent = false
-fill_texture_scale = 1.0
-fill_texture_absolute_position = false
-fill_texture_absolute_rotation = false
-fill_texture_offset = Vector2(0, 0)
-fill_texture_angle_offset = 0.0
-fill_mesh_offset = 0.0
-render_offset = 0.0
-
-[sub_resource type="Resource" id="Resource_ma2v2"]
-script = ExtResource("10_55l7o")
-textures = Array[Texture2D]([ExtResource("11_jc4po")])
-textures_corner_outer = Array[Texture2D]([ExtResource("13_f7yxr")])
-textures_corner_inner = Array[Texture2D]([ExtResource("12_va43u")])
-textures_taper_left = Array[Texture2D]([])
-textures_taper_right = Array[Texture2D]([])
-textures_taper_corner_left = Array[Texture2D]([])
-textures_taper_corner_right = Array[Texture2D]([])
-randomize_texture = false
-use_corner_texture = true
-use_taper_texture = true
-fit_mode = 0
-
-[sub_resource type="Resource" id="Resource_atqmo"]
-script = ExtResource("9_tqu3h")
-begin = 0.0
-distance = 360.0
-edgeRendering = Vector2(0, 0)
-
-[sub_resource type="Resource" id="Resource_vboqn"]
-script = ExtResource("10_umoay")
-edge_material = SubResource("Resource_ma2v2")
-normal_range = SubResource("Resource_atqmo")
-weld = true
-taper_sharp_corners = false
-render = true
-z_index = 0
-z_as_relative = true
-offset = 0.0
-
-[sub_resource type="Resource" id="Resource_xsfev"]
-script = ExtResource("7_ncdby")
-_edge_meta_materials = Array[ExtResource("10_umoay")]([SubResource("Resource_vboqn")])
-fill_textures = Array[Texture2D]([ExtResource("6_ry0x0")])
-fill_texture_z_index = -10
-fill_texture_show_behind_parent = false
-fill_texture_scale = 1.0
-fill_texture_absolute_position = false
-fill_texture_absolute_rotation = false
-fill_texture_offset = Vector2(0, 0)
-fill_texture_angle_offset = 0.0
-fill_mesh_offset = 0.0
-render_offset = 0.0
-
-[node name="Node2D" type="Node2D"]
-
-[node name="WithTapering" type="Node2D" parent="."]
-texture_repeat = 2
-position = Vector2(-30, 228)
-scale = Vector2(0.65, 0.65)
-script = ExtResource("1_5hlge")
-_points = SubResource("Resource_o2w6y")
-shape_material = SubResource("Resource_07ymf")
-
-[node name="WithoutTapering" type="Node2D" parent="."]
-texture_repeat = 2
-position = Vector2(400, 10)
-scale = Vector2(0.65, 0.65)
-script = ExtResource("1_5hlge")
-_points = SubResource("Resource_o2w6y")
-shape_material = SubResource("Resource_xsfev")
-
-[node name="Message" type="Label" parent="."]
-offset_left = 33.0
-offset_top = 82.0
-offset_right = 434.0
-offset_bottom = 190.0
-theme_override_font_sizes/font_size = 24
-text = "NOTE: Sharp corner tapering can
-cause issues when used on shapes
-with curved geometry"
-
-[node name="With" type="Label" parent="."]
-offset_left = 275.0
-offset_top = 454.0
-offset_right = 503.0
-offset_bottom = 477.0
-text = "Sharp Corner Tapering"
-
-[node name="Without" type="Label" parent="."]
-offset_left = 666.0
-offset_top = 237.0
-offset_right = 907.0
-offset_bottom = 260.0
-text = "No Sharp Corner Tapering"
diff --git a/fonts/PressStart2P-Regular.ttf b/fonts/PressStart2P-Regular.ttf
new file mode 100644
index 0000000..2442aff
Binary files /dev/null and b/fonts/PressStart2P-Regular.ttf differ
diff --git a/fonts/PressStart2P-Regular.ttf.import b/fonts/PressStart2P-Regular.ttf.import
new file mode 100644
index 0000000..b1ca4c1
--- /dev/null
+++ b/fonts/PressStart2P-Regular.ttf.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="font_data_dynamic"
+type="FontFile"
+uid="uid://xm0vbusjr7b7"
+path="res://.godot/imported/PressStart2P-Regular.ttf-c7d83f2c4bd295d4c960a93a703ae2b2.fontdata"
+
+[deps]
+
+source_file="res://fonts/PressStart2P-Regular.ttf"
+dest_files=["res://.godot/imported/PressStart2P-Regular.ttf-c7d83f2c4bd295d4c960a93a703ae2b2.fontdata"]
+
+[params]
+
+Rendering=null
+antialiasing=1
+generate_mipmaps=false
+disable_embedded_bitmaps=true
+multichannel_signed_distance_field=false
+msdf_pixel_range=8
+msdf_size=48
+allow_system_fallback=true
+force_autohinter=false
+hinting=1
+subpixel_positioning=1
+oversampling=0.0
+Fallbacks=null
+fallbacks=[]
+Compress=null
+compress=true
+preload=[]
+language_support={}
+script_support={}
+opentype_features={}
diff --git a/objects/cage.tscn b/objects/cage.tscn
index 93845f0..640b766 100644
--- a/objects/cage.tscn
+++ b/objects/cage.tscn
@@ -15,6 +15,7 @@ physics_layer_0/collision_layer = 1
sources/0 = SubResource("TileSetAtlasSource_aivtb")
[node name="Cage" type="Node2D"]
+z_index = 2
scale = Vector2(2, 2)
[node name="TileMapLayer" type="TileMapLayer" parent="."]
diff --git a/objects/child.tscn b/objects/child.tscn
index 633671d..3c16ec7 100644
--- a/objects/child.tscn
+++ b/objects/child.tscn
@@ -1,9 +1,10 @@
-[gd_scene load_steps=9 format=3 uid="uid://d0s2abysa86rq"]
+[gd_scene load_steps=10 format=3 uid="uid://d0s2abysa86rq"]
[ext_resource type="Texture2D" uid="uid://b7gp0gqvkv8j4" path="res://sprites/MrBrick_base.png" id="1_jcisc"]
[ext_resource type="Texture2D" uid="uid://jl1gwqchhpdc" path="res://sprites/left_eye.png" id="2_8jrbi"]
[ext_resource type="Texture2D" uid="uid://iiawtnwmeny3" path="res://sprites/right_eye.png" id="3_k41y7"]
[ext_resource type="Texture2D" uid="uid://8h05rd26t66q" path="res://sprites/lollipop.png" id="4_vq1oq"]
+[ext_resource type="Texture2D" uid="uid://dpbpjffbdbovp" path="res://sprites/cap.png" id="5_m7x6t"]
[ext_resource type="Script" path="res://scripts/components/collectable.gd" id="5_wc3ym"]
[ext_resource type="Resource" uid="uid://b6apusc0jmi3x" path="res://resources/collectables/child.tres" id="6_vmvuo"]
[ext_resource type="AudioStream" uid="uid://drfr0wlgkhkdq" path="res://sfx/child_pickup.wav" id="7_j8eyh"]
@@ -40,6 +41,10 @@ position = Vector2(3, 1)
rotation = 0.785398
texture = ExtResource("4_vq1oq")
+[node name="Cap" type="Sprite2D" parent="Root"]
+position = Vector2(4, -18)
+texture = ExtResource("5_m7x6t")
+
[node name="CollectableComponent" type="Node" parent="." node_paths=PackedStringArray("area2d", "sfx")]
script = ExtResource("5_wc3ym")
area2d = NodePath("..")
diff --git a/project.godot b/project.godot
index 8895a83..264060b 100644
--- a/project.godot
+++ b/project.godot
@@ -28,6 +28,7 @@ config/icon="res://icon.svg"
GameManager="*res://objects/game_manager.tscn"
PhantomCameraManager="*res://addons/phantom_camera/scripts/managers/phantom_camera_manager.gd"
+GUIDE="*res://addons/guide/guide.gd"
[display]
@@ -46,7 +47,7 @@ project/assembly_name="Mr. Brick Adventures"
[editor_plugins]
-enabled=PackedStringArray("res://addons/phantom_camera/plugin.cfg", "res://addons/rmsmartshape/plugin.cfg")
+enabled=PackedStringArray("res://addons/guide/plugin.cfg", "res://addons/phantom_camera/plugin.cfg")
[global_group]
diff --git a/sprites/cap.png b/sprites/cap.png
new file mode 100644
index 0000000..0821231
Binary files /dev/null and b/sprites/cap.png differ
diff --git a/addons/godot-rapier2d/logo_square_2d.png.import b/sprites/cap.png.import
similarity index 66%
rename from addons/godot-rapier2d/logo_square_2d.png.import
rename to sprites/cap.png.import
index 35dc183..3dd19b0 100644
--- a/addons/godot-rapier2d/logo_square_2d.png.import
+++ b/sprites/cap.png.import
@@ -2,16 +2,16 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://d1iwpd5epp8u6"
-path="res://.godot/imported/logo_square_2d.png-ee7cb55fd9c4d1815ce7e83cd5401198.ctex"
+uid="uid://dpbpjffbdbovp"
+path="res://.godot/imported/cap.png-129e6e9f426ec3391fbd49f80f263310.ctex"
metadata={
"vram_texture": false
}
[deps]
-source_file="res://addons/godot-rapier2d/logo_square_2d.png"
-dest_files=["res://.godot/imported/logo_square_2d.png-ee7cb55fd9c4d1815ce7e83cd5401198.ctex"]
+source_file="res://sprites/cap.png"
+dest_files=["res://.godot/imported/cap.png-129e6e9f426ec3391fbd49f80f263310.ctex"]
[params]
diff --git a/sprites/ui/gdb-keyboard-2.png b/sprites/ui/gdb-keyboard-2.png
new file mode 100644
index 0000000..da9dd18
Binary files /dev/null and b/sprites/ui/gdb-keyboard-2.png differ
diff --git a/sprites/ui/gdb-keyboard-2.png.import b/sprites/ui/gdb-keyboard-2.png.import
new file mode 100644
index 0000000..04f0128
--- /dev/null
+++ b/sprites/ui/gdb-keyboard-2.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bolnn3e0u8d5a"
+path="res://.godot/imported/gdb-keyboard-2.png-e4700488c574704d1c341bd05b453580.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://sprites/ui/gdb-keyboard-2.png"
+dest_files=["res://.godot/imported/gdb-keyboard-2.png-e4700488c574704d1c341bd05b453580.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/sprites/ui/gdb-playstation-3.png b/sprites/ui/gdb-playstation-3.png
new file mode 100644
index 0000000..1750b6a
Binary files /dev/null and b/sprites/ui/gdb-playstation-3.png differ
diff --git a/sprites/ui/gdb-playstation-3.png.import b/sprites/ui/gdb-playstation-3.png.import
new file mode 100644
index 0000000..9355a8d
--- /dev/null
+++ b/sprites/ui/gdb-playstation-3.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://8gwdsf8gptjf"
+path="res://.godot/imported/gdb-playstation-3.png-1052be9ad0b0e4e3aceaf539c8b22a36.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://sprites/ui/gdb-playstation-3.png"
+dest_files=["res://.godot/imported/gdb-playstation-3.png-1052be9ad0b0e4e3aceaf539c8b22a36.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/sprites/ui/gdb-xbox-2.png b/sprites/ui/gdb-xbox-2.png
new file mode 100644
index 0000000..49dcd12
Binary files /dev/null and b/sprites/ui/gdb-xbox-2.png differ
diff --git a/sprites/ui/gdb-xbox-2.png.import b/sprites/ui/gdb-xbox-2.png.import
new file mode 100644
index 0000000..a587c2d
--- /dev/null
+++ b/sprites/ui/gdb-xbox-2.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cqq1u0ignemi5"
+path="res://.godot/imported/gdb-xbox-2.png-894d57920d16ff5da033d2acef32774b.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://sprites/ui/gdb-xbox-2.png"
+dest_files=["res://.godot/imported/gdb-xbox-2.png-894d57920d16ff5da033d2acef32774b.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