Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ce39b237c3 | |||
| a69c60ce85 | |||
| 969741aa31 | |||
| e70ce76207 | |||
| 926446e5cf | |||
| e7299b17e9 | |||
| e2dc429caa | |||
| 25221bf3cb |
@@ -0,0 +1,18 @@
|
||||
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.
|
||||
@@ -0,0 +1,91 @@
|
||||
Godot Jolt 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/godot-jolt/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.
|
||||
|
||||
Jolt Physics (https://github.com/godot-jolt/jolt)
|
||||
|
||||
Copyright (c) 2021 Jorrit Rouwe.
|
||||
|
||||
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.
|
||||
|
||||
mimalloc (https://github.com/godot-jolt/mimalloc)
|
||||
|
||||
Copyright (c) 2018-2021 Microsoft Corporation, Daan Leijen.
|
||||
|
||||
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.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,41 @@
|
||||
[godot-jolt]
|
||||
version = "0.16.0-stable"
|
||||
build = "8eca5bb795"
|
||||
|
||||
[configuration]
|
||||
|
||||
entry_symbol = "godot_jolt_main"
|
||||
compatibility_minimum = "4.3"
|
||||
compatibility_maximum = "4.7"
|
||||
|
||||
[libraries]
|
||||
|
||||
windows.release.single.x86_64 = "windows/godot-jolt_windows-x64.dll"
|
||||
windows.debug.single.x86_64 = "windows/godot-jolt_windows-x64_editor.dll"
|
||||
|
||||
windows.release.single.x86_32 = "windows/godot-jolt_windows-x86.dll"
|
||||
windows.debug.single.x86_32 = "windows/godot-jolt_windows-x86_editor.dll"
|
||||
|
||||
linux.release.single.x86_64 = "linux/godot-jolt_linux-x64.so"
|
||||
linux.debug.single.x86_64 = "linux/godot-jolt_linux-x64_editor.so"
|
||||
|
||||
linux.release.single.x86_32 = "linux/godot-jolt_linux-x86.so"
|
||||
linux.debug.single.x86_32 = "linux/godot-jolt_linux-x86_editor.so"
|
||||
|
||||
macos.release.single = "macos/godot-jolt_macos.framework"
|
||||
macos.debug.single = "macos/godot-jolt_macos_editor.framework"
|
||||
|
||||
ios.release.single = "ios/godot-jolt_ios.framework"
|
||||
ios.debug.single = "ios/godot-jolt_ios_editor.framework"
|
||||
|
||||
android.release.single.arm64 = "android/libgodot-jolt_android-arm64.so"
|
||||
android.debug.single.arm64 = "android/libgodot-jolt_android-arm64_editor.so"
|
||||
|
||||
android.release.single.arm32 = "android/libgodot-jolt_android-arm32.so"
|
||||
android.debug.single.arm32 = "android/libgodot-jolt_android-arm32_editor.so"
|
||||
|
||||
android.release.single.x86_64 = "android/libgodot-jolt_android-x64.so"
|
||||
android.debug.single.x86_64 = "android/libgodot-jolt_android-x64_editor.so"
|
||||
|
||||
android.release.single.x86_32 = "android/libgodot-jolt_android-x86.so"
|
||||
android.debug.single.x86_32 = "android/libgodot-jolt_android-x86_editor.so"
|
||||
@@ -0,0 +1 @@
|
||||
uid://bc7tkad04qybu
|
||||
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>godot-jolt_ios</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Godot Jolt</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Godot Jolt</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>org.godot-jolt.godot-jolt</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright (c) Mikael Hermansson and Godot Jolt contributors.</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>0.16.0</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.16.0</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>iPhoneOS</string>
|
||||
</array>
|
||||
<key>CSResourcesFileMapped</key>
|
||||
<true/>
|
||||
<key>DTPlatformName</key>
|
||||
<string>iphoneos</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>12.0</string>
|
||||
|
||||
<!--
|
||||
HACK(mihe): This is to work around a bug in Godot 4.3-beta1, where it treats Framework
|
||||
bundles the same as XCFramework bundles, and expects there to be an `AvailableLibraries`
|
||||
entry, which is really only a thing in XCFramework bundles. Note that we also lie about the
|
||||
binary path having a `.dylib` extension in order for Godot to correctly identify this as a
|
||||
dynamically linked bundle.
|
||||
-->
|
||||
<key>AvailableLibraries</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>BinaryPath</key>
|
||||
<string>godot-jolt_ios.dylib</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
Binary file not shown.
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>godot-jolt_ios_editor</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Godot Jolt</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Godot Jolt</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>org.godot-jolt.godot-jolt</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright (c) Mikael Hermansson and Godot Jolt contributors.</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>0.16.0</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.16.0</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>iPhoneOS</string>
|
||||
</array>
|
||||
<key>CSResourcesFileMapped</key>
|
||||
<true/>
|
||||
<key>DTPlatformName</key>
|
||||
<string>iphoneos</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>12.0</string>
|
||||
|
||||
<!--
|
||||
HACK(mihe): This is to work around a bug in Godot 4.3-beta1, where it treats Framework
|
||||
bundles the same as XCFramework bundles, and expects there to be an `AvailableLibraries`
|
||||
entry, which is really only a thing in XCFramework bundles. Note that we also lie about the
|
||||
binary path having a `.dylib` extension in order for Godot to correctly identify this as a
|
||||
dynamically linked bundle.
|
||||
-->
|
||||
<key>AvailableLibraries</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>BinaryPath</key>
|
||||
<string>godot-jolt_ios_editor.dylib</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>godot-jolt_macos</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Godot Jolt</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Godot Jolt</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>org.godot-jolt.godot-jolt</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright (c) Mikael Hermansson and Godot Jolt contributors.</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>0.16.0</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.16.0</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>MacOSX</string>
|
||||
</array>
|
||||
<key>CSResourcesFileMapped</key>
|
||||
<true/>
|
||||
<key>DTPlatformName</key>
|
||||
<string>macosx</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.12</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,128 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>files</key>
|
||||
<dict>
|
||||
<key>Resources/Info.plist</key>
|
||||
<data>
|
||||
PLFiiedGCgBUVIdXAwIQZP3TzhU=
|
||||
</data>
|
||||
</dict>
|
||||
<key>files2</key>
|
||||
<dict>
|
||||
<key>Resources/Info.plist</key>
|
||||
<dict>
|
||||
<key>hash2</key>
|
||||
<data>
|
||||
oUfCierc8a2c8bsFW1mu5lCv8dOCnLWbP/Ju8VP12gk=
|
||||
</data>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>rules</key>
|
||||
<dict>
|
||||
<key>^Resources/</key>
|
||||
<true/>
|
||||
<key>^Resources/.*\.lproj/</key>
|
||||
<dict>
|
||||
<key>optional</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>1000</real>
|
||||
</dict>
|
||||
<key>^Resources/.*\.lproj/locversion.plist$</key>
|
||||
<dict>
|
||||
<key>omit</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>1100</real>
|
||||
</dict>
|
||||
<key>^Resources/Base\.lproj/</key>
|
||||
<dict>
|
||||
<key>weight</key>
|
||||
<real>1010</real>
|
||||
</dict>
|
||||
<key>^version.plist$</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>rules2</key>
|
||||
<dict>
|
||||
<key>.*\.dSYM($|/)</key>
|
||||
<dict>
|
||||
<key>weight</key>
|
||||
<real>11</real>
|
||||
</dict>
|
||||
<key>^(.*/)?\.DS_Store$</key>
|
||||
<dict>
|
||||
<key>omit</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>2000</real>
|
||||
</dict>
|
||||
<key>^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/</key>
|
||||
<dict>
|
||||
<key>nested</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>10</real>
|
||||
</dict>
|
||||
<key>^.*</key>
|
||||
<true/>
|
||||
<key>^Info\.plist$</key>
|
||||
<dict>
|
||||
<key>omit</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>20</real>
|
||||
</dict>
|
||||
<key>^PkgInfo$</key>
|
||||
<dict>
|
||||
<key>omit</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>20</real>
|
||||
</dict>
|
||||
<key>^Resources/</key>
|
||||
<dict>
|
||||
<key>weight</key>
|
||||
<real>20</real>
|
||||
</dict>
|
||||
<key>^Resources/.*\.lproj/</key>
|
||||
<dict>
|
||||
<key>optional</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>1000</real>
|
||||
</dict>
|
||||
<key>^Resources/.*\.lproj/locversion.plist$</key>
|
||||
<dict>
|
||||
<key>omit</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>1100</real>
|
||||
</dict>
|
||||
<key>^Resources/Base\.lproj/</key>
|
||||
<dict>
|
||||
<key>weight</key>
|
||||
<real>1010</real>
|
||||
</dict>
|
||||
<key>^[^/]+$</key>
|
||||
<dict>
|
||||
<key>nested</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>10</real>
|
||||
</dict>
|
||||
<key>^embedded\.provisionprofile$</key>
|
||||
<dict>
|
||||
<key>weight</key>
|
||||
<real>20</real>
|
||||
</dict>
|
||||
<key>^version\.plist$</key>
|
||||
<dict>
|
||||
<key>weight</key>
|
||||
<real>20</real>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
Binary file not shown.
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>godot-jolt_macos_editor</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Godot Jolt</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Godot Jolt</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>org.godot-jolt.godot-jolt</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright (c) Mikael Hermansson and Godot Jolt contributors.</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>0.16.0</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.16.0</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>MacOSX</string>
|
||||
</array>
|
||||
<key>CSResourcesFileMapped</key>
|
||||
<true/>
|
||||
<key>DTPlatformName</key>
|
||||
<string>macosx</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.12</string>
|
||||
</dict>
|
||||
</plist>
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>files</key>
|
||||
<dict>
|
||||
<key>Resources/Info.plist</key>
|
||||
<data>
|
||||
xxh2HxAbRksJ2pI8miqkcxNuMMY=
|
||||
</data>
|
||||
</dict>
|
||||
<key>files2</key>
|
||||
<dict>
|
||||
<key>Resources/Info.plist</key>
|
||||
<dict>
|
||||
<key>hash2</key>
|
||||
<data>
|
||||
HuUpHzJ0lNjh0+cAW+2ottBM6fM0woHZDn2U83XmNzY=
|
||||
</data>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>rules</key>
|
||||
<dict>
|
||||
<key>^Resources/</key>
|
||||
<true/>
|
||||
<key>^Resources/.*\.lproj/</key>
|
||||
<dict>
|
||||
<key>optional</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>1000</real>
|
||||
</dict>
|
||||
<key>^Resources/.*\.lproj/locversion.plist$</key>
|
||||
<dict>
|
||||
<key>omit</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>1100</real>
|
||||
</dict>
|
||||
<key>^Resources/Base\.lproj/</key>
|
||||
<dict>
|
||||
<key>weight</key>
|
||||
<real>1010</real>
|
||||
</dict>
|
||||
<key>^version.plist$</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>rules2</key>
|
||||
<dict>
|
||||
<key>.*\.dSYM($|/)</key>
|
||||
<dict>
|
||||
<key>weight</key>
|
||||
<real>11</real>
|
||||
</dict>
|
||||
<key>^(.*/)?\.DS_Store$</key>
|
||||
<dict>
|
||||
<key>omit</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>2000</real>
|
||||
</dict>
|
||||
<key>^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/</key>
|
||||
<dict>
|
||||
<key>nested</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>10</real>
|
||||
</dict>
|
||||
<key>^.*</key>
|
||||
<true/>
|
||||
<key>^Info\.plist$</key>
|
||||
<dict>
|
||||
<key>omit</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>20</real>
|
||||
</dict>
|
||||
<key>^PkgInfo$</key>
|
||||
<dict>
|
||||
<key>omit</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>20</real>
|
||||
</dict>
|
||||
<key>^Resources/</key>
|
||||
<dict>
|
||||
<key>weight</key>
|
||||
<real>20</real>
|
||||
</dict>
|
||||
<key>^Resources/.*\.lproj/</key>
|
||||
<dict>
|
||||
<key>optional</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>1000</real>
|
||||
</dict>
|
||||
<key>^Resources/.*\.lproj/locversion.plist$</key>
|
||||
<dict>
|
||||
<key>omit</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>1100</real>
|
||||
</dict>
|
||||
<key>^Resources/Base\.lproj/</key>
|
||||
<dict>
|
||||
<key>weight</key>
|
||||
<real>1010</real>
|
||||
</dict>
|
||||
<key>^[^/]+$</key>
|
||||
<dict>
|
||||
<key>nested</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>10</real>
|
||||
</dict>
|
||||
<key>^embedded\.provisionprofile$</key>
|
||||
<dict>
|
||||
<key>weight</key>
|
||||
<real>20</real>
|
||||
</dict>
|
||||
<key>^version\.plist$</key>
|
||||
<dict>
|
||||
<key>weight</key>
|
||||
<real>20</real>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,31 @@
|
||||
# Contributors
|
||||
|
||||
This addon, and the entirety of [netfox] is a shared effort of [Fox's Sake
|
||||
Studio], and the community. The following is the list of community contributors
|
||||
involved with netfox:
|
||||
|
||||
* Alberto Klocker <albertok@gmail.com>
|
||||
* Andrew Davis <jonandrewdavis@gmail.com>
|
||||
* Bryan Lee <42545742+bryanmylee@users.noreply.github.com>
|
||||
* Dan Schuman <danschuman@gmail.com>
|
||||
* Dustie <77035922+DustieDog@users.noreply.github.com>
|
||||
* Eric Volpone <ericvolpone@gmail.com>
|
||||
* Gordon MacPherson <gordon@gordonite.tech>
|
||||
* Jake Cattrall <krazyjakee@gmail.com>
|
||||
* Jon Stevens <1030863+jonstvns@users.noreply.github.com>
|
||||
* Joseph Michael Ware <9at25jnr3@mozmail.com>
|
||||
* Matias Kumpulainen <kumpulainen.matias@gmail.com>
|
||||
* Nicolas Batty <nicolas.batty@gmail.com>
|
||||
* Ricky <77863853+RickyYCheng@users.noreply.github.com>
|
||||
* Riordan-DC <44295008+Riordan-DC@users.noreply.github.com>
|
||||
* Russ <622740+russ-@users.noreply.github.com>
|
||||
* Ryan Roden-Corrent <github@rcorre.net>
|
||||
* TheYellowArchitect <dmalandris@uth.gr>
|
||||
* TheYellowArchitect <hello@theyellowarchitect.com>
|
||||
* camperotactico <alberto.vgdd@gmail.com>
|
||||
* gk98s <89647115+gk98s@users.noreply.github.com>
|
||||
* zibetnu <9at25jnr3@mozmail.com>
|
||||
|
||||
[netfox]: https://github.com/foxssake/netfox
|
||||
[Fox's Sake Studio]: https://github.com/foxssake/
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
Copyright 2023 Gálffy Tamás
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,34 @@
|
||||
# netfox.extras
|
||||
|
||||
High-level, game-specific extras for [netfox]!
|
||||
|
||||
## Features
|
||||
|
||||
* 🔫 Networked weapons
|
||||
* ⌨️ Rollback-aware base class for input
|
||||
|
||||
## Overview
|
||||
|
||||
This addon is pretty much a catch-all project for things that are useful, but
|
||||
are not core to netfox.
|
||||
|
||||
## Install
|
||||
|
||||
See the root [README](../../README.md).
|
||||
|
||||
## Usage
|
||||
|
||||
See the [docs](https://foxssake.github.io/netfox/).
|
||||
|
||||
## License
|
||||
|
||||
netfox.extras is under the [MIT license](LICENSE).
|
||||
|
||||
## Issues
|
||||
|
||||
In case of any issues, comments, or questions, please feel free to [open an issue]!
|
||||
|
||||
[netfox]: https://github.com/foxssake/netfox
|
||||
[source]: https://github.com/foxssake/netfox/archive/refs/heads/main.zip
|
||||
[open an issue]: https://github.com/foxssake/netfox/issues
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
extends Node
|
||||
class_name BaseNetInput
|
||||
|
||||
## Base class for Input nodes used with rollback.
|
||||
|
||||
func _ready():
|
||||
NetworkTime.before_tick_loop.connect(func():
|
||||
if is_multiplayer_authority():
|
||||
_gather()
|
||||
)
|
||||
|
||||
## Method for gathering input.
|
||||
##
|
||||
## This method is supposed to be overridden with your input logic. The input
|
||||
## data itself may be gathered outside of this method ( e.g. gathering it over
|
||||
## multiple _process calls ), but this is the point where the input variables
|
||||
## must be set.
|
||||
##
|
||||
## [i]Note:[/i] This is only called for the local player's input nodes.
|
||||
func _gather():
|
||||
pass
|
||||
@@ -0,0 +1 @@
|
||||
uid://cpiuf65vuu0oa
|
||||
@@ -0,0 +1,68 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="network-rigid-body-2d.svg"
|
||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="22.627417"
|
||||
inkscape:cx="-6.098796"
|
||||
inkscape:cy="12.705825"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1043"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1"
|
||||
showgrid="true">
|
||||
<inkscape:grid
|
||||
id="grid1"
|
||||
units="px"
|
||||
originx="0"
|
||||
originy="0"
|
||||
spacingx="1"
|
||||
spacingy="1"
|
||||
empcolor="#0099e5"
|
||||
empopacity="0.30196078"
|
||||
color="#0099e5"
|
||||
opacity="0.14901961"
|
||||
empspacing="5"
|
||||
enabled="true"
|
||||
visible="true" />
|
||||
</sodipodi:namedview>
|
||||
<path
|
||||
fill="#8da5f3"
|
||||
d="M 8,1 A 7,7 0 0 0 8,15 7,7 0 0 0 8,1 Z M 9.951,2.334 A 6,6 0 0 1 14.001,8 h -7 A 2,2 0 0 0 6.051,6.3 C 7.397,4.271 8.655,3.037 9.951,2.333 Z m -6.828,2.18 c 0.146,0.65 0.358,1.283 0.684,1.884 A 2,2 0 0 0 3,8 H 2 A 6,6 0 0 1 3.123,4.514 Z M 3.8,9.6 a 2,2 0 0 0 2.4,0 c 1.472,2.027 2.728,3.264 3.8,4 A 6,6 0 0 1 2.85,11.1 c 0.25,-0.6 0.5,-1 0.95,-1.5 z"
|
||||
id="path1"
|
||||
style="stroke-width:1" />
|
||||
<g
|
||||
id="g9"
|
||||
transform="matrix(0.25,0,0,0.25,0,0.825)">
|
||||
<path
|
||||
id="path1_00000101101429107098172700000017609064003007162547_"
|
||||
class="st1"
|
||||
d="M 32,40.2 23.8,56.6 15.6,40.2 Z"
|
||||
style="fill:#5fff97" />
|
||||
<path
|
||||
id="path2_00000126280959979031010980000006949781421479937947_"
|
||||
class="st2"
|
||||
d="M 48.4,56.7 40.2,40.3 32,56.7 Z"
|
||||
style="fill:#ff5f5f" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="network-rigid-body-3d.svg"
|
||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="21.364726"
|
||||
inkscape:cx="21.109561"
|
||||
inkscape:cy="8.776148"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1043"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" />
|
||||
<path
|
||||
fill="#fc7f7f"
|
||||
d="M 8,1 A 7,7 0 0 0 8,15 7,7 0 0 0 8,1 Z M 9.951,2.334 A 6,6 0 0 1 14.001,8 h -7 A 2,2 0 0 0 6.051,6.3 C 7.397,4.271 8.655,3.037 9.951,2.333 Z m -6.828,2.18 c 0.146,0.65 0.358,1.283 0.684,1.884 A 2,2 0 0 0 3,8 H 2 A 6,6 0 0 1 3.123,4.514 Z M 3.8,9.6 a 2,2 0 0 0 2.4,0 c 1.472,2.027 2.728,3.264 3.8,4 A 6,6 0 0 1 2.85,11.1 c 0.25,-0.6 0.5,-1 0.95,-1.5 z"
|
||||
id="path1"
|
||||
style="stroke-width:1" />
|
||||
<g
|
||||
id="g9"
|
||||
transform="matrix(0.25,0,0,0.25,0,0.825)">
|
||||
<path
|
||||
id="path1_00000101101429107098172700000017609064003007162547_"
|
||||
class="st1"
|
||||
d="M 32,40.2 23.8,56.6 15.6,40.2 Z"
|
||||
style="fill:#5fff97" />
|
||||
<path
|
||||
id="path2_00000126280959979031010980000006949781421479937947_"
|
||||
class="st2"
|
||||
d="M 48.4,56.7 40.2,40.3 32,56.7 Z"
|
||||
style="fill:#ff5f5f" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,91 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 16 16"
|
||||
xml:space="preserve"
|
||||
sodipodi:docname="rewindable-state-machine.svg"
|
||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||
width="16"
|
||||
height="16"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1"><inkscape:perspective
|
||||
sodipodi:type="inkscape:persp3d"
|
||||
inkscape:vp_x="0 : -47.5 : 1"
|
||||
inkscape:vp_y="0 : 15.625 : 0"
|
||||
inkscape:vp_z="1 : -47.5 : 1"
|
||||
inkscape:persp3d-origin="0.5 : -47.666667 : 1"
|
||||
id="perspective1" /></defs><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="24.411443"
|
||||
inkscape:cx="2.5397925"
|
||||
inkscape:cy="9.4832576"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1043"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Layer_1" />
|
||||
<style
|
||||
type="text/css"
|
||||
id="style1">
|
||||
.st0{fill:none;stroke:#E0E0E0;stroke-width:6.875;stroke-linecap:round;stroke-linejoin:round;}
|
||||
.st1{fill:#5FFF97;}
|
||||
.st2{fill:#FF5F5F;}
|
||||
.st3{opacity:0.8;}
|
||||
</style>
|
||||
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:none;stroke:#e0e0e0;stroke-width:10.42235363;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path1"
|
||||
inkscape:flatsided="true"
|
||||
sodipodi:sides="6"
|
||||
sodipodi:cx="8.0625"
|
||||
sodipodi:cy="8.0625"
|
||||
sodipodi:r1="27.830996"
|
||||
sodipodi:r2="24.102348"
|
||||
sodipodi:arg1="0.53701091"
|
||||
sodipodi:arg2="1.0606097"
|
||||
inkscape:rounded="-9.1940344e-17"
|
||||
inkscape:randomized="0"
|
||||
d="M 31.97605,22.3 7.6892381,35.890992 -16.224312,21.653492 -15.85105,-6.1750005 8.4357619,-19.765992 32.349312,-5.5284919 Z"
|
||||
transform="matrix(0.19189523,0,0,0.19189524,6.4528447,6.4528446)" />
|
||||
|
||||
|
||||
<circle
|
||||
style="fill:none;stroke:#e0e0e0;stroke-width:1;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path9"
|
||||
cx="7.9999995"
|
||||
cy="7.8039598"
|
||||
r="2.5" /><circle
|
||||
style="fill:none;stroke:#e0e0e0;stroke-width:1;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:0.66"
|
||||
id="circle9"
|
||||
cx="6.9999995"
|
||||
cy="7.8039598"
|
||||
r="2.5" /><circle
|
||||
style="fill:none;stroke:#e0e0e0;stroke-width:1;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:0.33"
|
||||
id="circle10"
|
||||
cx="5.9999995"
|
||||
cy="7.8039598"
|
||||
r="2.5" /><g
|
||||
id="g9"
|
||||
transform="matrix(0.25,0,0,0.25,0,-0.19604137)"><path
|
||||
id="path1_00000101101429107098172700000017609064003007162547_"
|
||||
class="st1"
|
||||
d="M 32,40.2 23.8,56.6 15.6,40.2 Z" /><path
|
||||
id="path2_00000126280959979031010980000006949781421479937947_"
|
||||
class="st2"
|
||||
d="M 48.4,56.7 40.2,40.3 32,56.7 Z" /></g></svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1,124 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 16 16"
|
||||
xml:space="preserve"
|
||||
sodipodi:docname="rewindable-state.svg"
|
||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||
width="16"
|
||||
height="16"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1"><inkscape:perspective
|
||||
sodipodi:type="inkscape:persp3d"
|
||||
inkscape:vp_x="0 : -47.5 : 1"
|
||||
inkscape:vp_y="0 : 15.625 : 0"
|
||||
inkscape:vp_z="1 : -47.5 : 1"
|
||||
inkscape:persp3d-origin="0.5 : -47.666667 : 1"
|
||||
id="perspective1" /></defs><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="32"
|
||||
inkscape:cx="6.28125"
|
||||
inkscape:cy="8.328125"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1043"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Layer_1" />
|
||||
<style
|
||||
type="text/css"
|
||||
id="style1">
|
||||
.st0{fill:none;stroke:#E0E0E0;stroke-width:6.875;stroke-linecap:round;stroke-linejoin:round;}
|
||||
.st1{fill:#5FFF97;}
|
||||
.st2{fill:#FF5F5F;}
|
||||
.st3{opacity:0.8;}
|
||||
</style>
|
||||
|
||||
<g
|
||||
id="g8"
|
||||
transform="matrix(0.25,0,0,0.25,10.94454,-0.19604083)"
|
||||
style="stroke:#e0e0e0;stroke-opacity:0.33;stroke-width:8;stroke-dasharray:none"><path
|
||||
sodipodi:type="star"
|
||||
style="fill:none;stroke:#e0e0e0;stroke-width:10.42235363;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:0.33"
|
||||
id="path7"
|
||||
inkscape:flatsided="true"
|
||||
sodipodi:sides="6"
|
||||
sodipodi:cx="8.0625"
|
||||
sodipodi:cy="8.0625"
|
||||
sodipodi:r1="27.830996"
|
||||
sodipodi:r2="24.102348"
|
||||
sodipodi:arg1="0.53701091"
|
||||
sodipodi:arg2="1.0606097"
|
||||
inkscape:rounded="-9.1940344e-17"
|
||||
inkscape:randomized="0"
|
||||
d="M 31.97605,22.3 7.6892381,35.890992 -16.224312,21.653492 -15.85105,-6.1750005 8.4357619,-19.765992 32.349312,-5.5284919 Z"
|
||||
transform="matrix(0.76758094,0,0,0.76758094,-21.96678,26.595542)" /><path
|
||||
style="fill:none;stroke:#e0e0e0;stroke-width:8;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:0.33"
|
||||
d="m -34.133744,21.855729 18.355585,10.928434 -0.286508,21.360621"
|
||||
id="path8" /></g><g
|
||||
id="g6"
|
||||
transform="matrix(0.25,0,0,0.25,11.94454,-0.19604083)"
|
||||
style="stroke:#e0e0e0;stroke-opacity:0.66;stroke-width:8;stroke-dasharray:none"><path
|
||||
sodipodi:type="star"
|
||||
style="fill:none;stroke:#e0e0e0;stroke-width:10.42235363;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:0.66"
|
||||
id="path5"
|
||||
inkscape:flatsided="true"
|
||||
sodipodi:sides="6"
|
||||
sodipodi:cx="8.0625"
|
||||
sodipodi:cy="8.0625"
|
||||
sodipodi:r1="27.830996"
|
||||
sodipodi:r2="24.102348"
|
||||
sodipodi:arg1="0.53701091"
|
||||
sodipodi:arg2="1.0606097"
|
||||
inkscape:rounded="-9.1940344e-17"
|
||||
inkscape:randomized="0"
|
||||
d="M 31.97605,22.3 7.6892381,35.890992 -16.224312,21.653492 -15.85105,-6.1750005 8.4357619,-19.765992 32.349312,-5.5284919 Z"
|
||||
transform="matrix(0.76758094,0,0,0.76758094,-21.96678,26.595542)" /><path
|
||||
style="fill:none;stroke:#e0e0e0;stroke-width:8;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:0.66"
|
||||
d="m -34.133744,21.855729 18.355585,10.928434 -0.286508,21.360621"
|
||||
id="path6" /></g><g
|
||||
id="g4"
|
||||
transform="matrix(0.25,0,0,0.25,12.94454,-0.19604083)"
|
||||
style="stroke-width:8;stroke-dasharray:none"><path
|
||||
sodipodi:type="star"
|
||||
style="fill:none;stroke:#e0e0e0;stroke-width:10.42235363;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path1"
|
||||
inkscape:flatsided="true"
|
||||
sodipodi:sides="6"
|
||||
sodipodi:cx="8.0625"
|
||||
sodipodi:cy="8.0625"
|
||||
sodipodi:r1="27.830996"
|
||||
sodipodi:r2="24.102348"
|
||||
sodipodi:arg1="0.53701091"
|
||||
sodipodi:arg2="1.0606097"
|
||||
inkscape:rounded="-9.1940344e-17"
|
||||
inkscape:randomized="0"
|
||||
d="M 31.97605,22.3 7.6892381,35.890992 -16.224312,21.653492 -15.85105,-6.1750005 8.4357619,-19.765992 32.349312,-5.5284919 Z"
|
||||
transform="matrix(0.76758094,0,0,0.76758094,-21.96678,26.595542)" /><path
|
||||
style="fill:none;stroke:#e0e0e0;stroke-width:8;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m -34.133744,21.855729 18.355585,10.928434 -0.286508,21.360621"
|
||||
id="path4" /></g><path
|
||||
id="path1_00000101101429107098172700000017609064003007162547_"
|
||||
class="st1"
|
||||
d="m 7.9999998,9.8539592 -2.05,4.0999998 -2.0499999,-4.0999998 z"
|
||||
style="stroke-width:2;stroke-dasharray:none" />
|
||||
<path
|
||||
id="path2_00000126280959979031010980000006949781421479937947_"
|
||||
class="st2"
|
||||
d="M 12.1,13.978959 10.05,9.8789592 7.9999998,13.978959 Z"
|
||||
style="stroke-width:2;stroke-dasharray:none" />
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
@@ -0,0 +1,187 @@
|
||||
@tool
|
||||
extends EditorPlugin
|
||||
|
||||
const ROOT = "res://addons/netfox.extras"
|
||||
|
||||
var SETTINGS = [
|
||||
NetfoxLogger._make_setting("netfox/logging/netfox_extras_log_level"),
|
||||
|
||||
# Window Tiler Settings
|
||||
{
|
||||
"name": "netfox/extras/auto_tile_windows",
|
||||
"value": false,
|
||||
"type": TYPE_BOOL
|
||||
},
|
||||
{
|
||||
"name": "netfox/extras/screen",
|
||||
"value": 0,
|
||||
"type": TYPE_INT
|
||||
},
|
||||
{
|
||||
"name": "netfox/extras/borderless",
|
||||
"value": false,
|
||||
"type": TYPE_BOOL
|
||||
},
|
||||
|
||||
# Autoconnect settings
|
||||
{
|
||||
"name": "netfox/autoconnect/enabled",
|
||||
"value": false,
|
||||
"type": TYPE_BOOL
|
||||
},
|
||||
{
|
||||
"name": "netfox/autoconnect/host",
|
||||
"value": "127.0.0.1",
|
||||
"type": TYPE_STRING
|
||||
},
|
||||
{
|
||||
"name": "netfox/autoconnect/port",
|
||||
"value": 9999,
|
||||
"type": TYPE_INT,
|
||||
"hint": PROPERTY_HINT_RANGE,
|
||||
"hint_string": "1,65535,hide_slider"
|
||||
},
|
||||
{
|
||||
"name": "netfox/autoconnect/use_compression",
|
||||
"value": false,
|
||||
"type": TYPE_BOOL
|
||||
},
|
||||
{
|
||||
"name": "netfox/autoconnect/simulated_latency_ms",
|
||||
"value": 0.0,
|
||||
"type": TYPE_INT,
|
||||
"hint": PROPERTY_HINT_RANGE,
|
||||
"hint_string": "0,200,or_greater"
|
||||
},
|
||||
{
|
||||
"name": "netfox/autoconnect/simulated_packet_loss_chance",
|
||||
"value": 0.0,
|
||||
"type": TYPE_FLOAT,
|
||||
"hint": PROPERTY_HINT_RANGE,
|
||||
"hint_string": "0,1"
|
||||
}
|
||||
]
|
||||
|
||||
const AUTOLOADS = [
|
||||
{
|
||||
"name": "WindowTiler",
|
||||
"path": ROOT + "/window-tiler.gd"
|
||||
},
|
||||
{
|
||||
"name": "NetworkSimulator",
|
||||
"path": ROOT + "/network-simulator.gd"
|
||||
}
|
||||
]
|
||||
|
||||
const TYPES: Array[Dictionary] = [
|
||||
{
|
||||
"name": "RewindableStateMachine",
|
||||
"base": "Node",
|
||||
"script": ROOT + "/state-machine/rewindable-state-machine.gd",
|
||||
"icon": ROOT + "/icons/rewindable-state-machine.svg"
|
||||
},
|
||||
{
|
||||
"name": "RewindableState",
|
||||
"base": "Node",
|
||||
"script": ROOT + "/state-machine/rewindable-state.gd",
|
||||
"icon": ROOT + "/icons/rewindable-state.svg"
|
||||
},
|
||||
{
|
||||
"name": "NetworkRigidBody2D",
|
||||
"base": "RigidBody2D",
|
||||
"script": ROOT + "/physics/network-rigid-body-2d.gd",
|
||||
"icon": ROOT + "/icons/network-rigid-body-2d.svg"
|
||||
},
|
||||
{
|
||||
"name": "NetworkRigidBody3D",
|
||||
"base": "RigidBody3D",
|
||||
"script": ROOT + "/physics/network-rigid-body-3d.gd",
|
||||
"icon": ROOT + "/icons/network-rigid-body-3d.svg"
|
||||
},
|
||||
]
|
||||
|
||||
const PhysicsDriverToggles := preload("res://addons/netfox.extras/physics/physics-driver-toggles.gd")
|
||||
|
||||
var _tool_menu_items := [] as Array[String]
|
||||
|
||||
func _enter_tree():
|
||||
for setting in SETTINGS:
|
||||
add_setting(setting)
|
||||
|
||||
for autoload in AUTOLOADS:
|
||||
add_autoload_singleton(autoload.name, autoload.path)
|
||||
|
||||
for type in TYPES:
|
||||
add_custom_type(type.name, type.base, load(type.script), load(type.icon))
|
||||
|
||||
_render_tool_menu()
|
||||
|
||||
func _exit_tree():
|
||||
if ProjectSettings.get_setting("netfox/general/clear_settings", false):
|
||||
for setting in SETTINGS:
|
||||
remove_setting(setting)
|
||||
|
||||
for autoload in AUTOLOADS:
|
||||
remove_autoload_singleton(autoload.name)
|
||||
|
||||
for type in TYPES:
|
||||
remove_custom_type(type.name)
|
||||
|
||||
_free_tool_menu()
|
||||
|
||||
func add_setting(setting: Dictionary):
|
||||
if ProjectSettings.has_setting(setting.name):
|
||||
return
|
||||
|
||||
ProjectSettings.set_setting(setting.name, setting.value)
|
||||
ProjectSettings.set_initial_value(setting.name, setting.value)
|
||||
ProjectSettings.add_property_info({
|
||||
"name": setting.get("name"),
|
||||
"type": setting.get("type"),
|
||||
"hint": setting.get("hint", PROPERTY_HINT_NONE),
|
||||
"hint_string": setting.get("hint_string", "")
|
||||
})
|
||||
|
||||
func remove_setting(setting: Dictionary):
|
||||
if not ProjectSettings.has_setting(setting.name):
|
||||
return
|
||||
|
||||
ProjectSettings.clear(setting.name)
|
||||
|
||||
func _render_tool_menu():
|
||||
_free_tool_menu()
|
||||
for driver_toggle in PhysicsDriverToggles.all():
|
||||
var prefix := "Enable" if not driver_toggle.is_enabled() else "Disable"
|
||||
var item := "%s %s physics driver" % [prefix, driver_toggle.get_name()]
|
||||
|
||||
_tool_menu_items.append(item)
|
||||
|
||||
add_tool_menu_item(item, func():
|
||||
# Toggle physics driver, then re-render menu to show changes
|
||||
_call_physics_driver_toggle(driver_toggle)
|
||||
_render_tool_menu()
|
||||
)
|
||||
|
||||
func _free_tool_menu():
|
||||
for item in _tool_menu_items:
|
||||
remove_tool_menu_item(item)
|
||||
_tool_menu_items.clear()
|
||||
|
||||
func _call_physics_driver_toggle(driver_toggle: PhysicsDriverToggles.PhysicsDriverToggle):
|
||||
var error_messages := driver_toggle.toggle()
|
||||
if not error_messages.is_empty():
|
||||
var error_text := "\n".join(error_messages)
|
||||
|
||||
var dialog := AcceptDialog.new()
|
||||
dialog.title = "Physics driver toggle failed!"
|
||||
dialog.dialog_text = error_text
|
||||
|
||||
get_editor_interface().popup_dialog_centered(dialog)
|
||||
else:
|
||||
var dialog := AcceptDialog.new()
|
||||
dialog.title = "Physics driver toggle success!"
|
||||
dialog.dialog_text = ("%s physics driver was successfully toggled! " +
|
||||
"You might need to refresh your script or reload project.") %\
|
||||
[driver_toggle.get_name()]
|
||||
|
||||
get_editor_interface().popup_dialog_centered(dialog)
|
||||
@@ -0,0 +1 @@
|
||||
uid://ee1cb3bj7opr
|
||||
@@ -0,0 +1,238 @@
|
||||
extends Node
|
||||
|
||||
## Network Simulator
|
||||
##
|
||||
## Auto connects launched instances and simulates network conditions like
|
||||
## latency and packet loss. To use simply add this node to your scene tree and
|
||||
## hook up the signals.
|
||||
|
||||
## Signal emitted on the instance that successfully started a server.
|
||||
## Can be used for custom initialization logic, e.g. automatic login during
|
||||
## testing.
|
||||
signal server_created
|
||||
|
||||
## Signal emitted on instances that successfully connected as a client.
|
||||
## Can be used for custom initialization logic, e.g. automatic login during
|
||||
## testing.
|
||||
signal client_connected
|
||||
|
||||
## Enable to automatically host and connect on start
|
||||
var enabled: bool = false
|
||||
## Server listening address. Use [code]*[/code] for all interfaces, or
|
||||
#3 [code]127.0.0.1[/code] for localhost.
|
||||
var hostname: String = "127.0.0.1"
|
||||
|
||||
## Server port to listen on, UDP proxy will use port + 1 if simulating latency
|
||||
## or packet loss
|
||||
var server_port: int = 9999
|
||||
|
||||
## Use ENet's built-in range encoding for compression
|
||||
var use_compression: bool = true
|
||||
|
||||
## Simulated latency in milliseconds. Total ping time will be double this value
|
||||
## (to and from)
|
||||
var latency_ms: int = 0
|
||||
## Simulated packet loss percentage
|
||||
var packet_loss_percent: float = 0.0
|
||||
|
||||
static var _logger: NetfoxLogger = NetfoxLogger._for_extras("NetworkSimulator")
|
||||
|
||||
var _enet_peer: ENetMultiplayerPeer = ENetMultiplayerPeer.new()
|
||||
|
||||
# UDP proxy
|
||||
var _proxy_thread: Thread
|
||||
var _proxy_loop_enabled := true
|
||||
var _udp_proxy_server: PacketPeerUDP
|
||||
var _udp_proxy_port: int
|
||||
var _rng_packet_loss: RandomNumberGenerator = RandomNumberGenerator.new()
|
||||
|
||||
# Connection tracking
|
||||
var _client_peers: Dictionary = {} # port to PacketPeerUDP
|
||||
var _client_to_server_queue: Array[QueueEntry] = []
|
||||
var _server_to_client_queue: Array[QueueEntry] = []
|
||||
|
||||
class QueueEntry:
|
||||
var packet_data: PackedByteArray
|
||||
var queued_at: int
|
||||
var source_port: int # Which client port this came from
|
||||
|
||||
func _init(packet: PackedByteArray, timestamp: int, port: int) -> void:
|
||||
self.packet_data = packet
|
||||
self.queued_at = timestamp
|
||||
self.source_port = port
|
||||
|
||||
func _ready() -> void:
|
||||
# Check if enabled
|
||||
if not OS.has_feature("editor"):
|
||||
_logger.debug("Running outside editor, disabling")
|
||||
return
|
||||
|
||||
_load_project_settings()
|
||||
if not enabled:
|
||||
_logger.debug("Feature disabled")
|
||||
return
|
||||
|
||||
for env_var in ["CI", "NETFOX_CI", "NETFOX_NO_AUTOCONNECT"]:
|
||||
if OS.get_environment(env_var) != "":
|
||||
_logger.debug("Environment variable %s set, disabling", [env_var])
|
||||
return
|
||||
|
||||
await get_tree().process_frame
|
||||
_udp_proxy_port = server_port + 1
|
||||
|
||||
var status = _try_and_host()
|
||||
if status == Error.ERR_CANT_CREATE:
|
||||
_try_and_join()
|
||||
elif status != OK:
|
||||
_logger.error("Autoconnect failed with error - %s", [error_string(status)])
|
||||
|
||||
if use_compression:
|
||||
_enet_peer.host.compress(ENetConnection.COMPRESS_RANGE_CODER)
|
||||
|
||||
multiplayer.multiplayer_peer = _enet_peer
|
||||
|
||||
func _is_proxy_required() -> bool:
|
||||
return latency_ms > 0 or packet_loss_percent > 0.0
|
||||
|
||||
func _try_and_host() -> Error:
|
||||
var status = _enet_peer.create_server(server_port)
|
||||
if status == OK:
|
||||
if _is_proxy_required():
|
||||
_start_udp_proxy()
|
||||
server_created.emit()
|
||||
_logger.info("Server started on port %s", [server_port])
|
||||
return status
|
||||
|
||||
func _try_and_join() -> Error:
|
||||
var connect_port = server_port
|
||||
if _is_proxy_required():
|
||||
connect_port = _udp_proxy_port
|
||||
var status = _enet_peer.create_client(hostname, connect_port)
|
||||
if status == OK:
|
||||
client_connected.emit()
|
||||
_logger.info("Client connected to %s:%s", [hostname, connect_port])
|
||||
return status
|
||||
|
||||
# Starts a UDP proxy server to simulate network conditions
|
||||
# This will listen on _udp_proxy_port and forward packets to the server_port
|
||||
# Runs on its own thread to avoid blocking the main thread
|
||||
func _start_udp_proxy() -> void:
|
||||
_proxy_thread = Thread.new()
|
||||
_udp_proxy_server = PacketPeerUDP.new()
|
||||
|
||||
var bind_status = _udp_proxy_server.bind(_udp_proxy_port, hostname)
|
||||
if bind_status != OK:
|
||||
_logger.error("Failed to bind UDP proxy port: ", bind_status)
|
||||
return
|
||||
|
||||
_proxy_thread.start(_process_loop)
|
||||
|
||||
func _process_packets() -> void:
|
||||
var current_time: int = Time.get_ticks_msec()
|
||||
var send_threshold: int = current_time - latency_ms
|
||||
|
||||
_read_client_to_server_packets(current_time)
|
||||
_process_client_to_server_packets(send_threshold)
|
||||
|
||||
if not _client_peers.is_empty():
|
||||
_read_server_to_client_packets(current_time)
|
||||
_process_server_to_client_queue(send_threshold)
|
||||
|
||||
func _process_loop():
|
||||
while _proxy_loop_enabled:
|
||||
_process_packets()
|
||||
OS.delay_msec(1)
|
||||
|
||||
func _load_project_settings() -> void:
|
||||
enabled = ProjectSettings.get_setting(&"netfox/autoconnect/enabled", false)
|
||||
hostname = ProjectSettings.get_setting(&"netfox/autoconnect/host", "127.0.0.1")
|
||||
server_port = ProjectSettings.get_setting(&"netfox/autoconnect/port", 9999)
|
||||
use_compression = ProjectSettings.get_setting(&"netfox/autoconnect/use_compression", false)
|
||||
latency_ms = ProjectSettings.get_setting(&"netfox/autoconnect/simulated_latency_ms", 0)
|
||||
packet_loss_percent = ProjectSettings.get_setting(&"netfox/autoconnect/simulated_packet_loss_chance", 0.0)
|
||||
|
||||
func _is_data_available() -> bool:
|
||||
if _udp_proxy_server.get_available_packet_count() > 0:
|
||||
return true
|
||||
|
||||
if not _client_to_server_queue.is_empty() or not _server_to_client_queue.is_empty():
|
||||
return true
|
||||
|
||||
# Check if any client peers have packets waiting
|
||||
for client_peer in _client_peers.values():
|
||||
if client_peer.get_available_packet_count() > 0:
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
func _read_client_to_server_packets(current_time: int) -> void:
|
||||
while _udp_proxy_server.get_available_packet_count() > 0:
|
||||
var packet = _udp_proxy_server.get_packet()
|
||||
var err = _udp_proxy_server.get_packet_error()
|
||||
|
||||
if err != OK:
|
||||
_logger.error("UDP proxy incoming packet error: ", err)
|
||||
continue
|
||||
|
||||
var from_port = _udp_proxy_server.get_packet_port()
|
||||
_register_client_if_new(from_port)
|
||||
|
||||
_client_to_server_queue.push_back(QueueEntry.new(packet, current_time, from_port))
|
||||
|
||||
func _register_client_if_new(port: int) -> void:
|
||||
if _client_peers.has(port):
|
||||
return
|
||||
|
||||
# Create a dedicated peer for this client
|
||||
var client_peer = PacketPeerUDP.new()
|
||||
client_peer.set_dest_address(hostname, server_port)
|
||||
_client_peers[port] = client_peer
|
||||
|
||||
func _process_client_to_server_packets(send_threshold: int) -> void:
|
||||
var packets_to_keep: Array[QueueEntry] = []
|
||||
|
||||
for entry in _client_to_server_queue:
|
||||
if send_threshold < entry.queued_at:
|
||||
packets_to_keep.append(entry)
|
||||
else:
|
||||
if _should_send_packet():
|
||||
var peer = _client_peers[entry.source_port] as PacketPeerUDP
|
||||
peer.put_packet(entry.packet_data)
|
||||
|
||||
_client_to_server_queue = packets_to_keep
|
||||
|
||||
func _read_server_to_client_packets(current_time: int) -> void:
|
||||
for client_port in _client_peers.keys():
|
||||
var client_peer = _client_peers[client_port] as PacketPeerUDP
|
||||
|
||||
while client_peer.get_available_packet_count() > 0:
|
||||
var packet = client_peer.get_packet()
|
||||
var err = client_peer.get_packet_error()
|
||||
|
||||
if err != OK:
|
||||
_logger.error("UDP proxy server-to-client packet error from port %s : %s", [client_port, err])
|
||||
continue
|
||||
|
||||
_server_to_client_queue.push_back(QueueEntry.new(packet, current_time, client_port))
|
||||
|
||||
func _process_server_to_client_queue(send_threshold: int) -> void:
|
||||
var packets_to_keep: Array[QueueEntry] = []
|
||||
|
||||
for entry in _server_to_client_queue:
|
||||
if send_threshold < entry.queued_at:
|
||||
packets_to_keep.append(entry)
|
||||
else:
|
||||
if _should_send_packet():
|
||||
_udp_proxy_server.set_dest_address(hostname, entry.source_port)
|
||||
_udp_proxy_server.put_packet(entry.packet_data)
|
||||
|
||||
_server_to_client_queue = packets_to_keep
|
||||
|
||||
# Send packet or simulate loss
|
||||
func _should_send_packet() -> bool:
|
||||
return packet_loss_percent <= 0.0 or _rng_packet_loss.randf() >= (packet_loss_percent / 100.0)
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if _proxy_thread and _proxy_thread.is_started():
|
||||
_proxy_loop_enabled = false
|
||||
_proxy_thread.wait_to_finish()
|
||||
@@ -0,0 +1 @@
|
||||
uid://1y2ey1e6qjwr
|
||||
@@ -0,0 +1,72 @@
|
||||
extends PhysicsDriver
|
||||
|
||||
class_name PhysicsDriver2D
|
||||
|
||||
# Physics driver based on netfox ticks
|
||||
# Requires a custom build of Godot with https://github.com/godotengine/godot/pull/76462
|
||||
|
||||
var scene_collision_objects: Array = []
|
||||
var collision_objects_snapshots: Dictionary[int, Dictionary] = {}
|
||||
|
||||
func _init_physics_space() -> void:
|
||||
physics_space = get_viewport().world_2d.space
|
||||
PhysicsServer2D.space_set_active(physics_space, false)
|
||||
|
||||
get_tree().node_added.connect(node_added)
|
||||
get_tree().node_removed.connect(node_removed)
|
||||
scan_tree()
|
||||
|
||||
func _physics_step(delta) -> void:
|
||||
PhysicsServer2D.space_flush_queries(physics_space)
|
||||
PhysicsServer2D.space_step(physics_space, delta)
|
||||
|
||||
func _snapshot_space(tick: int) -> void:
|
||||
var rid_states: Dictionary[RID, Array] = {}
|
||||
for element in scene_collision_objects:
|
||||
if element is CharacterBody2D:
|
||||
element.force_update_transform() # force colliders to update
|
||||
|
||||
var rid = element.get_rid()
|
||||
rid_states[rid] = get_body_states(rid)
|
||||
snapshots[tick] = rid_states
|
||||
|
||||
func _rollback_space(tick) -> void:
|
||||
if snapshots.has(tick):
|
||||
var rid_states = snapshots[tick]
|
||||
for rid in rid_states.keys():
|
||||
set_body_states(rid, rid_states[rid])
|
||||
|
||||
for body in scene_collision_objects:
|
||||
if body is CharacterBody2D or body is AnimatableBody2D:
|
||||
body.force_update_transform() # force colliders to update
|
||||
|
||||
func get_body_states(rid: RID) -> Array:
|
||||
var body_state: Array = [Vector3.ZERO, Quaternion.IDENTITY, Vector3.ZERO, Vector3.ZERO]
|
||||
body_state[0] = PhysicsServer2D.body_get_state(rid, PhysicsServer2D.BODY_STATE_TRANSFORM)
|
||||
body_state[1] = PhysicsServer2D.body_get_state(rid, PhysicsServer2D.BODY_STATE_LINEAR_VELOCITY)
|
||||
body_state[2] = PhysicsServer2D.body_get_state(rid, PhysicsServer2D.BODY_STATE_ANGULAR_VELOCITY)
|
||||
body_state[3] = PhysicsServer2D.body_get_state(rid, PhysicsServer2D.BODY_STATE_SLEEPING)
|
||||
return body_state
|
||||
|
||||
func set_body_states(rid: RID, body_state: Array) -> void:
|
||||
PhysicsServer2D.body_set_state(rid, PhysicsServer2D.BODY_STATE_TRANSFORM, body_state[0])
|
||||
PhysicsServer2D.body_set_state(rid, PhysicsServer2D.BODY_STATE_LINEAR_VELOCITY, body_state[1])
|
||||
PhysicsServer2D.body_set_state(rid, PhysicsServer2D.BODY_STATE_ANGULAR_VELOCITY, body_state[2])
|
||||
PhysicsServer2D.body_set_state(rid, PhysicsServer2D.BODY_STATE_SLEEPING, body_state[3])
|
||||
|
||||
func scan_tree():
|
||||
scene_collision_objects.clear()
|
||||
scene_collision_objects = get_all_children(get_node('/root'))
|
||||
|
||||
func get_all_children(in_node: Node) -> Array:
|
||||
var nodes = []
|
||||
nodes = in_node.find_children("*", "PhysicsBody2D", true, false)
|
||||
return nodes
|
||||
|
||||
func node_added(node: Node) -> void:
|
||||
if node is PhysicsBody2D:
|
||||
scene_collision_objects.append(node)
|
||||
|
||||
func node_removed(node: Node) -> void:
|
||||
if node is PhysicsBody2D:
|
||||
scene_collision_objects.erase(node)
|
||||
@@ -0,0 +1 @@
|
||||
uid://c8p8gymii2y5v
|
||||
@@ -0,0 +1,71 @@
|
||||
extends PhysicsDriver
|
||||
class_name PhysicsDriver3D
|
||||
|
||||
# Physics driver based on netfox ticks
|
||||
# Requires a custom build of Godot with https://github.com/godotengine/godot/pull/76462
|
||||
|
||||
# Maps ticks ( int ) to global snapshots ( Dictionary<RID, Array> )
|
||||
var scene_collision_objects: Array = []
|
||||
|
||||
func _init_physics_space() -> void:
|
||||
physics_space = get_viewport().world_3d.space
|
||||
PhysicsServer3D.space_set_active(physics_space, false)
|
||||
|
||||
get_tree().node_added.connect(node_added)
|
||||
get_tree().node_removed.connect(node_removed)
|
||||
scan_tree()
|
||||
|
||||
func _physics_step(delta) -> void:
|
||||
PhysicsServer3D.space_flush_queries(physics_space)
|
||||
PhysicsServer3D.space_step(physics_space, delta)
|
||||
|
||||
func _snapshot_space(tick: int) -> void:
|
||||
# Maps RIDs to physics state ( Array )
|
||||
var rid_states := {}
|
||||
for element in scene_collision_objects:
|
||||
var rid = element.get_rid()
|
||||
rid_states[rid] = get_body_states(rid)
|
||||
|
||||
snapshots[tick] = rid_states
|
||||
|
||||
func _rollback_space(tick) -> void:
|
||||
if snapshots.has(tick):
|
||||
var rid_states = snapshots[tick]
|
||||
for rid in rid_states.keys():
|
||||
set_body_states(rid, rid_states[rid])
|
||||
|
||||
for body in scene_collision_objects:
|
||||
if body is CharacterBody3D or body is AnimatableBody3D:
|
||||
body.force_update_transform()
|
||||
|
||||
|
||||
func get_body_states(rid: RID) -> Array:
|
||||
var body_state: Array = [Vector3.ZERO, Quaternion.IDENTITY, Vector3.ZERO, Vector3.ZERO]
|
||||
body_state[0] = PhysicsServer3D.body_get_state(rid, PhysicsServer3D.BODY_STATE_TRANSFORM)
|
||||
body_state[1] = PhysicsServer3D.body_get_state(rid, PhysicsServer3D.BODY_STATE_LINEAR_VELOCITY)
|
||||
body_state[2] = PhysicsServer3D.body_get_state(rid, PhysicsServer3D.BODY_STATE_ANGULAR_VELOCITY)
|
||||
body_state[3] = PhysicsServer3D.body_get_state(rid, PhysicsServer3D.BODY_STATE_SLEEPING)
|
||||
return body_state
|
||||
|
||||
func set_body_states(rid: RID, body_state: Array) -> void:
|
||||
PhysicsServer3D.body_set_state(rid, PhysicsServer3D.BODY_STATE_TRANSFORM, body_state[0])
|
||||
PhysicsServer3D.body_set_state(rid, PhysicsServer3D.BODY_STATE_LINEAR_VELOCITY, body_state[1])
|
||||
PhysicsServer3D.body_set_state(rid, PhysicsServer3D.BODY_STATE_ANGULAR_VELOCITY, body_state[2])
|
||||
PhysicsServer3D.body_set_state(rid, PhysicsServer3D.BODY_STATE_SLEEPING, body_state[3])
|
||||
|
||||
func scan_tree():
|
||||
scene_collision_objects.clear()
|
||||
scene_collision_objects = get_all_children(get_node('/root'))
|
||||
|
||||
func get_all_children(in_node: Node) -> Array:
|
||||
var nodes = []
|
||||
nodes = in_node.find_children("*", "PhysicsBody3D", true, false)
|
||||
return nodes
|
||||
|
||||
func node_added(node: Node) -> void:
|
||||
if node is PhysicsBody3D:
|
||||
scene_collision_objects.append(node)
|
||||
|
||||
func node_removed(node: Node) -> void:
|
||||
if node is PhysicsBody3D:
|
||||
scene_collision_objects.erase(node)
|
||||
@@ -0,0 +1 @@
|
||||
uid://wr5ur5dqrkni
|
||||
@@ -0,0 +1,44 @@
|
||||
@icon("res://addons/netfox.extras/icons/network-rigid-body-2d.svg")
|
||||
extends RigidBody2D
|
||||
class_name NetworkRigidBody2D
|
||||
|
||||
## A rollback / state synchronizer class for RigidBody2D.
|
||||
## Set state property path to physics_state to synchronize the state of this body.
|
||||
|
||||
@onready var direct_state = PhysicsServer2D.body_get_direct_state(get_rid())
|
||||
|
||||
var physics_state: Array:
|
||||
get: return get_state()
|
||||
set(v): set_state(v)
|
||||
|
||||
enum {
|
||||
ORIGIN,
|
||||
ROT,
|
||||
LIN_VEL,
|
||||
ANG_VEL,
|
||||
SLEEPING
|
||||
}
|
||||
|
||||
func _notification(notification: int):
|
||||
if notification == NOTIFICATION_READY:
|
||||
add_to_group("network_rigid_body")
|
||||
|
||||
func get_state() -> Array:
|
||||
var body_state: Array = [Vector3.ZERO, Quaternion.IDENTITY, Vector3.ZERO, Vector3.ZERO, false]
|
||||
body_state[ORIGIN] = direct_state.transform.origin
|
||||
body_state[ROT] = direct_state.transform.get_rotation()
|
||||
body_state[LIN_VEL] = direct_state.linear_velocity
|
||||
body_state[ANG_VEL] = direct_state.angular_velocity
|
||||
body_state[SLEEPING] = direct_state.sleeping
|
||||
return body_state
|
||||
|
||||
func set_state(remote_state: Array) -> void:
|
||||
direct_state.transform = Transform2D(remote_state[ROT], remote_state[ORIGIN])
|
||||
direct_state.linear_velocity = remote_state[LIN_VEL]
|
||||
direct_state.angular_velocity = remote_state[ANG_VEL]
|
||||
direct_state.sleeping = remote_state[SLEEPING]
|
||||
|
||||
## Override and apply any logic, forces or impulses to the rigid body as you would in physics_process
|
||||
## The physics engine will run its simulation during rollback_tick with other nodes
|
||||
func _physics_rollback_tick(_delta, _tick):
|
||||
pass
|
||||
@@ -0,0 +1 @@
|
||||
uid://c8hw7ol53m55g
|
||||
@@ -0,0 +1,46 @@
|
||||
@icon("res://addons/netfox.extras/physics/network-rigid-body-3d.gd")
|
||||
extends RigidBody3D
|
||||
class_name NetworkRigidBody3D
|
||||
|
||||
## A rollback / state synchronizer class for RigidBody3D.
|
||||
## Set state property path to physics_state to synchronize the state of this body.
|
||||
|
||||
@onready var direct_state = PhysicsServer3D.body_get_direct_state(get_rid())
|
||||
|
||||
var physics_state: Array:
|
||||
get: return get_state()
|
||||
set(v): set_state(v)
|
||||
|
||||
enum {
|
||||
ORIGIN,
|
||||
QUAT,
|
||||
LIN_VEL,
|
||||
ANG_VEL,
|
||||
SLEEPING
|
||||
}
|
||||
|
||||
func _notification(notification: int):
|
||||
if notification == NOTIFICATION_READY:
|
||||
add_to_group("network_rigid_body")
|
||||
|
||||
func get_state() -> Array:
|
||||
var body_state: Array = [Vector3.ZERO, Quaternion.IDENTITY, Vector3.ZERO, Vector3.ZERO, false]
|
||||
body_state[ORIGIN] = direct_state.transform.origin
|
||||
body_state[QUAT] = direct_state.transform.basis.get_rotation_quaternion()
|
||||
body_state[LIN_VEL] = direct_state.linear_velocity
|
||||
body_state[ANG_VEL] = direct_state.angular_velocity
|
||||
body_state[SLEEPING] = direct_state.sleeping
|
||||
return body_state
|
||||
|
||||
func set_state(remote_state: Array) -> void:
|
||||
direct_state.transform.origin = remote_state[ORIGIN]
|
||||
direct_state.transform.basis = Basis(remote_state[QUAT])
|
||||
direct_state.linear_velocity = remote_state[LIN_VEL]
|
||||
direct_state.angular_velocity = remote_state[ANG_VEL]
|
||||
direct_state.sleeping = remote_state[SLEEPING]
|
||||
|
||||
|
||||
## Override and apply any logic, forces or impulses to the rigid body as you would in physics_process
|
||||
## The physics engine will run its simulation during rollback_tick with other nodes
|
||||
func _physics_rollback_tick(_delta, _tick):
|
||||
pass
|
||||
@@ -0,0 +1 @@
|
||||
uid://bklxcdyxgbjg2
|
||||
@@ -0,0 +1,88 @@
|
||||
extends Object
|
||||
|
||||
class PhysicsDriverToggle:
|
||||
const INACTIVE_SUFFIX := ".off"
|
||||
|
||||
func get_name() -> String:
|
||||
return "???"
|
||||
|
||||
func get_files() -> Array[String]:
|
||||
return []
|
||||
|
||||
func get_error_messages() -> Array[String]:
|
||||
return []
|
||||
|
||||
func is_enabled() -> bool:
|
||||
return get_files().any(func(it): return FileAccess.file_exists(it))
|
||||
|
||||
func toggle() -> Array[String]:
|
||||
var errors := get_error_messages()
|
||||
if not errors.is_empty():
|
||||
return errors
|
||||
|
||||
var enable := not is_enabled()
|
||||
|
||||
var uid_files := get_files().map(func(it): return it + ".uid")
|
||||
var renames = (get_files() + uid_files).map(func(it):
|
||||
if enable: return [it + INACTIVE_SUFFIX, it]
|
||||
else: return [it, it + INACTIVE_SUFFIX]
|
||||
)
|
||||
|
||||
for rename in renames:
|
||||
var result := DirAccess.rename_absolute(rename[0], rename[1])
|
||||
if result != OK:
|
||||
errors.append(
|
||||
"Failed rename \"%s\" -> \"%s\"; reason: %s" %
|
||||
[rename[0], rename[1], error_string(result)]
|
||||
)
|
||||
return errors
|
||||
|
||||
class Rapier2DPhysicsDriverToggle extends PhysicsDriverToggle:
|
||||
func get_name() -> String:
|
||||
return "Rapier2D"
|
||||
|
||||
func get_files() -> Array[String]:
|
||||
return [
|
||||
"res://addons/netfox.extras/physics/rapier_driver_2d.gd",
|
||||
]
|
||||
|
||||
func get_error_messages() -> Array[String]:
|
||||
if not ClassDB.class_exists("RapierPhysicsServer2D"):
|
||||
return ["Rapier physics is not available! Is the extension installed?"]
|
||||
return []
|
||||
|
||||
class Rapier3DPhysicsDriverToggle extends PhysicsDriverToggle:
|
||||
func get_name() -> String:
|
||||
return "Rapier3D"
|
||||
|
||||
func get_files() -> Array[String]:
|
||||
return [
|
||||
"res://addons/netfox.extras/physics/rapier_driver_3d.gd",
|
||||
]
|
||||
|
||||
func get_error_messages() -> Array[String]:
|
||||
if not ClassDB.class_exists("RapierPhysicsServer3D"):
|
||||
return ["Rapier physics is not available! Is the extension installed?"]
|
||||
return []
|
||||
|
||||
class GodotPhysicsDriverToggle extends PhysicsDriverToggle:
|
||||
func get_name() -> String:
|
||||
return "Godot"
|
||||
|
||||
func get_files() -> Array[String]:
|
||||
return [
|
||||
"res://addons/netfox.extras/physics/godot_driver_3d.gd",
|
||||
"res://addons/netfox.extras/physics/godot_driver_2d.gd"
|
||||
]
|
||||
|
||||
func get_error_messages() -> Array[String]:
|
||||
if not PhysicsServer3D.has_method("space_step") or not PhysicsServer2D.has_method("space_step"):
|
||||
return ["Physics stepping is not available! Is this the right Godot build?"]
|
||||
return []
|
||||
|
||||
static func all() -> Array[PhysicsDriverToggle]:
|
||||
return [
|
||||
Rapier2DPhysicsDriverToggle.new(),
|
||||
Rapier3DPhysicsDriverToggle.new(),
|
||||
GodotPhysicsDriverToggle.new()
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
uid://bu4ppfj0ovkbr
|
||||
@@ -0,0 +1,86 @@
|
||||
extends Node
|
||||
|
||||
class_name PhysicsDriver
|
||||
|
||||
# Physics driver based on netfox ticks
|
||||
# Step physics in time with netfox and participates in rollback
|
||||
|
||||
var physics_space: RID
|
||||
var snapshots: Dictionary = {}
|
||||
|
||||
# Number of physics steps to take per network tick
|
||||
@export var physics_factor: int = 2
|
||||
# Snapshot and Rollback entire physics space.
|
||||
@export var rollback_physics_space: bool = true
|
||||
|
||||
func _enter_tree():
|
||||
#regular ticks
|
||||
NetworkTime.before_tick.connect(before_tick)
|
||||
NetworkTime.after_tick_loop.connect(after_tick_loop)
|
||||
|
||||
#rollback ticks
|
||||
if rollback_physics_space:
|
||||
NetworkRollback.on_prepare_tick.connect(on_prepare_tick)
|
||||
NetworkRollback.on_process_tick.connect(on_process_tick)
|
||||
|
||||
func _exit_tree():
|
||||
NetworkTime.before_tick.disconnect(before_tick)
|
||||
NetworkTime.after_tick_loop.disconnect(after_tick_loop)
|
||||
|
||||
#rollback ticks
|
||||
if NetworkRollback.on_prepare_tick.is_connected(on_prepare_tick):
|
||||
NetworkRollback.on_prepare_tick.disconnect(on_prepare_tick)
|
||||
NetworkRollback.on_process_tick.disconnect(on_process_tick)
|
||||
|
||||
func _ready() -> void:
|
||||
_init_physics_space()
|
||||
|
||||
# Emitted before a tick is run.
|
||||
func before_tick(_delta: float, tick: int) -> void:
|
||||
_snapshot_space(tick)
|
||||
step_physics(_delta)
|
||||
|
||||
func on_prepare_tick(tick: int) -> void:
|
||||
if NetworkRollback._rollback_from == tick:
|
||||
# First tick of rollback loop, rewind
|
||||
_rollback_space(tick)
|
||||
else:
|
||||
# Subsequent ticks are re-writing history.
|
||||
_snapshot_space(tick)
|
||||
|
||||
func on_process_tick(_tick: int) -> void:
|
||||
step_physics(NetworkTime.ticktime)
|
||||
|
||||
func after_tick_loop() -> void:
|
||||
# Remove old snapshots
|
||||
for i in snapshots.keys():
|
||||
if i < NetworkRollback.history_start:
|
||||
snapshots.erase(i)
|
||||
|
||||
func step_physics(_delta: float) -> void:
|
||||
# Break up physics into smaller steps if needed
|
||||
var frac_delta = _delta / physics_factor
|
||||
var rollback_participants = get_tree().get_nodes_in_group("network_rigid_body")
|
||||
for i in range(physics_factor):
|
||||
for net_rigid_body in rollback_participants:
|
||||
net_rigid_body._physics_rollback_tick(frac_delta, NetworkTime.tick)
|
||||
|
||||
_physics_step(frac_delta)
|
||||
|
||||
## Override this method to initialize the physics space.
|
||||
func _init_physics_space() -> void:
|
||||
pass
|
||||
|
||||
## Override this method to take one step in the physics space.
|
||||
## [br][br]
|
||||
## It should also flush and update all Godot nodes
|
||||
func _physics_step(_delta) -> void:
|
||||
pass
|
||||
|
||||
## Override this method to record the current state of the physics space.
|
||||
func _snapshot_space(_tick: int) -> void:
|
||||
pass
|
||||
|
||||
## Override this method to restore the physics space to a previous state.
|
||||
func _rollback_space(_tick) -> void:
|
||||
pass
|
||||
@@ -0,0 +1 @@
|
||||
uid://dcws6qk4hxtun
|
||||
@@ -0,0 +1,18 @@
|
||||
extends PhysicsDriver
|
||||
|
||||
class_name RapierDriver2D
|
||||
|
||||
func _init_physics_space() -> void:
|
||||
physics_space = get_viewport().world_2d.space
|
||||
PhysicsServer2D.space_set_active(physics_space, false)
|
||||
|
||||
func _physics_step(delta) -> void:
|
||||
RapierPhysicsServer2D.space_step(physics_space, delta)
|
||||
RapierPhysicsServer2D.space_flush_queries(physics_space)
|
||||
|
||||
func _snapshot_space(tick: int) -> void:
|
||||
snapshots[tick] = RapierPhysicsServer2D.export_binary(physics_space)
|
||||
|
||||
func _rollback_space(tick) -> void:
|
||||
if snapshots.has(tick):
|
||||
RapierPhysicsServer2D.import_binary(physics_space, snapshots[tick])
|
||||
@@ -0,0 +1 @@
|
||||
uid://bcc28dvk0pufe
|
||||
@@ -0,0 +1,18 @@
|
||||
extends PhysicsDriver
|
||||
|
||||
class_name RapierDriver3D
|
||||
|
||||
func _init_physics_space() -> void:
|
||||
physics_space = get_viewport().world_3d.space
|
||||
PhysicsServer3D.space_set_active(physics_space, false)
|
||||
|
||||
func _physics_step(delta) -> void:
|
||||
RapierPhysicsServer3D.space_step(physics_space, delta)
|
||||
RapierPhysicsServer3D.space_flush_queries(physics_space)
|
||||
|
||||
func _snapshot_space(tick: int) -> void:
|
||||
snapshots[tick] = RapierPhysicsServer3D.export_binary(physics_space)
|
||||
|
||||
func _rollback_space(tick) -> void:
|
||||
if snapshots.has(tick):
|
||||
RapierPhysicsServer3D.import_binary(physics_space, snapshots[tick])
|
||||
@@ -0,0 +1 @@
|
||||
uid://bo4lplbeepibj
|
||||
@@ -0,0 +1,7 @@
|
||||
[plugin]
|
||||
|
||||
name="netfox.extras"
|
||||
description="Game-specific utilities for Netfox"
|
||||
author="Tamas Galffy and contributors"
|
||||
version="1.35.3"
|
||||
script="netfox-extras.gd"
|
||||
@@ -0,0 +1,71 @@
|
||||
extends RefCounted
|
||||
class_name RewindableRandomNumberGenerator
|
||||
|
||||
## Provides methods for generating pseudo-random numbers in the rollback tick
|
||||
## loop.
|
||||
##
|
||||
## Using a regular [RandomNumberGenerator] in [code]_rollback_tick()[/code]
|
||||
## would generate different numbers on each peer. It also generates different
|
||||
## numbers when resimulating the same tick.
|
||||
## [br][br]
|
||||
## This class solves all of the above, making it suitable for use during
|
||||
## rollback.
|
||||
## [br][br]
|
||||
## The seed must be provided on instantiation, and must be the same on all peers
|
||||
## for the random number generator to work properly.
|
||||
##
|
||||
## @tutorial(RewindableRandomNumberGenerator Guide): https://foxssake.github.io/netfox/latest/netfox.extras/guides/rewindable-random-number-generator/
|
||||
|
||||
var _rng: RandomNumberGenerator
|
||||
var _last_reset_tick := -1
|
||||
var _last_reset_rollback_tick := -1
|
||||
|
||||
static var _logger := NetfoxLogger._for_extras("RewindableRandomNumberGenerator")
|
||||
|
||||
func _init(p_seed: int):
|
||||
_rng = RandomNumberGenerator.new()
|
||||
_rng.set_seed(p_seed)
|
||||
|
||||
## Returns a pseudo-random float between [code]0.0[/code] and [code]1.0[/code]
|
||||
## (inclusive).
|
||||
func randf() -> float:
|
||||
_ensure_state()
|
||||
return _rng.randf()
|
||||
|
||||
## Returns a pseudo-random float between [code]from[/code] and [code]to[/code]
|
||||
## (inclusive).
|
||||
func randf_range(from: float, to: float) -> float:
|
||||
_ensure_state()
|
||||
return _rng.randf_range(from, to)
|
||||
|
||||
## Returns a normally-distributed, pseudo-random floating-point number from the
|
||||
## specified [code]mean[/code] and a standard [code]deviation[/code]. This is
|
||||
## also known as a Gaussian distribution.
|
||||
func randfn(mean: float = 0.0, deviation: float = 1.0) -> float:
|
||||
_ensure_state()
|
||||
return _rng.randfn(mean, deviation)
|
||||
|
||||
## Returns a pseudo-random 32-bit unsigned integer between [code]0[/code] and
|
||||
## [code]4294967295[/code] (inclusive).
|
||||
func randi() -> int:
|
||||
_ensure_state()
|
||||
return _rng.randi()
|
||||
|
||||
## Returns a pseudo-random 32-bit unsigned integer between [code]from[/code] and
|
||||
## [code]to[/code] (inclusive).
|
||||
func randi_range(from: int, to: int) -> int:
|
||||
_ensure_state()
|
||||
return _rng.randi_range(from, to)
|
||||
|
||||
func _ensure_state() -> void:
|
||||
if NetworkTime.tick == _last_reset_tick and NetworkRollback.tick == _last_reset_rollback_tick:
|
||||
# State already has been set
|
||||
return
|
||||
|
||||
if NetworkRollback.is_rollback():
|
||||
_rng.state = hash([_rng.seed, NetworkRollback.tick])
|
||||
else:
|
||||
_rng.state = hash([_rng.seed, NetworkTime.tick])
|
||||
|
||||
_last_reset_rollback_tick = NetworkRollback.tick
|
||||
_last_reset_tick = NetworkTime.tick
|
||||
@@ -0,0 +1 @@
|
||||
uid://cqj8glnpkt5po
|
||||
@@ -0,0 +1,187 @@
|
||||
@tool
|
||||
@icon("res://addons/netfox.extras/icons/rewindable-state-machine.svg")
|
||||
extends Node
|
||||
class_name RewindableStateMachine
|
||||
|
||||
## A state machine that can be used with rollback.
|
||||
##
|
||||
## It relies on [RollbackSynchronizer] to manage its state. State transitions
|
||||
## are only triggered by gameplay code, and not by rollback reverting to an
|
||||
## earlier state.
|
||||
## [br][br]
|
||||
## For this node to work correctly, a [RollbackSynchronizer] must be added as
|
||||
## a sibling, and it must have the [RewindableStateMachine]'s [member state]
|
||||
## property configured as a state property.
|
||||
## [br][br]
|
||||
## To implement states, extend the [RewindableState] class and add it as a child
|
||||
## node.
|
||||
##
|
||||
## @tutorial(RewindableStateMachine Guide): https://foxssake.github.io/netfox/latest/netfox.extras/guides/rewindable-state-machine/
|
||||
|
||||
## Name of the current state.
|
||||
##
|
||||
## Can be an empty string if no state is active. Only modify directly if you
|
||||
## need to skip [method transition]'s callbacks.
|
||||
@export var state: StringName = "":
|
||||
get: return _state_object.name if _state_object != null else ""
|
||||
set(v): _set_state(v)
|
||||
|
||||
## Emitted during state transitions.
|
||||
##
|
||||
## This signal can be used to run gameplay code on state changes.
|
||||
## [br][br]
|
||||
## This signal is emitted whenever a transition happens during rollback, which
|
||||
## means it may be emitted multiple times for the same transition if it gets
|
||||
## resimulated during rollback.
|
||||
## [br][br]
|
||||
## [b]State changes are not necessarily emitted on all peers.[/b]
|
||||
## See: [url=https://foxssake.github.io/netfox/netfox.extras/guides/rewindable-state-machine/#caveats]RewindableStateMachine caveats[/url]
|
||||
signal on_state_changed(old_state: RewindableState, new_state: RewindableState)
|
||||
|
||||
## Emitted after the displayed state has changed.
|
||||
##
|
||||
## This signal can be used to update visuals based on state changes.
|
||||
## [br][br]
|
||||
## This signal is emitted whenever the state after a tick loop has changed.
|
||||
signal on_display_state_changed(old_state: RewindableState, new_state: RewindableState)
|
||||
|
||||
static var _logger: NetfoxLogger = NetfoxLogger._for_extras("RewindableStateMachine")
|
||||
|
||||
var _state_object: RewindableState = null
|
||||
var _previous_state_object: RewindableState = null
|
||||
var _available_states: Dictionary = {}
|
||||
var _prevent_transition: bool = false
|
||||
var _prevent_callable: Callable = func(): _prevent_transition = true
|
||||
|
||||
## Transition to a new state specified by [param new_state_name] and return
|
||||
## true.
|
||||
##
|
||||
## Finds the given state by name and transitions to it if possible. The new
|
||||
## state's [method RewindableState.can_enter] callback decides if it can be
|
||||
## entered from the current state.
|
||||
## [br][br]
|
||||
## Upon transitioning, [method RewindableState.exit] is called on the old state,
|
||||
## and [method RewindableState.enter] is called on the new state. In addition,
|
||||
## [signal on_state_changed] is emitted.
|
||||
## [br][br]
|
||||
## Does nothing if transitioning to the currently active state. Emits a warning
|
||||
## and does nothing when transitioning to an unknown state.
|
||||
func transition(new_state_name: StringName) -> bool:
|
||||
# Check if target state is valid
|
||||
if state == new_state_name:
|
||||
return false
|
||||
|
||||
if not _available_states.has(new_state_name):
|
||||
_logger.warning("Attempted to transition from state '%s' into unknown state '%s'", [state, new_state_name])
|
||||
return false
|
||||
|
||||
var from_state = _state_object
|
||||
var new_state: RewindableState = _available_states[new_state_name]
|
||||
_prevent_transition = false
|
||||
|
||||
# Validate transition
|
||||
if from_state:
|
||||
if !new_state.can_enter(_state_object):
|
||||
return false
|
||||
|
||||
# Emit exit signal, allow handlers to prevent transition
|
||||
_state_object.on_exit.emit(new_state, NetworkRollback.tick, _prevent_callable)
|
||||
if _prevent_transition: return false
|
||||
|
||||
new_state.on_enter.emit(from_state, NetworkRollback.tick, _prevent_callable)
|
||||
if _prevent_transition: return false
|
||||
|
||||
# Transition valid, run callbacks
|
||||
if is_instance_valid(from_state):
|
||||
from_state.exit(new_state, NetworkRollback.tick)
|
||||
new_state.enter(from_state, NetworkRollback.tick)
|
||||
|
||||
# Set new state
|
||||
_state_object = new_state
|
||||
on_state_changed.emit(from_state, new_state)
|
||||
|
||||
return true
|
||||
|
||||
## Update the internal cache of known states
|
||||
## [br][br]
|
||||
## Automatically called on ready and when a child node is added or removed. Call
|
||||
## manually to force an update.
|
||||
func update_states() -> void:
|
||||
_available_states.clear()
|
||||
|
||||
for child in find_children("*", "RewindableState", false):
|
||||
_available_states[child.name] = child
|
||||
|
||||
func _notification(what: int):
|
||||
# Use notification instead of _ready, so users can write their own _ready
|
||||
# callback without having to call super()
|
||||
if Engine.is_editor_hint(): return
|
||||
|
||||
match what:
|
||||
NOTIFICATION_CHILD_ORDER_CHANGED:
|
||||
update_states()
|
||||
NOTIFICATION_ENTER_TREE:
|
||||
# Compare states after tick loop
|
||||
NetworkTime.after_tick_loop.connect(_after_tick_loop)
|
||||
update_states()
|
||||
NOTIFICATION_EXIT_TREE:
|
||||
# Disconnect handlers
|
||||
NetworkTime.after_tick_loop.disconnect(_after_tick_loop)
|
||||
|
||||
func _get_configuration_warnings():
|
||||
const MISSING_SYNCHRONIZER_ERROR := \
|
||||
"RewindableStateMachine is not managed by a RollbackSynchronizer! Add it as a sibling node to fix this."
|
||||
const INVALID_SYNCHRONIZER_CONFIG_ERROR := \
|
||||
"RollbackSynchronizer configuration is invalid, it can't manage this state machine!" +\
|
||||
"\nNote: You may need to reload this scene after fixing for this warning to disappear."
|
||||
const MISSING_PROPERTY_ERROR := \
|
||||
"State is not managed by RollbackSynchronizer! Add `state` property to the synchronizer to fix this. " +\
|
||||
"\nNote: You may need to reload this scene after fixing for this warning to disappear."
|
||||
|
||||
# Check if there's a rollback synchronizer
|
||||
var rollback_synchronizer_node = get_parent().find_children("*", "RollbackSynchronizer", false).pop_front()
|
||||
if not rollback_synchronizer_node:
|
||||
return [MISSING_SYNCHRONIZER_ERROR]
|
||||
|
||||
var rollback_synchronizer := rollback_synchronizer_node as RollbackSynchronizer
|
||||
|
||||
# Check if its configuration is valid
|
||||
# TODO: Expose this as a property?
|
||||
if not rollback_synchronizer.root:
|
||||
return [INVALID_SYNCHRONIZER_CONFIG_ERROR]
|
||||
|
||||
# Check if it manages our `state` property
|
||||
for state_property_path in rollback_synchronizer.state_properties:
|
||||
var property = PropertyEntry.parse(rollback_synchronizer.root, state_property_path)
|
||||
if property.node == self and property.property == "state":
|
||||
return []
|
||||
|
||||
return [MISSING_PROPERTY_ERROR]
|
||||
|
||||
func _rollback_tick(delta: float, tick: int, is_fresh: bool) -> void:
|
||||
if _state_object:
|
||||
_state_object.tick(delta, tick, is_fresh)
|
||||
_state_object.on_tick.emit(delta, tick, is_fresh)
|
||||
|
||||
func _after_tick_loop():
|
||||
if _state_object != _previous_state_object:
|
||||
on_display_state_changed.emit(_previous_state_object, _state_object)
|
||||
|
||||
if _previous_state_object:
|
||||
_previous_state_object.on_display_exit.emit(_state_object, NetworkTime.tick)
|
||||
_previous_state_object.display_exit(_state_object, NetworkTime.tick)
|
||||
|
||||
_state_object.on_display_enter.emit(_previous_state_object, NetworkTime.tick)
|
||||
_state_object.display_enter(_previous_state_object, NetworkTime.tick)
|
||||
|
||||
_previous_state_object = _state_object
|
||||
|
||||
func _set_state(new_state: StringName) -> void:
|
||||
if not new_state:
|
||||
return
|
||||
|
||||
if not _available_states.has(new_state):
|
||||
_logger.warning("Attempted to jump to unknown state: %s", [new_state])
|
||||
return
|
||||
|
||||
_state_object = _available_states[new_state]
|
||||
@@ -0,0 +1 @@
|
||||
uid://byrgwv2o7hstx
|
||||
@@ -0,0 +1,114 @@
|
||||
@tool
|
||||
@icon("res://addons/netfox.extras/icons/rewindable-state.svg")
|
||||
extends Node
|
||||
class_name RewindableState
|
||||
|
||||
## Base class for states to be used with [RewindableStateMachine].
|
||||
##
|
||||
## Provides multiple callback methods for a state's lifecycle, which can be
|
||||
## overridden by extending classes.
|
||||
## [br][br]
|
||||
## Must have a [RewindableStateMachine] as a parent.
|
||||
##
|
||||
## @tutorial(RewindableStateMachine Guide): https://foxssake.github.io/netfox/latest/netfox.extras/guides/rewindable-state-machine/
|
||||
|
||||
## Emitted when entering the state
|
||||
signal on_enter(previous_state: RewindableState, tick: int, prevent: Callable)
|
||||
|
||||
## Emitted on every rollback tick while the state is active
|
||||
signal on_tick(delta: float, tick: int, is_fresh: bool)
|
||||
|
||||
## Emitted when exiting the state
|
||||
signal on_exit(next_state: RewindableState, tick: int, prevent: Callable)
|
||||
|
||||
## Emitted before displaying this state
|
||||
signal on_display_enter(previous_state: RewindableState, tick: int)
|
||||
|
||||
## Emitted before displaying another state
|
||||
signal on_display_exit(next_state: RewindableState, tick: int)
|
||||
|
||||
## The [RewindableStateMachine] this state belongs to.
|
||||
## [br][br]
|
||||
## [i]read-only[/i]
|
||||
var state_machine: RewindableStateMachine:
|
||||
get: return _state_machine
|
||||
|
||||
var _state_machine: RewindableStateMachine
|
||||
|
||||
## Callback to run a single tick.
|
||||
##
|
||||
## This method is called by the [RewindableStateMachine] during the rollback
|
||||
## tick loop to update game state.
|
||||
## [br][br]
|
||||
## [i]override[/i] to implement game logic
|
||||
func tick(delta: float, tick: int, is_fresh: bool) -> void:
|
||||
pass
|
||||
|
||||
## Callback for entering the state.
|
||||
##
|
||||
## This method is called whenever the state machine enters this state.
|
||||
## [br][br]
|
||||
## It is best practice to only modify game state here, i.e. properties that are
|
||||
## configured as state in a [RollbackSynchronizer].
|
||||
## [br][br]
|
||||
## [i]override[/i] to implement game logic reacting to state transitions
|
||||
func enter(previous_state: RewindableState, tick: int) -> void:
|
||||
pass
|
||||
|
||||
## Callback for exiting the state.
|
||||
##
|
||||
## This method is called whenever the state machine exits this state.
|
||||
## [br][br]
|
||||
## It is best practice to only modify game state here, i.e. properties that are
|
||||
## configured as state in a [RollbackSynchronizer].
|
||||
## [br][br]
|
||||
## [i]override[/i] to implement game logic reacting to state transitions
|
||||
func exit(next_state: RewindableState, tick: int) -> void:
|
||||
pass
|
||||
|
||||
## Callback for validating state transitions.
|
||||
##
|
||||
## Whenever the [RewindableStateMachine] attempts to enter this state, it will
|
||||
## call this method to ensure that the transition is valid.
|
||||
## [br][br]
|
||||
## If this method returns true, the transition is valid and the state machine
|
||||
## will enter this state. Otherwise, the transition is invalid, and nothing
|
||||
## happens.
|
||||
## [br][br]
|
||||
## [i]override[/i] to implement custom transition validation logic
|
||||
func can_enter(previous_state: RewindableState) -> bool:
|
||||
# Add your validation logic here
|
||||
# Return true if the state machine can transition to the next state
|
||||
return true
|
||||
|
||||
## Callback for displaying the state.
|
||||
##
|
||||
## After each tick loop, the [RewindableStateMachine] checks the final state,
|
||||
## i.e. the state that will be active until the next tick loop. If that state
|
||||
## has changed [b]to[/b] this one, the [RewindableStateMachine] will call this
|
||||
## method.
|
||||
## [br][br]
|
||||
## [i]override[/i] to implement visuals / effects reacting to state transitions
|
||||
func display_enter(previous_state: RewindableState, tick: int) -> void:
|
||||
pass
|
||||
|
||||
## Callback for displaying a different state.
|
||||
##
|
||||
## After each tick loop, the [RewindableStateMachine] checks the final state,
|
||||
## i.e. the state that will be active until the next tick loop. If that state
|
||||
## has changed [b]from[/b] this one, the [RewindableStateMachine] will call this
|
||||
## method.
|
||||
## [br][br]
|
||||
## [i]override[/i] to implement visuals / effects reacting to state transitions
|
||||
func display_exit(next_state: RewindableState, tick: int) -> void:
|
||||
pass
|
||||
|
||||
func _get_configuration_warnings():
|
||||
return [] if get_parent() is RewindableStateMachine else ["This state should be a child of a RewindableStateMachine."]
|
||||
|
||||
func _notification(what: int):
|
||||
# Use notification instead of _ready, so users can write their own _ready
|
||||
# callback without having to call super()
|
||||
if what == NOTIFICATION_READY:
|
||||
if _state_machine == null and get_parent() is RewindableStateMachine:
|
||||
_state_machine = get_parent()
|
||||
@@ -0,0 +1 @@
|
||||
uid://usyufdtn83hc
|
||||
@@ -0,0 +1,75 @@
|
||||
extends Node2D
|
||||
class_name NetworkWeapon2D
|
||||
|
||||
## A 2D-specific implementation of [NetworkWeapon].
|
||||
|
||||
## Distance to consider too large during reconciliation checks.
|
||||
@export var distance_threshold: float = 1.0
|
||||
|
||||
var _weapon: _NetworkWeaponProxy
|
||||
|
||||
func can_fire() -> bool:
|
||||
return _weapon.can_fire()
|
||||
|
||||
func fire() -> Node2D:
|
||||
return _weapon.fire()
|
||||
|
||||
func get_fired_tick() -> int:
|
||||
return _weapon.get_fired_tick()
|
||||
|
||||
func _init():
|
||||
_weapon = _NetworkWeaponProxy.new()
|
||||
add_child(_weapon, true, INTERNAL_MODE_BACK)
|
||||
_weapon.owner = self
|
||||
|
||||
_weapon.c_can_fire = _can_fire
|
||||
_weapon.c_can_peer_use = _can_peer_use
|
||||
_weapon.c_after_fire = _after_fire
|
||||
_weapon.c_spawn = _spawn
|
||||
_weapon.c_get_data = _get_data
|
||||
_weapon.c_apply_data = _apply_data
|
||||
_weapon.c_is_reconcilable = _is_reconcilable
|
||||
_weapon.c_reconcile = _reconcile
|
||||
|
||||
|
||||
## See [NetworkWeapon]
|
||||
func _can_fire() -> bool:
|
||||
return false
|
||||
|
||||
## See [NetworkWeapon]
|
||||
func _can_peer_use(peer_id: int) -> bool:
|
||||
return true
|
||||
|
||||
## See [NetworkWeapon]
|
||||
func _after_fire(projectile: Node2D):
|
||||
pass
|
||||
|
||||
## See [NetworkWeapon]
|
||||
func _spawn() -> Node2D:
|
||||
return null
|
||||
|
||||
func _get_data(projectile: Node2D) -> Dictionary:
|
||||
return {
|
||||
"global_transform": projectile.global_transform
|
||||
}
|
||||
|
||||
func _apply_data(projectile: Node2D, data: Dictionary):
|
||||
projectile.global_transform = data["global_transform"]
|
||||
|
||||
func _is_reconcilable(projectile: Node2D, request_data: Dictionary, local_data: Dictionary) -> bool:
|
||||
var req_transform = request_data["global_transform"] as Transform2D
|
||||
var loc_transform = local_data["global_transform"] as Transform2D
|
||||
|
||||
var request_pos = req_transform.origin
|
||||
var local_pos = loc_transform.origin
|
||||
|
||||
return request_pos.distance_to(local_pos) < distance_threshold
|
||||
|
||||
func _reconcile(projectile: Node2D, local_data: Dictionary, remote_data: Dictionary):
|
||||
var local_transform = local_data["global_transform"] as Transform2D
|
||||
var remote_transform = remote_data["global_transform"] as Transform2D
|
||||
|
||||
var relative_transform = projectile.global_transform * local_transform.inverse()
|
||||
var final_transform = remote_transform * relative_transform
|
||||
|
||||
projectile.global_transform = final_transform
|
||||
@@ -0,0 +1 @@
|
||||
uid://c8pw311ku24we
|
||||
@@ -0,0 +1,75 @@
|
||||
extends Node3D
|
||||
class_name NetworkWeapon3D
|
||||
|
||||
## A 3D-specific implementation of [NetworkWeapon].
|
||||
|
||||
## Distance to consider too large during reconciliation checks.
|
||||
@export var distance_threshold: float = 1.0
|
||||
|
||||
var _weapon: _NetworkWeaponProxy
|
||||
|
||||
func can_fire() -> bool:
|
||||
return _weapon.can_fire()
|
||||
|
||||
func fire() -> Node3D:
|
||||
return _weapon.fire()
|
||||
|
||||
func get_fired_tick() -> int:
|
||||
return _weapon.get_fired_tick()
|
||||
|
||||
func _init():
|
||||
_weapon = _NetworkWeaponProxy.new()
|
||||
add_child(_weapon, true, INTERNAL_MODE_BACK)
|
||||
_weapon.owner = self
|
||||
|
||||
_weapon.c_can_fire = _can_fire
|
||||
_weapon.c_can_peer_use = _can_peer_use
|
||||
_weapon.c_after_fire = _after_fire
|
||||
_weapon.c_spawn = _spawn
|
||||
_weapon.c_get_data = _get_data
|
||||
_weapon.c_apply_data = _apply_data
|
||||
_weapon.c_is_reconcilable = _is_reconcilable
|
||||
_weapon.c_reconcile = _reconcile
|
||||
|
||||
|
||||
## See [NetworkWeapon]
|
||||
func _can_fire() -> bool:
|
||||
return false
|
||||
|
||||
## See [NetworkWeapon]
|
||||
func _can_peer_use(peer_id: int) -> bool:
|
||||
return true
|
||||
|
||||
## See [NetworkWeapon]
|
||||
func _after_fire(projectile: Node3D):
|
||||
pass
|
||||
|
||||
## See [NetworkWeapon]
|
||||
func _spawn() -> Node3D:
|
||||
return null
|
||||
|
||||
func _get_data(projectile: Node3D) -> Dictionary:
|
||||
return {
|
||||
"global_transform": projectile.global_transform
|
||||
}
|
||||
|
||||
func _apply_data(projectile: Node3D, data: Dictionary):
|
||||
projectile.global_transform = data["global_transform"]
|
||||
|
||||
func _is_reconcilable(projectile: Node3D, request_data: Dictionary, local_data: Dictionary) -> bool:
|
||||
var req_transform = request_data["global_transform"] as Transform3D
|
||||
var loc_transform = local_data["global_transform"] as Transform3D
|
||||
|
||||
var request_pos = req_transform.origin
|
||||
var local_pos = loc_transform.origin
|
||||
|
||||
return request_pos.distance_to(local_pos) < distance_threshold
|
||||
|
||||
func _reconcile(projectile: Node3D, local_data: Dictionary, remote_data: Dictionary):
|
||||
var local_transform = local_data["global_transform"] as Transform3D
|
||||
var remote_transform = remote_data["global_transform"] as Transform3D
|
||||
|
||||
var relative_transform = projectile.global_transform * local_transform.inverse()
|
||||
var final_transform = remote_transform * relative_transform
|
||||
|
||||
projectile.global_transform = final_transform
|
||||
@@ -0,0 +1 @@
|
||||
uid://c1xyp6tx4hf3p
|
||||
@@ -0,0 +1,127 @@
|
||||
extends Node3D
|
||||
class_name NetworkWeaponHitscan3D
|
||||
|
||||
## A 3D-specific implementation of a networked hitscan (raycast) weapon.
|
||||
|
||||
## Maximum distance to cast the ray
|
||||
@export var max_distance: float = 1000.0
|
||||
|
||||
## Mask used to detect raycast hits
|
||||
@export_flags_3d_physics var collision_mask: int = 0xFFFFFFFF
|
||||
|
||||
## Colliders excluded from raycast hits
|
||||
@export var exclude: Array[RID] = []
|
||||
|
||||
var _weapon: _NetworkWeaponProxy
|
||||
|
||||
## Try to fire the weapon and return the projectile.
|
||||
## [br][br]
|
||||
## Returns true if the weapon was fired.
|
||||
func fire() -> bool:
|
||||
if not can_fire():
|
||||
return false
|
||||
|
||||
_apply_data(_get_data())
|
||||
_after_fire()
|
||||
return true
|
||||
|
||||
## Check whether this weapon can be fired.
|
||||
func can_fire() -> bool:
|
||||
return _weapon.can_fire()
|
||||
|
||||
func _init():
|
||||
_weapon = _NetworkWeaponProxy.new()
|
||||
add_child(_weapon, true, INTERNAL_MODE_BACK)
|
||||
_weapon.owner = self
|
||||
|
||||
_weapon.c_can_fire = _can_fire
|
||||
_weapon.c_can_peer_use = _can_peer_use
|
||||
_weapon.c_after_fire = _after_fire
|
||||
_weapon.c_spawn = _spawn
|
||||
_weapon.c_get_data = _get_data
|
||||
_weapon.c_apply_data = _apply_data
|
||||
_weapon.c_is_reconcilable = _is_reconcilable
|
||||
_weapon.c_reconcile = _reconcile
|
||||
|
||||
## Override this method with your own can fire logic.
|
||||
## [br][br]
|
||||
## See [NetworkWeapon].
|
||||
func _can_fire() -> bool:
|
||||
return true
|
||||
|
||||
## Override this method to check if a given peer can use this weapon.
|
||||
## [br][br]
|
||||
## See [NetworkWeapon].
|
||||
func _can_peer_use(peer_id: int) -> bool:
|
||||
return true
|
||||
|
||||
## Override this method to run any logic needed after successfully firing the
|
||||
## weapon.
|
||||
## [br][br]
|
||||
## See [NetworkWeapon].
|
||||
func _after_fire():
|
||||
pass
|
||||
|
||||
func _spawn():
|
||||
# No projectile is spawned for a hitscan weapon.
|
||||
pass
|
||||
|
||||
func _get_data() -> Dictionary:
|
||||
# Collect data needed to synchronize the firing event.
|
||||
return {
|
||||
"origin": global_transform.origin,
|
||||
"direction": -global_transform.basis.z # Assuming forward direction.
|
||||
}
|
||||
|
||||
func _apply_data(data: Dictionary):
|
||||
# Reproduces the firing event on all peers.
|
||||
var origin = data["origin"] as Vector3
|
||||
var direction = data["direction"] as Vector3
|
||||
|
||||
# Perform the raycast from origin in the given direction.
|
||||
var space_state = get_world_3d().direct_space_state
|
||||
|
||||
# Create a PhysicsRayQueryParameters3D object.
|
||||
var ray_params = PhysicsRayQueryParameters3D.new()
|
||||
ray_params.from = origin
|
||||
ray_params.to = origin + direction * max_distance
|
||||
|
||||
# Set collision masks or exclude objects:
|
||||
ray_params.collision_mask = collision_mask
|
||||
ray_params.exclude = exclude
|
||||
|
||||
var result = space_state.intersect_ray(ray_params)
|
||||
|
||||
if result:
|
||||
# Handle the hit result, such as spawning hit effects.
|
||||
_on_hit(result)
|
||||
|
||||
# Play firing effects on all peers.
|
||||
_on_fire()
|
||||
|
||||
func _is_reconcilable(request_data: Dictionary, local_data: Dictionary) -> bool:
|
||||
# Always reconcilable
|
||||
return true
|
||||
|
||||
func _reconcile(local_data: Dictionary, remote_data: Dictionary):
|
||||
# Nothing to do on reconcile
|
||||
pass
|
||||
|
||||
## Override to implement raycast hit logic.
|
||||
## [br][br]
|
||||
## The parameter is the result of a
|
||||
## [method PhysicsDirectSpaceState3D.intersect_ray] call.
|
||||
func _on_hit(result: Dictionary):
|
||||
# Implement hit effect logic here.
|
||||
# var hit_position = result.position
|
||||
# var hit_normal = result.normal
|
||||
# var collider = result.collider
|
||||
|
||||
# For example, you might emit a signal or instantiate a hit effect scene:
|
||||
# emit_signal("hit_detected", hit_position, hit_normal, collider)
|
||||
pass
|
||||
|
||||
## Override to implement firing effects, like muzzle flash or sound.
|
||||
func _on_fire():
|
||||
# Implement firing effect logic here.
|
||||
pass
|
||||
@@ -0,0 +1 @@
|
||||
uid://daf6uabdxsfwq
|
||||
@@ -0,0 +1,35 @@
|
||||
extends NetworkWeapon
|
||||
class_name _NetworkWeaponProxy
|
||||
|
||||
var c_can_fire: Callable
|
||||
var c_can_peer_use: Callable
|
||||
var c_after_fire: Callable
|
||||
var c_spawn: Callable
|
||||
var c_get_data: Callable
|
||||
var c_apply_data: Callable
|
||||
var c_is_reconcilable: Callable
|
||||
var c_reconcile: Callable
|
||||
|
||||
func _can_fire() -> bool:
|
||||
return c_can_fire.call()
|
||||
|
||||
func _can_peer_use(peer_id: int) -> bool:
|
||||
return c_can_peer_use.call(peer_id)
|
||||
|
||||
func _after_fire(projectile: Node):
|
||||
c_after_fire.call(projectile)
|
||||
|
||||
func _spawn() -> Node:
|
||||
return c_spawn.call()
|
||||
|
||||
func _get_data(projectile: Node) -> Dictionary:
|
||||
return c_get_data.call(projectile)
|
||||
|
||||
func _apply_data(projectile: Node, data: Dictionary):
|
||||
c_apply_data.call(projectile, data)
|
||||
|
||||
func _is_reconcilable(projectile: Node, request_data: Dictionary, local_data: Dictionary) -> bool:
|
||||
return c_is_reconcilable.call(projectile, request_data, local_data)
|
||||
|
||||
func _reconcile(projectile: Node, local_data: Dictionary, remote_data: Dictionary):
|
||||
c_reconcile.call(projectile, local_data, remote_data)
|
||||
@@ -0,0 +1 @@
|
||||
uid://76n8gu7udnum
|
||||
@@ -0,0 +1,223 @@
|
||||
extends Node
|
||||
class_name NetworkWeapon
|
||||
|
||||
## Base class for creating responsive weapons, by spawning projectiles locally,
|
||||
## but keeping control on the server.
|
||||
|
||||
var _projectiles: Dictionary = {}
|
||||
var _projectile_data: Dictionary = {}
|
||||
var _reconcile_buffer: Array = []
|
||||
var _rng: RandomNumberGenerator = RandomNumberGenerator.new()
|
||||
var _fired_tick: int = -1
|
||||
|
||||
static var _logger: NetfoxLogger = NetfoxLogger._for_extras("NetworkWeapon")
|
||||
|
||||
func _ready():
|
||||
_rng.randomize()
|
||||
NetworkTime.before_tick_loop.connect(_before_tick_loop)
|
||||
|
||||
## Check whether this weapon can be fired.
|
||||
func can_fire() -> bool:
|
||||
return _can_fire()
|
||||
|
||||
## Try to fire the weapon and return the projectile.
|
||||
## [br][br]
|
||||
## Returns null if the weapon can't be fired.
|
||||
func fire() -> Node:
|
||||
if not can_fire():
|
||||
return null
|
||||
|
||||
var id: String = _generate_id()
|
||||
var projectile = _spawn()
|
||||
_save_projectile(projectile, id)
|
||||
var data = _projectile_data[id]
|
||||
|
||||
if not is_multiplayer_authority():
|
||||
_request_projectile.rpc_id(get_multiplayer_authority(), id, NetworkTime.tick, data)
|
||||
else:
|
||||
_accept_projectile.rpc(id, NetworkTime.tick, data)
|
||||
|
||||
_logger.debug("Calling after fire hook for %s", [projectile.name])
|
||||
_fired_tick = NetworkTime.tick
|
||||
_after_fire(projectile)
|
||||
|
||||
return projectile
|
||||
|
||||
## Get the tick when the weapon was fired.
|
||||
## [br][br]
|
||||
## Whenever a weapon gets fired, it takes time for that event to be transmitted
|
||||
## to the server. To account for this latency, the exact tick is sent along
|
||||
## with other data, so weapon implementations can compensate for the latency.
|
||||
## [br][br]
|
||||
## One way to use this is to manually simulate the projectile after it's
|
||||
## created:
|
||||
## [codeblock]
|
||||
## func _after_fire(projectile: Node3D):
|
||||
## last_fire = get_fired_tick()
|
||||
## sound.play()
|
||||
##
|
||||
## for t in range(get_fired_tick(), NetworkTime.tick):
|
||||
## if projectile.is_queued_for_deletion():
|
||||
## break
|
||||
## projectile._tick(NetworkTime.ticktime, t)
|
||||
## [/codeblock]
|
||||
func get_fired_tick() -> int:
|
||||
return _fired_tick
|
||||
|
||||
## Override this method with your own can fire logic.
|
||||
## [br][br]
|
||||
## This can be used to implement e.g. firing cooldowns and ammo checks.
|
||||
func _can_fire() -> bool:
|
||||
return false
|
||||
|
||||
## Override this method to check if a given peer can use this weapon.
|
||||
## [br][br]
|
||||
## Usually this should check if the weapon's owner is trying to fire it, but
|
||||
## for some special cases this can be some different logic, e.g. weapons that
|
||||
## can be used by any player on a given team.
|
||||
func _can_peer_use(peer_id: int) -> bool:
|
||||
return true
|
||||
|
||||
## Override this method to run any logic needed after successfully firing the
|
||||
## weapon.
|
||||
## [br][br]
|
||||
## This can be used to e.g. reset the firing cooldown or deduct ammo.
|
||||
func _after_fire(projectile: Node):
|
||||
pass
|
||||
|
||||
## Override this method to spawn and initialize a projectile.
|
||||
## [br][br]
|
||||
## Make sure to return the projectile spawned!
|
||||
func _spawn() -> Node:
|
||||
return null
|
||||
|
||||
## Override this method to extract projectile data that should be synchronized
|
||||
## over the network.
|
||||
## [br][br]
|
||||
## This will be captured both locally and on the server, and will be used for
|
||||
## reconciliation.
|
||||
func _get_data(projectile: Node) -> Dictionary:
|
||||
return {}
|
||||
|
||||
## Override this method to apply projectile data that should be synchronized
|
||||
## over the network.
|
||||
## [br][br]
|
||||
## This is used in cases where some other client fires a weapon and the server
|
||||
## instructs us to spawn a projectile for it.
|
||||
func _apply_data(projectile: Node, data: Dictionary):
|
||||
pass
|
||||
|
||||
## Override this method to check if two projectile states can be reconciled.
|
||||
## [br][br]
|
||||
## This can be used to prevent cheating, for example by not allowing the client
|
||||
## to say it's firing from the other side of the map compared to its actual
|
||||
## position.
|
||||
## [br][br]
|
||||
## When this method returns false, the server will decline the projectile
|
||||
## request.
|
||||
func _is_reconcilable(projectile: Node, request_data: Dictionary, local_data: Dictionary) -> bool:
|
||||
return true
|
||||
|
||||
## Override this method to reconcile the initial local and remote projectile
|
||||
## state.
|
||||
## [br][br]
|
||||
## Let's say the projectile travels in a straight line from its origin, but we
|
||||
## receive a different origin from the server. In this reconciliation step,
|
||||
## the projectile's position can be adjusted to account for the different origin.
|
||||
## [br][br]
|
||||
## Unless the use case is niche, the best practice is to consider the server's
|
||||
## state as authorative.
|
||||
func _reconcile(projectile: Node, local_data: Dictionary, remote_data: Dictionary):
|
||||
pass
|
||||
|
||||
func _save_projectile(projectile: Node, id: String, data: Dictionary = {}):
|
||||
_projectiles[id] = projectile
|
||||
projectile.name += " " + id
|
||||
projectile.set_multiplayer_authority(get_multiplayer_authority())
|
||||
|
||||
if data.is_empty():
|
||||
data = _get_data(projectile)
|
||||
|
||||
_projectile_data[id] = data
|
||||
|
||||
func _before_tick_loop():
|
||||
# Reconcile projectiles
|
||||
for recon in _reconcile_buffer:
|
||||
var projectile = recon[0]
|
||||
var local_data = recon[1]
|
||||
var response_data = recon[2]
|
||||
var projectile_id = recon[3]
|
||||
|
||||
if is_instance_valid(projectile):
|
||||
_reconcile(projectile, local_data, response_data)
|
||||
else:
|
||||
_logger.warning("Projectile %s vanished by the time of reconciliation!", [projectile_id])
|
||||
|
||||
_reconcile_buffer.clear()
|
||||
|
||||
func _generate_id(length: int = 12, charset: String = "abcdefghijklmnopqrstuvwxyz0123456789") -> String:
|
||||
var result = ""
|
||||
|
||||
# Generate a random ID
|
||||
for i in range(length):
|
||||
var idx = _rng.randi_range(0, charset.length() - 1)
|
||||
result += charset[idx]
|
||||
|
||||
return result
|
||||
|
||||
@rpc("any_peer", "reliable", "call_remote")
|
||||
func _request_projectile(id: String, tick: int, request_data: Dictionary):
|
||||
var sender = multiplayer.get_remote_sender_id()
|
||||
|
||||
# Reject if sender can't use this input
|
||||
_fired_tick = tick
|
||||
if not _can_peer_use(sender) or not _can_fire():
|
||||
_decline_projectile.rpc_id(sender, id)
|
||||
_logger.error("Projectile %s rejected! Peer %s can't use this weapon now", [id, sender])
|
||||
return
|
||||
|
||||
# Validate incoming data
|
||||
var projectile = _spawn()
|
||||
var local_data: Dictionary = _get_data(projectile)
|
||||
|
||||
if not _is_reconcilable(projectile, request_data, local_data):
|
||||
projectile.queue_free()
|
||||
_decline_projectile.rpc_id(sender, id)
|
||||
_logger.error("Projectile %s rejected! Can't reconcile states: [%s, %s]", [id, request_data, local_data])
|
||||
return
|
||||
|
||||
_save_projectile(projectile, id, local_data)
|
||||
_accept_projectile.rpc(id, tick, local_data)
|
||||
_after_fire(projectile)
|
||||
|
||||
@rpc("authority", "reliable", "call_local")
|
||||
func _accept_projectile(id: String, tick: int, response_data: Dictionary):
|
||||
if multiplayer.get_unique_id() == multiplayer.get_remote_sender_id():
|
||||
# Projectile is local, nothing to do
|
||||
return
|
||||
|
||||
_logger.info("Accepting projectile %s from %s", [id, multiplayer.get_remote_sender_id()])
|
||||
|
||||
if _projectiles.has(id):
|
||||
var projectile = _projectiles[id]
|
||||
var local_data = _projectile_data[id]
|
||||
_reconcile_buffer.push_back([projectile, local_data, response_data, id])
|
||||
else:
|
||||
_fired_tick = tick
|
||||
var projectile = _spawn()
|
||||
_apply_data(projectile, response_data)
|
||||
_projectile_data.erase(id)
|
||||
_save_projectile(projectile, id, response_data)
|
||||
_after_fire(projectile)
|
||||
|
||||
@rpc("authority", "reliable", "call_remote")
|
||||
func _decline_projectile(id: String):
|
||||
if not _projectiles.has(id):
|
||||
return
|
||||
|
||||
var p = _projectiles[id]
|
||||
if is_instance_valid(p):
|
||||
p.queue_free()
|
||||
|
||||
_projectiles.erase(id)
|
||||
_projectile_data.erase(id)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cill8uva6thl5
|
||||
@@ -0,0 +1,138 @@
|
||||
extends Node
|
||||
|
||||
# Settings
|
||||
var _is_enabled: bool = ProjectSettings.get_setting(&"netfox/extras/auto_tile_windows", false)
|
||||
var _is_borderless: bool = ProjectSettings.get_setting(&"netfox/extras/borderless", false)
|
||||
var _tile_screen: int = ProjectSettings.get_setting(&"netfox/extras/screen", 0)
|
||||
|
||||
# Hash the game name, so we always get a valid filename
|
||||
var _prefix: String = "netfox-window-tiler-%x" % [ProjectSettings.get_setting(&"application/config/name").hash()]
|
||||
|
||||
var _sid: String = "%x" % [hash(int(Time.get_unix_time_from_system() / 2.))]
|
||||
var _uid: String = "%d" % [Time.get_unix_time_from_system() * 1000_0000.]
|
||||
|
||||
static var _logger: NetfoxLogger = NetfoxLogger._for_extras("WindowTiler")
|
||||
|
||||
func _ready() -> void:
|
||||
# Running on a non-editor (export template) build
|
||||
if OS.has_feature("template"):
|
||||
return
|
||||
|
||||
# Running in headless mode
|
||||
if DisplayServer.get_name() == "headless":
|
||||
return
|
||||
|
||||
# Running in CI
|
||||
for env_var in ["CI", "NETFOX_CI"]:
|
||||
if OS.get_environment(env_var) != "":
|
||||
_logger.debug("Environment variable %s set, disabling", [env_var])
|
||||
return
|
||||
|
||||
# Cleanup in case some files were left
|
||||
_cleanup()
|
||||
|
||||
# Running embedded in editor
|
||||
if _is_embedded():
|
||||
return
|
||||
|
||||
# Don't tile if disabled
|
||||
if not _is_enabled:
|
||||
return
|
||||
|
||||
_logger.debug("Tiling with sid: %s, uid: %s", [_sid, _uid])
|
||||
|
||||
var err = _make_lock(_sid, _uid)
|
||||
if err != Error.OK:
|
||||
_logger.warning("Failed to create lock for tiling, reason: %s", [error_string(err)])
|
||||
return
|
||||
|
||||
# Search for locks, stop once no new locks are found
|
||||
var locks = []
|
||||
await get_tree().create_timer(0.25).timeout
|
||||
for i in range(20):
|
||||
await get_tree().create_timer(0.1).timeout
|
||||
var new_locks = _list_lock_ids()
|
||||
|
||||
if locks == new_locks:
|
||||
break
|
||||
|
||||
locks = new_locks
|
||||
|
||||
var tile_count = locks.size()
|
||||
var idx = locks.find(_uid)
|
||||
|
||||
_logger.debug("Tiling as idx %d / %d - %s in %s", [idx, tile_count, _uid, locks])
|
||||
_tile_window(idx, tile_count)
|
||||
|
||||
func _is_embedded() -> bool:
|
||||
if Engine.has_method("is_embedded_in_editor"):
|
||||
return Engine.call("is_embedded_in_editor")
|
||||
return false
|
||||
|
||||
func _make_lock(sid: String, uid: String) -> Error:
|
||||
var path = "%s/%s-%s-%s" % [OS.get_cache_dir(), _prefix, sid, uid]
|
||||
var file := FileAccess.open(path, FileAccess.WRITE)
|
||||
|
||||
if file == null:
|
||||
return FileAccess.get_open_error()
|
||||
|
||||
file.close()
|
||||
return Error.OK
|
||||
|
||||
func _list_lock_ids() -> Array[String]:
|
||||
var result: Array[String] = []
|
||||
var dir := DirAccess.open(OS.get_cache_dir())
|
||||
|
||||
if dir:
|
||||
for f in dir.get_files():
|
||||
if f.begins_with(_prefix):
|
||||
result.append(_get_uid(f))
|
||||
|
||||
return result
|
||||
|
||||
func _cleanup():
|
||||
var result: Array[String] = []
|
||||
var dir := DirAccess.open(OS.get_cache_dir())
|
||||
|
||||
if dir:
|
||||
for f in dir.get_files():
|
||||
if f.begins_with(_prefix) and _get_sid(f) != _sid:
|
||||
_logger.trace("Cleaned up lock: %s", [f])
|
||||
dir.remove(OS.get_cache_dir() + "/" + f)
|
||||
|
||||
func _get_sid(filename: String) -> String:
|
||||
return filename.substr(_prefix.length() + 1).get_slice("-", 0)
|
||||
|
||||
func _get_uid(filename: String) -> String:
|
||||
return filename.substr(_prefix.length() + 1).get_slice("-", 1)
|
||||
|
||||
func _tile_window(i: int, total: int) -> void:
|
||||
var screen = _tile_screen
|
||||
var screen_rect = DisplayServer.screen_get_usable_rect(screen)
|
||||
|
||||
var window: Window = get_tree().get_root()
|
||||
window.set_current_screen(screen)
|
||||
|
||||
if total == 1:
|
||||
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_MAXIMIZED)
|
||||
return
|
||||
|
||||
window.borderless = _is_borderless
|
||||
|
||||
# Divide up the screen
|
||||
var windows_per_row = int(ceil(sqrt(total)))
|
||||
var windows_per_col = int(ceil(total / float(windows_per_row)))
|
||||
var window_size = Vector2(
|
||||
screen_rect.size.x / windows_per_row, screen_rect.size.y / windows_per_col
|
||||
)
|
||||
|
||||
window.set_size(window_size)
|
||||
|
||||
# Position of the window based on index.
|
||||
var row = i / windows_per_row
|
||||
var col = i % windows_per_row
|
||||
|
||||
var x = screen_rect.position.x + col * window_size.x
|
||||
var y = screen_rect.position.y + row * window_size.y
|
||||
|
||||
window.set_position(Vector2(x, y))
|
||||
@@ -0,0 +1 @@
|
||||
uid://cmqjajhjgffm1
|
||||
@@ -0,0 +1,31 @@
|
||||
# Contributors
|
||||
|
||||
This addon, and the entirety of [netfox] is a shared effort of [Fox's Sake
|
||||
Studio], and the community. The following is the list of community contributors
|
||||
involved with netfox:
|
||||
|
||||
* Alberto Klocker <albertok@gmail.com>
|
||||
* Andrew Davis <jonandrewdavis@gmail.com>
|
||||
* Bryan Lee <42545742+bryanmylee@users.noreply.github.com>
|
||||
* Dan Schuman <danschuman@gmail.com>
|
||||
* Dustie <77035922+DustieDog@users.noreply.github.com>
|
||||
* Eric Volpone <ericvolpone@gmail.com>
|
||||
* Gordon MacPherson <gordon@gordonite.tech>
|
||||
* Jake Cattrall <krazyjakee@gmail.com>
|
||||
* Jon Stevens <1030863+jonstvns@users.noreply.github.com>
|
||||
* Joseph Michael Ware <9at25jnr3@mozmail.com>
|
||||
* Matias Kumpulainen <kumpulainen.matias@gmail.com>
|
||||
* Nicolas Batty <nicolas.batty@gmail.com>
|
||||
* Ricky <77863853+RickyYCheng@users.noreply.github.com>
|
||||
* Riordan-DC <44295008+Riordan-DC@users.noreply.github.com>
|
||||
* Russ <622740+russ-@users.noreply.github.com>
|
||||
* Ryan Roden-Corrent <github@rcorre.net>
|
||||
* TheYellowArchitect <dmalandris@uth.gr>
|
||||
* TheYellowArchitect <hello@theyellowarchitect.com>
|
||||
* camperotactico <alberto.vgdd@gmail.com>
|
||||
* gk98s <89647115+gk98s@users.noreply.github.com>
|
||||
* zibetnu <9at25jnr3@mozmail.com>
|
||||
|
||||
[netfox]: https://github.com/foxssake/netfox
|
||||
[Fox's Sake Studio]: https://github.com/foxssake/
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
Copyright 2023 Gálffy Tamás
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,8 @@
|
||||
# netfox.internals
|
||||
|
||||
Shared utilities for [netfox] addons. Not intended for standalone usage.
|
||||
|
||||
Instead, check out the other addons in the [netfox] repository.
|
||||
|
||||
|
||||
[netfox]: https://github.com/foxssake/netfox
|
||||
@@ -0,0 +1,56 @@
|
||||
extends RefCounted
|
||||
class_name _BiMap
|
||||
|
||||
# Maps one-to-one associations in a bidirectional way
|
||||
|
||||
var _keys_to_values := {}
|
||||
var _values_to_keys := {}
|
||||
|
||||
func put(key: Variant, value: Variant) -> void:
|
||||
var old_value = _keys_to_values.get(key)
|
||||
var old_key = _values_to_keys.get(value)
|
||||
|
||||
if old_value != null: _values_to_keys.erase(old_value)
|
||||
if old_key != null: _keys_to_values.erase(old_key)
|
||||
|
||||
_keys_to_values[key] = value
|
||||
_values_to_keys[value] = key
|
||||
|
||||
func get_by_key(key: Variant) -> Variant:
|
||||
return _keys_to_values.get(key)
|
||||
|
||||
func get_by_value(value: Variant) -> Variant:
|
||||
return _values_to_keys.get(value)
|
||||
|
||||
func has_key(key: Variant) -> bool:
|
||||
return _keys_to_values.has(key)
|
||||
|
||||
func has_value(value: Variant) -> bool:
|
||||
return _values_to_keys.has(value)
|
||||
|
||||
func erase_key(key: Variant) -> bool:
|
||||
if not has_key(key):
|
||||
return false
|
||||
|
||||
var value = get_by_key(key)
|
||||
_values_to_keys.erase(value)
|
||||
_keys_to_values.erase(key)
|
||||
return true
|
||||
|
||||
func erase_value(value: Variant) -> bool:
|
||||
if not has_value(value):
|
||||
return false
|
||||
|
||||
var key = get_by_value(value)
|
||||
_values_to_keys.erase(value)
|
||||
_keys_to_values.erase(key)
|
||||
return true
|
||||
|
||||
func size() -> int:
|
||||
return _keys_to_values.size()
|
||||
|
||||
func keys() -> Array:
|
||||
return _keys_to_values.keys()
|
||||
|
||||
func values() -> Array:
|
||||
return _values_to_keys.keys()
|
||||
@@ -0,0 +1 @@
|
||||
uid://b826s26d4ud5j
|
||||
@@ -0,0 +1,44 @@
|
||||
extends Object
|
||||
class_name _NetfoxEditorUtils
|
||||
|
||||
static func gather_properties(root: Node, callback_name: String, handler: Callable) -> Array[String]:
|
||||
var result: Array[String] = []
|
||||
|
||||
var nodes: Array[Node] = root.find_children("*")
|
||||
nodes.push_front(root)
|
||||
for node in nodes:
|
||||
if not node.has_method(callback_name):
|
||||
continue
|
||||
|
||||
var readable_node_name := "\"%s\" (\"%s\")" % [node.name, root.get_path_to(node)]
|
||||
if node.get(callback_name) == null:
|
||||
result.push_back("Can't grab method \"%s\" from node %s! Is it a @tool?" % [callback_name, readable_node_name])
|
||||
continue
|
||||
|
||||
var props = node.get(callback_name).call()
|
||||
if not props is Array:
|
||||
result.push_back("Node %s didn't return an array on calling \"%s\"" % [readable_node_name, callback_name])
|
||||
continue
|
||||
|
||||
for prop in props:
|
||||
if prop is String:
|
||||
# Property is a string, meaning property path relative to node
|
||||
handler.call(node, prop)
|
||||
elif prop is Array and prop.size() >= 2:
|
||||
# Property is a node-property tuple
|
||||
var prop_node: Node = null
|
||||
|
||||
# Node can be a String, NodePath, or an actual Node
|
||||
if prop[0] is String or prop[0] is NodePath:
|
||||
prop_node = node.get_node(prop[0])
|
||||
elif prop[0] is Node:
|
||||
prop_node = prop[0]
|
||||
else:
|
||||
result.push_back("Node %s specified invalid node in \"%s\": %s" % [readable_node_name, callback_name, prop])
|
||||
continue
|
||||
|
||||
handler.call(prop_node, prop[1])
|
||||
else:
|
||||
result.push_back("Node %s specified invalid property in \"%s\": %s" % [readable_node_name, callback_name, prop])
|
||||
|
||||
return result
|
||||
@@ -0,0 +1 @@
|
||||
uid://0ohd4n5yjilb
|
||||
@@ -0,0 +1,70 @@
|
||||
extends RefCounted
|
||||
class_name _HistoryBuffer
|
||||
|
||||
# Maps ticks (int) to arbitrary data
|
||||
var _buffer: Dictionary = {}
|
||||
|
||||
func get_snapshot(tick: int):
|
||||
if _buffer.has(tick):
|
||||
return _buffer[tick]
|
||||
else:
|
||||
return null
|
||||
|
||||
func set_snapshot(tick: int, data):
|
||||
_buffer[tick] = data
|
||||
|
||||
func get_buffer() -> Dictionary:
|
||||
return _buffer
|
||||
|
||||
func get_earliest_tick() -> int:
|
||||
return _buffer.keys().min()
|
||||
|
||||
func get_latest_tick() -> int:
|
||||
return _buffer.keys().max()
|
||||
|
||||
func get_closest_tick(tick: int) -> int:
|
||||
if _buffer.has(tick):
|
||||
return tick
|
||||
|
||||
if _buffer.is_empty():
|
||||
return -1
|
||||
|
||||
var earliest_tick = get_earliest_tick()
|
||||
if tick < earliest_tick:
|
||||
return earliest_tick
|
||||
|
||||
var latest_tick = get_latest_tick()
|
||||
if tick > latest_tick:
|
||||
return latest_tick
|
||||
|
||||
return _buffer.keys() \
|
||||
.filter(func (key): return key < tick) \
|
||||
.max()
|
||||
|
||||
func get_history(tick: int):
|
||||
var closest_tick = get_closest_tick(tick)
|
||||
return _buffer.get(closest_tick)
|
||||
|
||||
func trim(earliest_tick_to_keep: int):
|
||||
var ticks := _buffer.keys()
|
||||
for tick in ticks:
|
||||
if tick < earliest_tick_to_keep:
|
||||
_buffer.erase(tick)
|
||||
|
||||
func clear():
|
||||
_buffer.clear()
|
||||
|
||||
func size() -> int:
|
||||
return _buffer.size()
|
||||
|
||||
func is_empty() -> bool:
|
||||
return _buffer.is_empty()
|
||||
|
||||
func has(tick) -> bool:
|
||||
return _buffer.has(tick)
|
||||
|
||||
func ticks() -> Array:
|
||||
return _buffer.keys()
|
||||
|
||||
func erase(tick):
|
||||
_buffer.erase(tick)
|
||||
@@ -0,0 +1 @@
|
||||
uid://btuvvlw3jkx2e
|
||||
@@ -0,0 +1,208 @@
|
||||
extends RefCounted
|
||||
class_name NetfoxLogger
|
||||
|
||||
## Logger implementation for use with netfox
|
||||
##
|
||||
## NetfoxLoggers implement distinct log levels. These can be used to filter
|
||||
## which messages are actually emitted. All messages are output using [method
|
||||
## @GlobalScope.print]. Warnings and errors are also pushed to the debug panel,
|
||||
## using [method @GlobalScope.push_warning] and [method @GlobalScope.push_error]
|
||||
## respectively, if [member push_to_debugger] is enabled.
|
||||
## [br][br]
|
||||
## Every logger has a name, and belongs to a module. Logging level can be
|
||||
## overridden per module, using [member module_log_level].
|
||||
## [br][br]
|
||||
## Loggers also support tags. Tags can be used to provide extra pieces of
|
||||
## information that are logged with each message. Some tags are provided by
|
||||
## netfox. Additional tags can be added using [method register_tag].
|
||||
##
|
||||
## @tutorial(Logging Guide): https://foxssake.github.io/netfox/latest/netfox/guides/logging/
|
||||
|
||||
|
||||
enum {
|
||||
LOG_ALL, ## Filter level to log every message
|
||||
LOG_TRACE, ## Trace logs, the most verbose level
|
||||
LOG_DEBUG, ## Debug logs
|
||||
LOG_INFO, ## Info logs
|
||||
LOG_WARN, ## Warnings
|
||||
LOG_ERROR, ## Errors
|
||||
LOG_NONE ## Filter level to log no messages
|
||||
}
|
||||
|
||||
## Default log level to fall back on, if not configured
|
||||
const DEFAULT_LOG_LEVEL := LOG_DEBUG
|
||||
|
||||
const _LEVEL_PREFIXES: Array[String] = [
|
||||
"",
|
||||
"TRC",
|
||||
"DBG",
|
||||
"INF",
|
||||
"WRN",
|
||||
"ERR",
|
||||
""
|
||||
]
|
||||
|
||||
## Global logging level, used by all loggers
|
||||
static var log_level: int
|
||||
|
||||
## Per-module logging level, used only by loggers belonging to the given module
|
||||
## [br][br]
|
||||
## This is a dictionary that associates module names ( strings ) to log levels
|
||||
## ( int, e.g. [constant LOG_DEBUG] ).
|
||||
static var module_log_level: Dictionary
|
||||
|
||||
## Set to true to enable calling [@GlobalScope.push_warning] and
|
||||
## [@GlobalScope.push_error]
|
||||
static var push_to_debugger := true
|
||||
|
||||
static var _tags: Dictionary = {}
|
||||
static var _ordered_tags: Array[Callable] = []
|
||||
|
||||
## Logger module
|
||||
var module: String
|
||||
## Logger name
|
||||
var name: String
|
||||
|
||||
|
||||
## Register a tag
|
||||
## [br][br]
|
||||
## Tags are callables that provide pieces of context, included in all log
|
||||
## messges. The [param tag] callable must return a string.
|
||||
static func register_tag(tag: Callable, priority: int = 0) -> void:
|
||||
# Save tag
|
||||
if not _tags.has(priority):
|
||||
_tags[priority] = [tag]
|
||||
else:
|
||||
_tags[priority].push_back(tag)
|
||||
|
||||
# Recalculate tag order
|
||||
_ordered_tags.clear()
|
||||
|
||||
var prio_groups = _tags.keys()
|
||||
prio_groups.sort()
|
||||
|
||||
for prio_group in prio_groups:
|
||||
var tag_group = _tags[prio_group]
|
||||
_ordered_tags.append_array(tag_group)
|
||||
|
||||
## Free an already registered tag
|
||||
static func free_tag(tag: Callable) -> void:
|
||||
for priority in _tags.keys():
|
||||
var priority_group := _tags[priority] as Array
|
||||
priority_group.erase(tag)
|
||||
|
||||
# NOTE: Arrays are passed as reference, no need to re-assign after modifying
|
||||
if priority_group.is_empty():
|
||||
_tags.erase(priority)
|
||||
|
||||
_ordered_tags.erase(tag)
|
||||
|
||||
static func _static_init():
|
||||
log_level = ProjectSettings.get_setting(&"netfox/logging/log_level", DEFAULT_LOG_LEVEL)
|
||||
module_log_level = {
|
||||
"netfox": ProjectSettings.get_setting(&"netfox/logging/netfox_log_level", DEFAULT_LOG_LEVEL),
|
||||
"netfox.noray": ProjectSettings.get_setting(&"netfox/logging/netfox_noray_log_level", DEFAULT_LOG_LEVEL),
|
||||
"netfox.extras": ProjectSettings.get_setting(&"netfox/logging/netfox_extras_log_level", DEFAULT_LOG_LEVEL)
|
||||
}
|
||||
|
||||
static func _for_netfox(p_name: String) -> NetfoxLogger:
|
||||
return NetfoxLogger.new("netfox", p_name)
|
||||
|
||||
static func _for_noray(p_name: String) -> NetfoxLogger:
|
||||
return NetfoxLogger.new("netfox.noray", p_name)
|
||||
|
||||
static func _for_extras(p_name: String) -> NetfoxLogger:
|
||||
return NetfoxLogger.new("netfox.extras", p_name)
|
||||
|
||||
static func _make_setting(name: String) -> Dictionary:
|
||||
return {
|
||||
"name": name,
|
||||
"value": DEFAULT_LOG_LEVEL,
|
||||
"type": TYPE_INT,
|
||||
"hint": PROPERTY_HINT_ENUM,
|
||||
"hint_string": "All,Trace,Debug,Info,Warning,Error,None"
|
||||
}
|
||||
|
||||
|
||||
func _init(p_module: String, p_name: String):
|
||||
module = p_module
|
||||
name = p_name
|
||||
|
||||
## Log a trace message
|
||||
## [br][br]
|
||||
## Traces are the most verbose, usually used for drilling down into very niche
|
||||
## bugs.
|
||||
func trace(text: String, values: Array = []):
|
||||
_log_text(text, values, LOG_TRACE)
|
||||
|
||||
## Log a debug message
|
||||
## [br][br]
|
||||
## Debug messages are verbose, usually used to reconstruct and investigate bugs.
|
||||
func debug(text: String, values: Array = []):
|
||||
_log_text(text, values, LOG_DEBUG)
|
||||
|
||||
## Log an info message
|
||||
## [br][br]
|
||||
## Info messages provide general notifications about application events.
|
||||
func info(text: String, values: Array = []):
|
||||
_log_text(text, values, LOG_INFO)
|
||||
|
||||
## Log a warning message
|
||||
## [br][br]
|
||||
## This is also forwarded to [method @GlobalScope.push_warning], if enabled with
|
||||
## [member push_to_debugger]. Warning messages usually indicate that something
|
||||
## has gone wrong, but is recoverable.
|
||||
func warning(text: String, values: Array = []):
|
||||
if _check_log_level(LOG_WARN):
|
||||
var formatted_text = _format_text(text, values, LOG_WARN)
|
||||
if push_to_debugger:
|
||||
push_warning(formatted_text)
|
||||
|
||||
# Print so it shows up in the Output panel too
|
||||
print(formatted_text)
|
||||
|
||||
## Log an error message
|
||||
## [br][br]
|
||||
## This is also forwarded to [method @GlobalScope.push_error], if enabled with
|
||||
## [member push_to_debugger]. Error messages usually indicate an issue that
|
||||
## can't be recovered from.
|
||||
func error(text: String, values: Array = []):
|
||||
if _check_log_level(LOG_ERROR):
|
||||
var formatted_text = _format_text(text, values, LOG_ERROR)
|
||||
if push_to_debugger:
|
||||
push_error(formatted_text)
|
||||
|
||||
# Print so it shows up in the Output panel too
|
||||
print(formatted_text)
|
||||
|
||||
func _check_log_level(level: int) -> bool:
|
||||
var cmp_level = log_level
|
||||
if level < cmp_level:
|
||||
return false
|
||||
|
||||
if module_log_level.has(module):
|
||||
var module_level = module_log_level.get(module)
|
||||
return level >= module_level
|
||||
|
||||
return true
|
||||
|
||||
func _format_text(text: String, values: Array, level: int) -> String:
|
||||
level = clampi(level, LOG_TRACE, LOG_ERROR)
|
||||
|
||||
var result := PackedStringArray()
|
||||
|
||||
result.append("[%s]" % [_LEVEL_PREFIXES[level]])
|
||||
for tag in _ordered_tags:
|
||||
result.append("[%s]" % [tag.call()])
|
||||
result.append("[%s::%s] " % [module, name])
|
||||
|
||||
if values.is_empty():
|
||||
result.append(text)
|
||||
else:
|
||||
result.append(text % values)
|
||||
|
||||
return "".join(result)
|
||||
|
||||
func _log_text(text: String, values: Array, level: int):
|
||||
if _check_log_level(level):
|
||||
print(_format_text(text, values, level))
|
||||
@@ -0,0 +1 @@
|
||||
uid://dp4rakv0drj1f
|
||||
@@ -0,0 +1,7 @@
|
||||
[plugin]
|
||||
|
||||
name="netfox.internals"
|
||||
description="Shared internals for netfox addons"
|
||||
author="Tamas Galffy and contributors"
|
||||
version="1.35.3"
|
||||
script="plugin.gd"
|
||||
@@ -0,0 +1,34 @@
|
||||
@tool
|
||||
extends EditorPlugin
|
||||
|
||||
var SETTINGS = [
|
||||
NetfoxLogger._make_setting("netfox/logging/log_level")
|
||||
]
|
||||
|
||||
func _enter_tree():
|
||||
for setting in SETTINGS:
|
||||
add_setting(setting)
|
||||
|
||||
func _exit_tree():
|
||||
if ProjectSettings.get_setting("netfox/general/clear_settings", false):
|
||||
for setting in SETTINGS:
|
||||
remove_setting(setting)
|
||||
|
||||
func add_setting(setting: Dictionary):
|
||||
if ProjectSettings.has_setting(setting.name):
|
||||
return
|
||||
|
||||
ProjectSettings.set_setting(setting.name, setting.value)
|
||||
ProjectSettings.set_initial_value(setting.name, setting.value)
|
||||
ProjectSettings.add_property_info({
|
||||
"name": setting.get("name"),
|
||||
"type": setting.get("type"),
|
||||
"hint": setting.get("hint", PROPERTY_HINT_NONE),
|
||||
"hint_string": setting.get("hint_string", "")
|
||||
})
|
||||
|
||||
func remove_setting(setting: Dictionary):
|
||||
if not ProjectSettings.has_setting(setting.name):
|
||||
return
|
||||
|
||||
ProjectSettings.clear(setting.name)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bvhj7f66i6yl4
|
||||
@@ -0,0 +1,34 @@
|
||||
extends RefCounted
|
||||
class_name _RingBuffer
|
||||
|
||||
var _data: Array
|
||||
var _capacity: int
|
||||
var _size: int = 0
|
||||
var _head: int = 0
|
||||
|
||||
func _init(p_capacity: int):
|
||||
_capacity = p_capacity
|
||||
_data = []
|
||||
_data.resize(p_capacity)
|
||||
|
||||
func push(item):
|
||||
_data[_head] = item
|
||||
|
||||
_size += 1
|
||||
_head = (_head + 1) % _capacity
|
||||
|
||||
func get_data() -> Array:
|
||||
if _size < _capacity:
|
||||
return _data.slice(0, _size)
|
||||
else:
|
||||
return _data
|
||||
|
||||
func size() -> int:
|
||||
return _size
|
||||
|
||||
func is_empty() -> bool:
|
||||
return _size == 0
|
||||
|
||||
func clear():
|
||||
_size = 0
|
||||
_head = 0
|
||||
@@ -0,0 +1 @@
|
||||
uid://cmfbs3cx33lkw
|
||||
@@ -0,0 +1,64 @@
|
||||
extends RefCounted
|
||||
class_name _Set
|
||||
|
||||
var _data: Dictionary = {}
|
||||
var _iterator_idx: int = -1
|
||||
|
||||
static func of(items: Array) -> _Set:
|
||||
var result := _Set.new()
|
||||
for item in items:
|
||||
result.add(item)
|
||||
return result
|
||||
|
||||
func add(value):
|
||||
_data[value] = true
|
||||
|
||||
func has(value) -> bool:
|
||||
return _data.has(value)
|
||||
|
||||
func size() -> int:
|
||||
return _data.size()
|
||||
|
||||
func is_empty() -> bool:
|
||||
return _data.is_empty()
|
||||
|
||||
func erase(value):
|
||||
return _data.erase(value)
|
||||
|
||||
func clear():
|
||||
_data.clear()
|
||||
|
||||
func values() -> Array:
|
||||
return _data.keys()
|
||||
|
||||
func min():
|
||||
return _data.keys().min()
|
||||
|
||||
func max():
|
||||
return _data.keys().max()
|
||||
|
||||
func equals(other) -> bool:
|
||||
if not other or not other is _Set:
|
||||
return false
|
||||
|
||||
return values() == other.values()
|
||||
|
||||
func _to_string():
|
||||
return "Set" + str(values())
|
||||
|
||||
func _iter_init(arg) -> bool:
|
||||
_iterator_idx = 0
|
||||
return _can_iterate()
|
||||
|
||||
func _iter_next(arg) -> bool:
|
||||
_iterator_idx += 1
|
||||
return _can_iterate()
|
||||
|
||||
func _iter_get(arg):
|
||||
return _data.keys()[_iterator_idx]
|
||||
|
||||
func _can_iterate() -> bool:
|
||||
if _data.is_empty() or _iterator_idx >= _data.size():
|
||||
_iterator_idx = -1
|
||||
return false
|
||||
return true
|
||||
@@ -0,0 +1 @@
|
||||
uid://bjvmp7eed1aj0
|
||||
@@ -0,0 +1,31 @@
|
||||
# Contributors
|
||||
|
||||
This addon, and the entirety of [netfox] is a shared effort of [Fox's Sake
|
||||
Studio], and the community. The following is the list of community contributors
|
||||
involved with netfox:
|
||||
|
||||
* Alberto Klocker <albertok@gmail.com>
|
||||
* Andrew Davis <jonandrewdavis@gmail.com>
|
||||
* Bryan Lee <42545742+bryanmylee@users.noreply.github.com>
|
||||
* Dan Schuman <danschuman@gmail.com>
|
||||
* Dustie <77035922+DustieDog@users.noreply.github.com>
|
||||
* Eric Volpone <ericvolpone@gmail.com>
|
||||
* Gordon MacPherson <gordon@gordonite.tech>
|
||||
* Jake Cattrall <krazyjakee@gmail.com>
|
||||
* Jon Stevens <1030863+jonstvns@users.noreply.github.com>
|
||||
* Joseph Michael Ware <9at25jnr3@mozmail.com>
|
||||
* Matias Kumpulainen <kumpulainen.matias@gmail.com>
|
||||
* Nicolas Batty <nicolas.batty@gmail.com>
|
||||
* Ricky <77863853+RickyYCheng@users.noreply.github.com>
|
||||
* Riordan-DC <44295008+Riordan-DC@users.noreply.github.com>
|
||||
* Russ <622740+russ-@users.noreply.github.com>
|
||||
* Ryan Roden-Corrent <github@rcorre.net>
|
||||
* TheYellowArchitect <dmalandris@uth.gr>
|
||||
* TheYellowArchitect <hello@theyellowarchitect.com>
|
||||
* camperotactico <alberto.vgdd@gmail.com>
|
||||
* gk98s <89647115+gk98s@users.noreply.github.com>
|
||||
* zibetnu <9at25jnr3@mozmail.com>
|
||||
|
||||
[netfox]: https://github.com/foxssake/netfox
|
||||
[Fox's Sake Studio]: https://github.com/foxssake/
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
Copyright 2023 Gálffy Tamás
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,39 @@
|
||||
# netfox.noray
|
||||
|
||||
Bulletproof your connectivity with [netfox]'s [noray] integration!
|
||||
|
||||
## Features
|
||||
|
||||
* 🤝 Establish connectivity using NAT punchthrough
|
||||
* Uses [noray] for orchestration
|
||||
* Implements a full UDP handshake
|
||||
* 🛜 Use [noray] as a relay
|
||||
* Useful in cases where NAT punchthrough fails
|
||||
* If you can see this repo, you probably can connect through [noray]
|
||||
|
||||
## Install
|
||||
|
||||
See the root [README](../../README.md).
|
||||
|
||||
> *Note*, that while *netfox.noray* is part of the *netfox* suite, it can be
|
||||
> used alone, without installing *netfox* itself.
|
||||
|
||||
## Usage
|
||||
|
||||
See the [docs](https://foxssake.github.io/netfox/netfox.noray/guides/noray/).
|
||||
|
||||
For a full example, see [noray-bootstrapper.gd].
|
||||
|
||||
## License
|
||||
|
||||
netfox.noray is under the [MIT license](LICENSE).
|
||||
|
||||
## Issues
|
||||
|
||||
In case of any issues, comments, or questions, please feel free to [open an issue]!
|
||||
|
||||
[netfox]: https://github.com/foxssake/netfox
|
||||
[source]: https://github.com/foxssake/netfox/archive/refs/heads/main.zip
|
||||
[noray]: https://github.com/foxssake/noray
|
||||
[noray-bootstrapper.gd]: ../../examples/shared/scripts/noray-bootstrapper.gd
|
||||
[open an issue]: https://github.com/foxssake/netfox/issues
|
||||
@@ -0,0 +1,53 @@
|
||||
@tool
|
||||
extends EditorPlugin
|
||||
|
||||
const ROOT = "res://addons/netfox.noray"
|
||||
|
||||
var SETTINGS = [
|
||||
NetfoxLogger._make_setting("netfox/logging/netfox_noray_log_level")
|
||||
]
|
||||
|
||||
const AUTOLOADS = [
|
||||
{
|
||||
"name": "Noray",
|
||||
"path": ROOT + "/noray.gd"
|
||||
},
|
||||
{
|
||||
"name": "PacketHandshake",
|
||||
"path": ROOT + "/packet-handshake.gd"
|
||||
}
|
||||
]
|
||||
|
||||
func _enter_tree():
|
||||
for setting in SETTINGS:
|
||||
add_setting(setting)
|
||||
|
||||
for autoload in AUTOLOADS:
|
||||
add_autoload_singleton(autoload.name, autoload.path)
|
||||
|
||||
func _exit_tree():
|
||||
if ProjectSettings.get_setting("netfox/general/clear_settings", false):
|
||||
for setting in SETTINGS:
|
||||
remove_setting(setting)
|
||||
|
||||
for autoload in AUTOLOADS:
|
||||
remove_autoload_singleton(autoload.name)
|
||||
|
||||
func add_setting(setting: Dictionary):
|
||||
if ProjectSettings.has_setting(setting.name):
|
||||
return
|
||||
|
||||
ProjectSettings.set_setting(setting.name, setting.value)
|
||||
ProjectSettings.set_initial_value(setting.name, setting.value)
|
||||
ProjectSettings.add_property_info({
|
||||
"name": setting.get("name"),
|
||||
"type": setting.get("type"),
|
||||
"hint": setting.get("hint", PROPERTY_HINT_NONE),
|
||||
"hint_string": setting.get("hint_string", "")
|
||||
})
|
||||
|
||||
func remove_setting(setting: Dictionary):
|
||||
if not ProjectSettings.has_setting(setting.name):
|
||||
return
|
||||
|
||||
ProjectSettings.clear(setting.name)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user