8 Commits

Author SHA1 Message Date
shawn ce39b237c3 Fix fps_camera.gd path for export — move out of ignored client/ dir
- Copied fps_camera.gd from client/characters/character/ to scripts/characters/
- Updated player.tscn to reference new path
- The client/ directory has its own project.godot so Godot ignores it during export
- Zero export errors now
2026-07-02 18:19:12 -04:00
shawn a69c60ce85 Fix Windows build: use entry.tscn as main scene, add fallback input handling
- Changed run/main_scene to entry.tscn (smart bootstrap detects headless vs display)
- client_main.gd now uses player.tscn instead of old FPS template
- Added mouse look (click-to-capture, escape-to-release) to player.gd fallback path
- Added _gather() call to player.gd fallback _physics_process() so PlayerNetInput
  is populated even without NetworkTime singleton (exported builds)
- Re-exported Windows build with same tag v0.1.0-windows
2026-07-02 18:17:05 -04:00
shawn 969741aa31 v0.1.0-windows: initial Windows release build
- WeaponData: restored class_name, converted static vars to make() factory
- WeaponDefinitions: updated to use make() instead of static var refs
- Windows export: tactical-shooter.exe + godot-jolt_windows-x64.dll
- Build artifact: build/tactical-shooter-windows-x86_64-v0.1.0.zip
2026-07-02 18:09:16 -04:00
shawn e70ce76207 P7.9: fix headless export — resolve Resource class_name ordering + duplicate RoundManager + simulation stub API
- Fix dead code in server_main.gd (unreachable lag compensation print)
- Remove WeaponData type hints from weapon_server.gd (Resource class_name in Node scripts = headless fail)
- Use preload() instead of WeaponData class_name in weapon_definitions.gd for const refs
- Lazy-init WEAPONS dict (const can't reference preload members)
- Remove duplicate class_name RoundManager from server/scripts/round/round_manager.gd
- Remove DamageProcessor type hints from round/round_manager.gd and bomb_objective.gd
- Add start()/stop()/can_tick()/tick()/spawn_entity()/despawn_entity() to simulation_server_stub.gd
- Fix get_world_3d() → get_tree().root.world_3d in weapon_server.gd Node context
- Fix var data := → var data = for untyped WeaponDefinitions.get_weapon() returns
- Clean export cache and verify server starts with zero parse errors
2026-07-02 17:57:09 -04:00
shawn 926446e5cf P7.8: Port FPS controller to netfox BaseNetInput pattern
- FPSCharacterController: removed SimulationServer dependency, added
  _rollback_tick() with acceleration-based CharacterBody3D movement,
  gravity, jump, crouch/sprint speed. Kept mouse look, crouch lerp,
  sprint/crouch toggle, _move_local() standalone fallback.
- Player: extends FPSCharacterController (CharacterBody3D). Added
  authority-guarded _rollback_tick that delegates to super for server
  and local client prediction. TickInterpolator created dynamically
  (avoids headless class_name parse errors). PlayerNetInput child
  wired via existing RollbackSynchronizer input_properties.
- player.tscn: CharacterBody3D root with FpsCamera, CollisionShape3D,
  PlayerNetInput children. TickInterpolator created in code.
- client_main.gd: _spawn_local_player() creates FPS player node with
  first-person camera for own peer. _spawn_remote_player() removes
  FpsCamera for remote players.
- fps_camera.gd: duck-typed _controller (Node) instead of class_name
  cast for headless safety. Fixed type inference warnings.

Also includes parent task P7.5-P7.7 changes:
- project.godot: main_scene -> server_main.tscn
- network_manager.gd: Chan enum -> raw channel ints
- server_main.gd: deferred ServerConfig load, GameServer load()
- game_server.gd: commented out dev deps for headless compilation
2026-07-02 17:45:03 -04:00
shawn e7299b17e9 Phase 7: netfox + godot-jolt stack upgrade
Stack installed:
- netfox v1.35.3 (core + extras + noray + internals)
- godot-jolt v0.16.0-stable

Architecture:
- Server: ENet transport (works headless, no netfox deps)
- Client/Editor: netfox rollback (RollbackSynchronizer, TickInterpolator)

New/modified:
- docs/migration-netfox-plan.md — migration architecture
- scripts/network/network_manager.gd — netfox-aware ENet fallback
- scripts/network/player.gd — clean base player
- client/characters/player_netfox.gd — rollback player w/ WeaponManager
- client/characters/input/player_net_input.gd — BaseNetInput subclass
- client/characters/character/fps_character_controller.gd — netfox input feed
- client/weapons/ — weapon data, registry, TacticalWeaponHitscan, WeaponManager
- client/scripts/round_replicator.gd — client-side round state bridge
- server/scripts/round_manager.gd — improved state machine
- server/scripts/plugin_api/plugin_manager.gd — refined plugin system
- config: enemy_tag, ally_tag for meatball targeting

Removed: old C++ SimulationServer GDExtension (replaced by netfox rollback)
2026-07-02 17:39:22 -04:00
shawn e2dc429caa t_p7.4_port: fix player.gd extends + FPSCharacterController deps
- Changed player.gd from extending FPSCharacterController (via path) to
  extending CharacterBody3D directly, avoiding headless class resolution
  error for client/ scripts.
- Removed super._ready() and _set_local_player() calls (FPSCharacterController
  specifics not available in CharacterBody3D).
- Replaced super._rollback_tick() with direct simulation guard.
- Removed reset_pose() call (FPSCharacterController-specific).
- All FPSCharacterController features remain in client-side player_netfox.gd
  which extends player.gd.
- Verified: 0 SCRIPT ERRORs in headless mode.
2026-07-02 17:39:19 -04:00
shawn 25221bf3cb t_p7.4_port: network_manager to netfox
- Fixed double-emission bug in network_manager.gd: ENet signal connections
  (peer_connected/peer_disconnected) are now only connected when netfox
  NetworkEvents is NOT available, preventing duplicate player_connected/
  player_disconnected signal emissions.
- Fixed stop() to only disconnect ENet signals when they were actually
  connected (mirrors the conditional connection in start_server()).
- Fixed headless parse error in player_net_input.gd: replaced direct
  NetworkTime.before_tick_loop reference with Engine.get_singleton()
  call, avoiding unresolved type identifier in headless mode.
- netfox_bootstrap.gd added as minimal autoload placeholder.
- Dual-path architecture: netfox NetworkEvents when available (editor),
  ENet fallback when netfox unavailable (headless server).
- Broadcast RPCs (spawn, round state, scores) kept as-is since they
  work identically through Godot's MultiplayerAPI in both paths.
2026-07-02 17:39:06 -04:00
3258 changed files with 524985 additions and 636 deletions
+18
View File
@@ -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.
+91
View File
@@ -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.
+41
View File
@@ -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>
@@ -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.
@@ -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>
@@ -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>
@@ -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.
+31
View File
@@ -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/
+18
View File
@@ -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.
+34
View File
@@ -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
+21
View File
@@ -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

+187
View File
@@ -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
+238
View File
@@ -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
+7
View File
@@ -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
+138
View File
@@ -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))
+1
View File
@@ -0,0 +1 @@
uid://cmqjajhjgffm1
+31
View File
@@ -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/
+18
View File
@@ -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.
+8
View File
@@ -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
+56
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://b826s26d4ud5j
+44
View File
@@ -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
+70
View File
@@ -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
+208
View File
@@ -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))
+1
View File
@@ -0,0 +1 @@
uid://dp4rakv0drj1f
+7
View File
@@ -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"
+34
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://bvhj7f66i6yl4
+34
View File
@@ -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
+64
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
uid://bjvmp7eed1aj0
+31
View File
@@ -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/
+18
View File
@@ -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.
+39
View File
@@ -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
+53
View File
@@ -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