├── addons ├── debug_draw_3d │ ├── libs │ │ ├── .gdignore │ │ ├── libdd3d.linux.editor.x86_64.so │ │ ├── libdd3d.windows.editor.x86_64.dll │ │ ├── libdd3d.web.template_debug.wasm32.wasm │ │ ├── libdd3d.android.template_debug.arm32.so │ │ ├── libdd3d.android.template_debug.arm64.so │ │ ├── libdd3d.android.template_debug.x86_32.so │ │ ├── libdd3d.android.template_debug.x86_64.so │ │ ├── libdd3d.android.template_release.arm32.so │ │ ├── libdd3d.android.template_release.arm64.so │ │ ├── libdd3d.linux.template_release.x86_64.so │ │ ├── libdd3d.web.template_release.wasm32.wasm │ │ ├── libdd3d.android.template_release.x86_32.so │ │ ├── libdd3d.android.template_release.x86_64.so │ │ ├── libdd3d.ios.template_debug.universal.dylib │ │ ├── libdd3d.windows.template_release.x86_64.dll │ │ ├── libdd3d.ios.template_release.universal.dylib │ │ ├── libdd3d.web.template_debug.wasm32.threads.wasm │ │ ├── libdd3d.linux.template_release.x86_64.enabled.so │ │ ├── libdd3d.web.template_release.wasm32.enabled.wasm │ │ ├── libdd3d.web.template_release.wasm32.threads.wasm │ │ ├── libdd3d.windows.template_release.x86_64.enabled.dll │ │ ├── libdd3d.ios.template_release.universal.enabled.dylib │ │ ├── libdd3d.web.template_release.wasm32.threads.enabled.wasm │ │ ├── libdd3d.macos.editor.universal.framework │ │ │ ├── libdd3d.macos.editor.universal.dylib │ │ │ └── Resources │ │ │ │ └── Info.plist │ │ ├── libdd3d.macos.template_release.universal.framework │ │ │ ├── libdd3d.macos.template_release.universal.dylib │ │ │ └── Resources │ │ │ │ └── Info.plist │ │ └── libdd3d.macos.template_release.universal.enabled.framework │ │ │ ├── libdd3d.macos.template_release.universal.enabled.dylib │ │ │ └── Resources │ │ │ └── Info.plist │ ├── debug_draw_3d.gdextension.uid │ └── LICENSE ├── netfox │ ├── netfox.gd.uid │ ├── interpolators.gd.uid │ ├── network-time.gd.uid │ ├── network-events.gd.uid │ ├── rewindable-action.gd.uid │ ├── state-synchronizer.gd.uid │ ├── tick-interpolator.gd.uid │ ├── network-performance.gd.uid │ ├── peer-visibility-filter.gd.uid │ ├── time │ │ ├── network-clocks.gd.uid │ │ ├── network-clock-sample.gd.uid │ │ ├── network-tickrate-handshake.gd.uid │ │ ├── network-clock-sample.gd │ │ ├── network-clocks.gd │ │ └── network-tickrate-handshake.gd │ ├── encoder │ │ ├── diff-history-encoder.gd.uid │ │ ├── redundant-history-encoder.gd.uid │ │ ├── snapshot-history-encoder.gd.uid │ │ ├── snapshot-history-encoder.gd │ │ └── redundant-history-encoder.gd │ ├── network-time-synchronizer.gd.uid │ ├── properties │ │ ├── property-cache.gd.uid │ │ ├── property-config.gd.uid │ │ ├── property-entry.gd.uid │ │ ├── property-snapshot.gd.uid │ │ ├── property-history-buffer.gd.uid │ │ ├── property-cache.gd │ │ ├── property-history-buffer.gd │ │ ├── property-config.gd │ │ ├── property-entry.gd │ │ └── property-snapshot.gd │ ├── rollback │ │ ├── network-rollback.gd.uid │ │ ├── rollback-freshness-store.gd.uid │ │ ├── rollback-synchronizer.gd.uid │ │ ├── composite │ │ │ ├── rollback-history-recorder.gd.uid │ │ │ ├── rollback-history-transmitter.gd.uid │ │ │ └── rollback-history-recorder.gd │ │ └── rollback-freshness-store.gd │ ├── serializers │ │ ├── tickset-serializer.gd.uid │ │ └── tickset-serializer.gd │ ├── plugin.cfg │ ├── icons │ │ ├── rewindable-action.svg.import │ │ ├── tick-interpolator.svg.import │ │ ├── state-synchronizer.svg.import │ │ ├── rollback-synchronizer.svg.import │ │ ├── rollback-synchronizer.svg │ │ ├── state-synchronizer.svg │ │ ├── tick-interpolator.svg │ │ └── rewindable-action.svg │ ├── LICENSE │ ├── README.md │ └── interpolators.gd ├── netfox.internals │ ├── set.gd.uid │ ├── bimap.gd.uid │ ├── logger.gd.uid │ ├── plugin.gd.uid │ ├── editor-utils.gd.uid │ ├── ring-buffer.gd.uid │ ├── history-buffer.gd.uid │ ├── plugin.cfg │ ├── README.md │ ├── ring-buffer.gd │ ├── plugin.gd │ ├── LICENSE │ ├── set.gd │ ├── bimap.gd │ ├── history-buffer.gd │ ├── editor-utils.gd │ └── logger.gd ├── netfox.noray │ ├── noray.gd.uid │ ├── netfox-noray.gd.uid │ ├── packet-handshake.gd.uid │ ├── protocol-handler.gd.uid │ ├── plugin.cfg │ ├── protocol-handler.gd │ ├── LICENSE │ ├── README.md │ └── netfox-noray.gd └── netfox.extras │ ├── base-net-input.gd.uid │ ├── netfox-extras.gd.uid │ ├── network-simulator.gd.uid │ ├── window-tiler.gd.uid │ ├── weapon │ ├── network-weapon.gd.uid │ ├── network-weapon-2d.gd.uid │ ├── network-weapon-3d.gd.uid │ ├── network-weapon-proxy.gd.uid │ ├── network-weapon-hitscan-3d.gd.uid │ ├── network-weapon-proxy.gd │ ├── network-weapon-2d.gd │ ├── network-weapon-3d.gd │ └── network-weapon-hitscan-3d.gd │ ├── physics │ ├── godot_driver_2d.gd.uid │ ├── godot_driver_3d.gd.uid │ ├── physics_driver.gd.uid │ ├── network-rigid-body-2d.gd.uid │ ├── network-rigid-body-3d.gd.uid │ ├── physics-driver-toggles.gd.uid │ ├── rapier_driver_2d.gd.uid.off │ ├── rapier_driver_3d.gd.uid.off │ ├── rapier_driver_2d.gd.off │ ├── rapier_driver_3d.gd.off │ ├── network-rigid-body-2d.gd │ ├── network-rigid-body-3d.gd │ ├── physics-driver-toggles.gd │ ├── physics_driver.gd │ ├── godot_driver_3d.gd │ └── godot_driver_2d.gd │ ├── state-machine │ ├── rewindable-state.gd.uid │ └── rewindable-state-machine.gd.uid │ ├── rewindable-random-number-generator.gd.uid │ ├── plugin.cfg │ ├── base-net-input.gd │ ├── README.md │ ├── icons │ ├── rewindable-state.svg.import │ ├── network-rigid-body-2d.svg.import │ ├── network-rigid-body-3d.svg.import │ ├── rewindable-state-machine.svg.import │ ├── network-rigid-body-2d.svg │ ├── network-rigid-body-3d.svg │ └── rewindable-state-machine.svg │ ├── LICENSE │ └── rewindable-random-number-generator.gd ├── scripts ├── ball.gd.uid ├── game.gd.uid ├── goal.gd.uid ├── main.gd.uid ├── camera_3d.gd.uid ├── car_ai.gd.uid ├── confetti.gd.uid ├── input.gd.uid ├── player.gd.uid ├── player_rb.gd.uid ├── ui_info.gd.uid ├── wheel.gd.uid ├── rapier_driver.gd.uid ├── confetti.gd ├── goal.gd ├── input.gd ├── camera_3d.gd ├── main.gd ├── ball.gd ├── ui_info.gd ├── car_ai.gd ├── game.gd ├── player.gd └── wheel.gd ├── models ├── car.glb ├── car.blend ├── car.blend1 ├── field.glb ├── field.blend ├── field.blend1 ├── car.glb.import ├── field.glb.import ├── car.blend.import └── field.blend.import ├── materials ├── ball_orm.png ├── wall_orm.png ├── wall_sss.png ├── grass_orm.png ├── ball_albedo.png ├── ball_normal.png ├── grass_albedo.png ├── grass_normal.png ├── wall_albedo.png ├── wall_normal.png ├── ball_heightmap.png ├── grass_emission.png ├── grass_heightmap.png ├── wall_heightmap.png ├── car_paint.tres ├── car_window.tres ├── wall.tres ├── wall_orm.png.import ├── ball_albedo.png.import ├── wall_albedo.png.import ├── grass_emission.png.import ├── ball_orm.png.import ├── wall_sss.png.import ├── grass_orm.png.import ├── ball_normal.png.import ├── grass_normal.png.import ├── wall_normal.png.import ├── ball_heightmap.png.import ├── wall_heightmap.png.import ├── grass_heightmap.png.import ├── ball.tres ├── grass_albedo.png.import ├── grass.tres └── wall.ptex ├── scenes ├── car_ai.tscn ├── field.tscn ├── car_model.tscn ├── goal.tscn ├── wheel.tscn ├── ball.tscn ├── player.tscn ├── confetti.tscn └── player_rb.tscn ├── .gitignore ├── README.md ├── icon.svg ├── icon.svg.import ├── LICENSE └── project.godot /addons/debug_draw_3d/libs/.gdignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/ball.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c0s6il6j1o06a 2 | -------------------------------------------------------------------------------- /scripts/game.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c7wmmu5kvc8qq 2 | -------------------------------------------------------------------------------- /scripts/goal.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cau6jor3sbnjm 2 | -------------------------------------------------------------------------------- /scripts/main.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c6j4p68pfwm0u 2 | -------------------------------------------------------------------------------- /scripts/camera_3d.gd.uid: -------------------------------------------------------------------------------- 1 | uid://btvo6f7sbrn0q 2 | -------------------------------------------------------------------------------- /scripts/car_ai.gd.uid: -------------------------------------------------------------------------------- 1 | uid://ddst71w41ihgn 2 | -------------------------------------------------------------------------------- /scripts/confetti.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bxr4tbk4dq8v0 2 | -------------------------------------------------------------------------------- /scripts/input.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cbmc5r8tb2ub7 2 | -------------------------------------------------------------------------------- /scripts/player.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cgwxygf25ypdb 2 | -------------------------------------------------------------------------------- /scripts/player_rb.gd.uid: -------------------------------------------------------------------------------- 1 | uid://lxgmbbyk3nvi 2 | -------------------------------------------------------------------------------- /scripts/ui_info.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bt0tpn5p32eym 2 | -------------------------------------------------------------------------------- /scripts/wheel.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c8tk028y8ex3g 2 | -------------------------------------------------------------------------------- /addons/netfox/netfox.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cjrtjdfhdtjlm 2 | -------------------------------------------------------------------------------- /scripts/rapier_driver.gd.uid: -------------------------------------------------------------------------------- 1 | uid://be16aa8reh80u 2 | -------------------------------------------------------------------------------- /addons/netfox.internals/set.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bjvmp7eed1aj0 2 | -------------------------------------------------------------------------------- /addons/netfox.noray/noray.gd.uid: -------------------------------------------------------------------------------- 1 | uid://gu4bv4tgms3x 2 | -------------------------------------------------------------------------------- /addons/netfox/interpolators.gd.uid: -------------------------------------------------------------------------------- 1 | uid://wsx6ajtb42c2 2 | -------------------------------------------------------------------------------- /addons/netfox/network-time.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cnqdxyjptkfvp 2 | -------------------------------------------------------------------------------- /addons/netfox.internals/bimap.gd.uid: -------------------------------------------------------------------------------- 1 | uid://b826s26d4ud5j 2 | -------------------------------------------------------------------------------- /addons/netfox.internals/logger.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dp4rakv0drj1f 2 | -------------------------------------------------------------------------------- /addons/netfox.internals/plugin.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bvhj7f66i6yl4 2 | -------------------------------------------------------------------------------- /addons/netfox.noray/netfox-noray.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dj8vvyvvf4cl1 2 | -------------------------------------------------------------------------------- /addons/netfox/network-events.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bdmobxxptryj0 2 | -------------------------------------------------------------------------------- /addons/netfox/rewindable-action.gd.uid: -------------------------------------------------------------------------------- 1 | uid://chr4omg2hy3yu 2 | -------------------------------------------------------------------------------- /addons/netfox/state-synchronizer.gd.uid: -------------------------------------------------------------------------------- 1 | uid://3lbngqcexe0l 2 | -------------------------------------------------------------------------------- /addons/netfox/tick-interpolator.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dour8fehaaugp 2 | -------------------------------------------------------------------------------- /addons/netfox.extras/base-net-input.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cpiuf65vuu0oa 2 | -------------------------------------------------------------------------------- /addons/netfox.extras/netfox-extras.gd.uid: -------------------------------------------------------------------------------- 1 | uid://ee1cb3bj7opr 2 | -------------------------------------------------------------------------------- /addons/netfox.extras/network-simulator.gd.uid: -------------------------------------------------------------------------------- 1 | uid://1y2ey1e6qjwr 2 | -------------------------------------------------------------------------------- /addons/netfox.extras/window-tiler.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cmqjajhjgffm1 2 | -------------------------------------------------------------------------------- /addons/netfox.internals/editor-utils.gd.uid: -------------------------------------------------------------------------------- 1 | uid://0ohd4n5yjilb 2 | -------------------------------------------------------------------------------- /addons/netfox.internals/ring-buffer.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cmfbs3cx33lkw 2 | -------------------------------------------------------------------------------- /addons/netfox.noray/packet-handshake.gd.uid: -------------------------------------------------------------------------------- 1 | uid://tt3j25a05n7c 2 | -------------------------------------------------------------------------------- /addons/netfox.noray/protocol-handler.gd.uid: -------------------------------------------------------------------------------- 1 | uid://ou0pi8ebvqsi 2 | -------------------------------------------------------------------------------- /addons/netfox/network-performance.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cqgf7aqmjfoa3 2 | -------------------------------------------------------------------------------- /addons/netfox/peer-visibility-filter.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cp3pkerfb0mxk 2 | -------------------------------------------------------------------------------- /addons/netfox/time/network-clocks.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dnp07ns6cpc70 2 | -------------------------------------------------------------------------------- /addons/netfox.extras/weapon/network-weapon.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cill8uva6thl5 2 | -------------------------------------------------------------------------------- /addons/netfox.internals/history-buffer.gd.uid: -------------------------------------------------------------------------------- 1 | uid://btuvvlw3jkx2e 2 | -------------------------------------------------------------------------------- /addons/netfox/encoder/diff-history-encoder.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dc73evbedbmvs 2 | -------------------------------------------------------------------------------- /addons/netfox/network-time-synchronizer.gd.uid: -------------------------------------------------------------------------------- 1 | uid://5rmn2ik3kjo8 2 | -------------------------------------------------------------------------------- /addons/netfox/properties/property-cache.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cjb41satp02xy 2 | -------------------------------------------------------------------------------- /addons/netfox/properties/property-config.gd.uid: -------------------------------------------------------------------------------- 1 | uid://b58ojksbbkrvh 2 | -------------------------------------------------------------------------------- /addons/netfox/properties/property-entry.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cixo40ot0fqqv 2 | -------------------------------------------------------------------------------- /addons/netfox/properties/property-snapshot.gd.uid: -------------------------------------------------------------------------------- 1 | uid://d2dgbafx6338f 2 | -------------------------------------------------------------------------------- /addons/netfox/rollback/network-rollback.gd.uid: -------------------------------------------------------------------------------- 1 | uid://gas743comroj 2 | -------------------------------------------------------------------------------- /addons/netfox/time/network-clock-sample.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cdmch34dx2uhe 2 | -------------------------------------------------------------------------------- /addons/debug_draw_3d/debug_draw_3d.gdextension.uid: -------------------------------------------------------------------------------- 1 | uid://bfmlrrm0oq77g 2 | -------------------------------------------------------------------------------- /addons/netfox.extras/physics/godot_driver_2d.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c8p8gymii2y5v 2 | -------------------------------------------------------------------------------- /addons/netfox.extras/physics/godot_driver_3d.gd.uid: -------------------------------------------------------------------------------- 1 | uid://wr5ur5dqrkni 2 | -------------------------------------------------------------------------------- /addons/netfox.extras/physics/physics_driver.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dcws6qk4hxtun 2 | -------------------------------------------------------------------------------- /addons/netfox.extras/weapon/network-weapon-2d.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c8pw311ku24we 2 | -------------------------------------------------------------------------------- /addons/netfox.extras/weapon/network-weapon-3d.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c1xyp6tx4hf3p 2 | -------------------------------------------------------------------------------- /addons/netfox.extras/weapon/network-weapon-proxy.gd.uid: -------------------------------------------------------------------------------- 1 | uid://76n8gu7udnum 2 | -------------------------------------------------------------------------------- /addons/netfox/encoder/redundant-history-encoder.gd.uid: -------------------------------------------------------------------------------- 1 | uid://ytn07qahpatv 2 | -------------------------------------------------------------------------------- /addons/netfox/encoder/snapshot-history-encoder.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dwhlghjsgdy4o 2 | -------------------------------------------------------------------------------- /addons/netfox/rollback/rollback-freshness-store.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bjpcq3jhfbqeh 2 | -------------------------------------------------------------------------------- /addons/netfox/rollback/rollback-synchronizer.gd.uid: -------------------------------------------------------------------------------- 1 | uid://d350u8evihs1u 2 | -------------------------------------------------------------------------------- /addons/netfox/serializers/tickset-serializer.gd.uid: -------------------------------------------------------------------------------- 1 | uid://b88vxloxp2wff 2 | -------------------------------------------------------------------------------- /addons/netfox/time/network-tickrate-handshake.gd.uid: -------------------------------------------------------------------------------- 1 | uid://ododt6lhpbvq 2 | -------------------------------------------------------------------------------- /addons/netfox.extras/physics/network-rigid-body-2d.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c8hw7ol53m55g 2 | -------------------------------------------------------------------------------- /addons/netfox.extras/physics/network-rigid-body-3d.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bklxcdyxgbjg2 2 | -------------------------------------------------------------------------------- /addons/netfox.extras/physics/physics-driver-toggles.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bu4ppfj0ovkbr 2 | -------------------------------------------------------------------------------- /addons/netfox.extras/physics/rapier_driver_2d.gd.uid.off: -------------------------------------------------------------------------------- 1 | uid://bcc28dvk0pufe 2 | -------------------------------------------------------------------------------- /addons/netfox.extras/physics/rapier_driver_3d.gd.uid.off: -------------------------------------------------------------------------------- 1 | uid://bo4lplbeepibj 2 | -------------------------------------------------------------------------------- /addons/netfox.extras/state-machine/rewindable-state.gd.uid: -------------------------------------------------------------------------------- 1 | uid://usyufdtn83hc 2 | -------------------------------------------------------------------------------- /addons/netfox/properties/property-history-buffer.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dlkog3qntq03x 2 | -------------------------------------------------------------------------------- /addons/netfox.extras/rewindable-random-number-generator.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cqj8glnpkt5po 2 | -------------------------------------------------------------------------------- /addons/netfox.extras/weapon/network-weapon-hitscan-3d.gd.uid: -------------------------------------------------------------------------------- 1 | uid://daf6uabdxsfwq 2 | -------------------------------------------------------------------------------- /addons/netfox.extras/state-machine/rewindable-state-machine.gd.uid: -------------------------------------------------------------------------------- 1 | uid://byrgwv2o7hstx 2 | -------------------------------------------------------------------------------- /addons/netfox/rollback/composite/rollback-history-recorder.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bn6fsqxbfhihk 2 | -------------------------------------------------------------------------------- /addons/netfox/rollback/composite/rollback-history-transmitter.gd.uid: -------------------------------------------------------------------------------- 1 | uid://ofadsxmvd3e3 2 | -------------------------------------------------------------------------------- /models/car.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/models/car.glb -------------------------------------------------------------------------------- /models/car.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/models/car.blend -------------------------------------------------------------------------------- /models/car.blend1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/models/car.blend1 -------------------------------------------------------------------------------- /models/field.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/models/field.glb -------------------------------------------------------------------------------- /models/field.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/models/field.blend -------------------------------------------------------------------------------- /models/field.blend1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/models/field.blend1 -------------------------------------------------------------------------------- /materials/ball_orm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/materials/ball_orm.png -------------------------------------------------------------------------------- /materials/wall_orm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/materials/wall_orm.png -------------------------------------------------------------------------------- /materials/wall_sss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/materials/wall_sss.png -------------------------------------------------------------------------------- /materials/grass_orm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/materials/grass_orm.png -------------------------------------------------------------------------------- /materials/ball_albedo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/materials/ball_albedo.png -------------------------------------------------------------------------------- /materials/ball_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/materials/ball_normal.png -------------------------------------------------------------------------------- /materials/grass_albedo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/materials/grass_albedo.png -------------------------------------------------------------------------------- /materials/grass_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/materials/grass_normal.png -------------------------------------------------------------------------------- /materials/wall_albedo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/materials/wall_albedo.png -------------------------------------------------------------------------------- /materials/wall_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/materials/wall_normal.png -------------------------------------------------------------------------------- /materials/ball_heightmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/materials/ball_heightmap.png -------------------------------------------------------------------------------- /materials/grass_emission.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/materials/grass_emission.png -------------------------------------------------------------------------------- /materials/grass_heightmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/materials/grass_heightmap.png -------------------------------------------------------------------------------- /materials/wall_heightmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/materials/wall_heightmap.png -------------------------------------------------------------------------------- /scripts/confetti.gd: -------------------------------------------------------------------------------- 1 | extends GPUParticles3D 2 | 3 | func _ready() -> void: 4 | emitting = true 5 | 6 | func _on_finished() -> void: 7 | queue_free() 8 | -------------------------------------------------------------------------------- /addons/debug_draw_3d/libs/libdd3d.linux.editor.x86_64.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/addons/debug_draw_3d/libs/libdd3d.linux.editor.x86_64.so -------------------------------------------------------------------------------- /addons/debug_draw_3d/libs/libdd3d.windows.editor.x86_64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/addons/debug_draw_3d/libs/libdd3d.windows.editor.x86_64.dll -------------------------------------------------------------------------------- /addons/debug_draw_3d/libs/libdd3d.web.template_debug.wasm32.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/addons/debug_draw_3d/libs/libdd3d.web.template_debug.wasm32.wasm -------------------------------------------------------------------------------- /addons/debug_draw_3d/libs/libdd3d.android.template_debug.arm32.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/addons/debug_draw_3d/libs/libdd3d.android.template_debug.arm32.so -------------------------------------------------------------------------------- /addons/debug_draw_3d/libs/libdd3d.android.template_debug.arm64.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/addons/debug_draw_3d/libs/libdd3d.android.template_debug.arm64.so -------------------------------------------------------------------------------- /addons/debug_draw_3d/libs/libdd3d.android.template_debug.x86_32.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/addons/debug_draw_3d/libs/libdd3d.android.template_debug.x86_32.so -------------------------------------------------------------------------------- /addons/debug_draw_3d/libs/libdd3d.android.template_debug.x86_64.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/addons/debug_draw_3d/libs/libdd3d.android.template_debug.x86_64.so -------------------------------------------------------------------------------- /addons/debug_draw_3d/libs/libdd3d.android.template_release.arm32.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/addons/debug_draw_3d/libs/libdd3d.android.template_release.arm32.so -------------------------------------------------------------------------------- /addons/debug_draw_3d/libs/libdd3d.android.template_release.arm64.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/addons/debug_draw_3d/libs/libdd3d.android.template_release.arm64.so -------------------------------------------------------------------------------- /addons/debug_draw_3d/libs/libdd3d.linux.template_release.x86_64.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/addons/debug_draw_3d/libs/libdd3d.linux.template_release.x86_64.so -------------------------------------------------------------------------------- /addons/debug_draw_3d/libs/libdd3d.web.template_release.wasm32.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/addons/debug_draw_3d/libs/libdd3d.web.template_release.wasm32.wasm -------------------------------------------------------------------------------- /addons/debug_draw_3d/libs/libdd3d.android.template_release.x86_32.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/addons/debug_draw_3d/libs/libdd3d.android.template_release.x86_32.so -------------------------------------------------------------------------------- /addons/debug_draw_3d/libs/libdd3d.android.template_release.x86_64.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/addons/debug_draw_3d/libs/libdd3d.android.template_release.x86_64.so -------------------------------------------------------------------------------- /addons/debug_draw_3d/libs/libdd3d.ios.template_debug.universal.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/addons/debug_draw_3d/libs/libdd3d.ios.template_debug.universal.dylib -------------------------------------------------------------------------------- /addons/debug_draw_3d/libs/libdd3d.windows.template_release.x86_64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/addons/debug_draw_3d/libs/libdd3d.windows.template_release.x86_64.dll -------------------------------------------------------------------------------- /addons/netfox/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="netfox" 4 | description="Shared internals for netfox addons" 5 | author="Tamas Galffy and contributors" 6 | version="1.33.1" 7 | script="netfox.gd" 8 | -------------------------------------------------------------------------------- /addons/debug_draw_3d/libs/libdd3d.ios.template_release.universal.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/addons/debug_draw_3d/libs/libdd3d.ios.template_release.universal.dylib -------------------------------------------------------------------------------- /addons/debug_draw_3d/libs/libdd3d.web.template_debug.wasm32.threads.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/addons/debug_draw_3d/libs/libdd3d.web.template_debug.wasm32.threads.wasm -------------------------------------------------------------------------------- /addons/debug_draw_3d/libs/libdd3d.linux.template_release.x86_64.enabled.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/addons/debug_draw_3d/libs/libdd3d.linux.template_release.x86_64.enabled.so -------------------------------------------------------------------------------- /addons/debug_draw_3d/libs/libdd3d.web.template_release.wasm32.enabled.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/addons/debug_draw_3d/libs/libdd3d.web.template_release.wasm32.enabled.wasm -------------------------------------------------------------------------------- /addons/debug_draw_3d/libs/libdd3d.web.template_release.wasm32.threads.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/addons/debug_draw_3d/libs/libdd3d.web.template_release.wasm32.threads.wasm -------------------------------------------------------------------------------- /addons/debug_draw_3d/libs/libdd3d.windows.template_release.x86_64.enabled.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/addons/debug_draw_3d/libs/libdd3d.windows.template_release.x86_64.enabled.dll -------------------------------------------------------------------------------- /addons/debug_draw_3d/libs/libdd3d.ios.template_release.universal.enabled.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/addons/debug_draw_3d/libs/libdd3d.ios.template_release.universal.enabled.dylib -------------------------------------------------------------------------------- /addons/netfox.extras/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="netfox.extras" 4 | description="Game-specific utilities for Netfox" 5 | author="Tamas Galffy and contributors" 6 | version="1.33.1" 7 | script="netfox-extras.gd" 8 | -------------------------------------------------------------------------------- /addons/netfox.internals/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="netfox.internals" 4 | description="Shared internals for netfox addons" 5 | author="Tamas Galffy and contributors" 6 | version="1.33.1" 7 | script="plugin.gd" 8 | -------------------------------------------------------------------------------- /addons/debug_draw_3d/libs/libdd3d.web.template_release.wasm32.threads.enabled.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/addons/debug_draw_3d/libs/libdd3d.web.template_release.wasm32.threads.enabled.wasm -------------------------------------------------------------------------------- /addons/netfox.noray/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="netfox.noray" 4 | description="Bulletproof your connectivity with noray integration for netfox" 5 | author="Tamas Galffy and contributors" 6 | version="1.33.1" 7 | script="netfox-noray.gd" 8 | -------------------------------------------------------------------------------- /materials/car_paint.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="StandardMaterial3D" format=3 uid="uid://b6yeasaavorsj"] 2 | 3 | [resource] 4 | cull_mode = 2 5 | albedo_color = Color(0.913096, 0.098798, 0, 1) 6 | metallic = 0.75 7 | roughness = 0.2 8 | clearcoat_enabled = true 9 | -------------------------------------------------------------------------------- /scenes/car_ai.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://26d7swkp18du"] 2 | 3 | [ext_resource type="Script" uid="uid://ddst71w41ihgn" path="res://scripts/car_ai.gd" id="1_64sb6"] 4 | 5 | [node name="Car_AI" type="Node3D"] 6 | script = ExtResource("1_64sb6") 7 | -------------------------------------------------------------------------------- /addons/debug_draw_3d/libs/libdd3d.macos.editor.universal.framework/libdd3d.macos.editor.universal.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/addons/debug_draw_3d/libs/libdd3d.macos.editor.universal.framework/libdd3d.macos.editor.universal.dylib -------------------------------------------------------------------------------- /addons/netfox.internals/README.md: -------------------------------------------------------------------------------- 1 | # netfox.internals 2 | 3 | Shared utilities for [netfox] addons. Not intended for standalone usage. 4 | 5 | Instead, check out the other addons in the [netfox] repository. 6 | 7 | 8 | [netfox]: https://github.com/foxssake/netfox 9 | -------------------------------------------------------------------------------- /addons/debug_draw_3d/libs/libdd3d.macos.template_release.universal.framework/libdd3d.macos.template_release.universal.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/addons/debug_draw_3d/libs/libdd3d.macos.template_release.universal.framework/libdd3d.macos.template_release.universal.dylib -------------------------------------------------------------------------------- /scripts/goal.gd: -------------------------------------------------------------------------------- 1 | extends Area3D 2 | 3 | func _ready(): 4 | NetworkRollback.on_process_tick.connect(on_rollback_tick) 5 | 6 | func on_rollback_tick(tick : int) -> void: 7 | 8 | for body in get_overlapping_bodies(): 9 | if body is Ball: 10 | body.entered_goal_area(self, tick) 11 | 12 | 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Godot 4+ specific ignores 2 | .godot/ 3 | 4 | # Godot-specific ignores 5 | .import/ 6 | export.cfg 7 | export_credentials.cfg 8 | 9 | # Imported translations (automatically generated from CSV files) 10 | *.translation 11 | 12 | # Mono-specific ignores 13 | .mono/ 14 | data_*/ 15 | mono_crash.*.json 16 | -------------------------------------------------------------------------------- /materials/car_window.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="StandardMaterial3D" format=3 uid="uid://d0ug2v7jto7a4"] 2 | 3 | [resource] 4 | cull_mode = 2 5 | albedo_color = Color(0.051223397, 0.1020442, 0.1688985, 1) 6 | metallic = 0.52 7 | roughness = 0.1 8 | clearcoat_enabled = true 9 | clearcoat = 0.05 10 | clearcoat_roughness = 0.25 11 | -------------------------------------------------------------------------------- /addons/debug_draw_3d/libs/libdd3d.macos.template_release.universal.enabled.framework/libdd3d.macos.template_release.universal.enabled.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertok/godot-rocket-league/HEAD/addons/debug_draw_3d/libs/libdd3d.macos.template_release.universal.enabled.framework/libdd3d.macos.template_release.universal.enabled.dylib -------------------------------------------------------------------------------- /scripts/input.gd: -------------------------------------------------------------------------------- 1 | extends BaseNetInput 2 | class_name PlayerInput 3 | 4 | 5 | var ai_enabled: bool = false 6 | var ai_motion: Vector2 = Vector2() 7 | var ai_jumping: bool = false 8 | var jumping: bool = false 9 | var motion : Vector2 = Vector2.ZERO 10 | 11 | func _gather(): 12 | if is_multiplayer_authority(): 13 | if ai_enabled: 14 | motion = ai_motion 15 | jumping = ai_jumping 16 | else: 17 | motion = Input.get_vector("left", "right", "down", "up") 18 | jumping = Input.is_action_pressed("jump") 19 | -------------------------------------------------------------------------------- /scenes/field.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=4 format=3 uid="uid://u33v8symfbrp"] 2 | 3 | [ext_resource type="PackedScene" uid="uid://th8ke8d8f3dl" path="res://models/field.blend" id="1_6pmjc"] 4 | [ext_resource type="Material" uid="uid://uo7itoedkenq" path="res://materials/wall.tres" id="2_lptdg"] 5 | [ext_resource type="Material" uid="uid://bnqudedf7vlb0" path="res://materials/grass.tres" id="3_7156t"] 6 | 7 | [node name="field" instance=ExtResource("1_6pmjc")] 8 | 9 | [node name="arena" parent="." index="0"] 10 | surface_material_override/0 = ExtResource("2_lptdg") 11 | surface_material_override/1 = ExtResource("3_7156t") 12 | -------------------------------------------------------------------------------- /scenes/car_model.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=4 format=3 uid="uid://cylxynmfbbjwk"] 2 | 3 | [ext_resource type="PackedScene" uid="uid://mwlmhv68da6e" path="res://models/car.blend" id="1_bg842"] 4 | [ext_resource type="Material" uid="uid://b6yeasaavorsj" path="res://materials/car_paint.tres" id="2_ifs7h"] 5 | [ext_resource type="Material" uid="uid://d0ug2v7jto7a4" path="res://materials/car_window.tres" id="3_hhld5"] 6 | 7 | [node name="car" instance=ExtResource("1_bg842")] 8 | 9 | [node name="Car" parent="." index="0"] 10 | surface_material_override/0 = ExtResource("2_ifs7h") 11 | surface_material_override/1 = ExtResource("3_hhld5") 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # godot-rocket-league 2 | 3 | Network physics with rollback test using Godot and netfox 4 | 5 | Requires [Godot with Stepping PR](https://github.com/albertok/godot) or [Blazium](https://blazium.app/) 6 | 7 | Alternatily build Godot manually with [this pull request](https://github.com/godotengine/godot/pull/76462) applied. See the [Godot documentation](https://docs.godotengine.org/en/stable/contributing/development/compiling/index.html) for compilation instructions. 8 | 9 | The project will auto connect to itself when run. Just set as many run instances as needed. 10 | 11 | 12 | https://github.com/user-attachments/assets/b5319a5b-7460-4949-93e0-223572937ac2 13 | 14 | -------------------------------------------------------------------------------- /addons/netfox.extras/base-net-input.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | class_name BaseNetInput 3 | 4 | ## Base class for Input nodes used with rollback. 5 | 6 | func _ready(): 7 | NetworkTime.before_tick_loop.connect(func(): 8 | if is_multiplayer_authority(): 9 | _gather() 10 | ) 11 | 12 | ## Method for gathering input. 13 | ## 14 | ## This method is supposed to be overridden with your input logic. The input 15 | ## data itself may be gathered outside of this method ( e.g. gathering it over 16 | ## multiple _process calls ), but this is the point where the input variables 17 | ## must be set. 18 | ## 19 | ## [i]Note:[/i] This is only called for the local player's input nodes. 20 | func _gather(): 21 | pass 22 | -------------------------------------------------------------------------------- /addons/netfox.extras/physics/rapier_driver_2d.gd.off: -------------------------------------------------------------------------------- 1 | extends PhysicsDriver 2 | 3 | class_name RapierDriver2D 4 | 5 | func _init_physics_space() -> void: 6 | physics_space = get_viewport().world_2d.space 7 | PhysicsServer2D.space_set_active(physics_space, false) 8 | 9 | func _physics_step(delta) -> void: 10 | RapierPhysicsServer2D.space_step(physics_space, delta) 11 | RapierPhysicsServer2D.space_flush_queries(physics_space) 12 | 13 | func _snapshot_space(tick: int) -> void: 14 | snapshots[tick] = RapierPhysicsServer2D.export_binary(physics_space) 15 | 16 | func _rollback_space(tick) -> void: 17 | if snapshots.has(tick): 18 | RapierPhysicsServer2D.import_binary(physics_space, snapshots[tick]) 19 | -------------------------------------------------------------------------------- /addons/netfox.extras/physics/rapier_driver_3d.gd.off: -------------------------------------------------------------------------------- 1 | extends PhysicsDriver 2 | 3 | class_name RapierDriver3D 4 | 5 | func _init_physics_space() -> void: 6 | physics_space = get_viewport().world_3d.space 7 | PhysicsServer3D.space_set_active(physics_space, false) 8 | 9 | func _physics_step(delta) -> void: 10 | RapierPhysicsServer3D.space_step(physics_space, delta) 11 | RapierPhysicsServer3D.space_flush_queries(physics_space) 12 | 13 | func _snapshot_space(tick: int) -> void: 14 | snapshots[tick] = RapierPhysicsServer3D.export_binary(physics_space) 15 | 16 | func _rollback_space(tick) -> void: 17 | if snapshots.has(tick): 18 | RapierPhysicsServer3D.import_binary(physics_space, snapshots[tick]) 19 | -------------------------------------------------------------------------------- /addons/netfox.internals/ring-buffer.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name _RingBuffer 3 | 4 | var _data: Array 5 | var _capacity: int 6 | var _size: int = 0 7 | var _head: int = 0 8 | 9 | func _init(p_capacity: int): 10 | _capacity = p_capacity 11 | _data = [] 12 | _data.resize(p_capacity) 13 | 14 | func push(item): 15 | _data[_head] = item 16 | 17 | _size += 1 18 | _head = (_head + 1) % _capacity 19 | 20 | func get_data() -> Array: 21 | if _size < _capacity: 22 | return _data.slice(0, _size) 23 | else: 24 | return _data 25 | 26 | func size() -> int: 27 | return _size 28 | 29 | func is_empty() -> bool: 30 | return _size == 0 31 | 32 | func clear(): 33 | _size = 0 34 | _head = 0 35 | -------------------------------------------------------------------------------- /materials/wall.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="StandardMaterial3D" load_steps=3 format=3 uid="uid://uo7itoedkenq"] 2 | 3 | [ext_resource type="Texture2D" uid="uid://c5mr1vlj31pmx" path="res://materials/wall_albedo.png" id="1"] 4 | [ext_resource type="Texture2D" uid="uid://cx2hrsvj4gts6" path="res://materials/wall_normal.png" id="3"] 5 | 6 | [resource] 7 | transparency = 1 8 | blend_mode = 1 9 | cull_mode = 2 10 | albedo_color = Color(1, 1, 1, 0.258824) 11 | albedo_texture = ExtResource("1") 12 | metallic_specular = 0.1 13 | roughness = 0.6 14 | normal_enabled = true 15 | normal_texture = ExtResource("3") 16 | clearcoat_enabled = true 17 | anisotropy = -0.66 18 | refraction_scale = 0.25 19 | uv1_triplanar = true 20 | -------------------------------------------------------------------------------- /addons/netfox/time/network-clock-sample.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name NetworkClockSample 3 | 4 | var ping_sent: float 5 | var ping_received: float 6 | var pong_sent: float 7 | var pong_received: float 8 | 9 | func get_rtt() -> float: 10 | return pong_received - ping_sent 11 | 12 | func get_offset() -> float: 13 | # See: https://datatracker.ietf.org/doc/html/rfc5905#section-8 14 | # See: https://support.huawei.com/enterprise/en/doc/EDOC1100278232/729da750/ntp-fundamentals 15 | return ((ping_received - ping_sent) + (pong_sent - pong_received)) / 2. 16 | 17 | func _to_string() -> String: 18 | return "(theta=%.2fms; delta=%.2fms; t1=%.4fs; t2=%.4fs; t3=%.4fs; t4=%.4fs)" % [ 19 | get_offset() * 1000., get_rtt() * 1000., 20 | ping_sent, ping_received, pong_sent, pong_received 21 | ] 22 | -------------------------------------------------------------------------------- /addons/netfox/properties/property-cache.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name PropertyCache 3 | 4 | var root: Node 5 | var _cache: Dictionary = {} 6 | 7 | static var _logger: _NetfoxLogger = _NetfoxLogger.for_netfox("PropertyCache") 8 | 9 | func _init(p_root: Node): 10 | root = p_root 11 | 12 | func get_entry(path: String) -> PropertyEntry: 13 | if not _cache.has(path): 14 | var parsed = PropertyEntry.parse(root, path) 15 | if not parsed.is_valid(): 16 | _logger.warning("Invalid property path: %s", [path]) 17 | _cache[path] = parsed 18 | return _cache[path] 19 | 20 | func properties() -> Array: 21 | var result: Array[PropertyEntry] 22 | # Can be slow, but no other way to do this with type-safety 23 | # See: https://github.com/godotengine/godot/issues/72627 24 | result.assign(_cache.values()) 25 | return result 26 | 27 | func clear(): 28 | _cache.clear() 29 | -------------------------------------------------------------------------------- /addons/netfox/rollback/rollback-freshness-store.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name RollbackFreshnessStore 3 | 4 | ## This class tracks nodes and whether they have processed any given tick during 5 | ## a rollback. 6 | 7 | # TODO: _Set 8 | var _data: Dictionary = {} 9 | 10 | func is_fresh(node: Node, tick: int) -> bool: 11 | if not _data.has(tick): 12 | return true 13 | 14 | if not _data[tick].has(node): 15 | return true 16 | 17 | return false 18 | 19 | func notify_processed(node: Node, tick: int) -> void: 20 | if not _data.has(tick): 21 | _data[tick] = {} 22 | 23 | _data[tick][node] = true 24 | 25 | func trim() -> void: 26 | while not _data.is_empty(): 27 | var earliest_tick := _data.keys().min() 28 | if earliest_tick < NetworkRollback.history_start: 29 | _data.erase(earliest_tick) 30 | else: 31 | break 32 | 33 | func clear(): 34 | _data.clear() 35 | -------------------------------------------------------------------------------- /addons/netfox.extras/README.md: -------------------------------------------------------------------------------- 1 | # netfox.extras 2 | 3 | High-level, game-specific extras for [netfox]! 4 | 5 | ## Features 6 | 7 | * 🔫 Networked weapons 8 | * ⌨️ Rollback-aware base class for input 9 | 10 | ## Overview 11 | 12 | This addon is pretty much a catch-all project for things that are useful, but 13 | are not core to netfox. 14 | 15 | ## Install 16 | 17 | See the root [README](../../README.md). 18 | 19 | ## Usage 20 | 21 | See the [docs](https://foxssake.github.io/netfox/). 22 | 23 | ## License 24 | 25 | netfox.extras is under the [MIT license](LICENSE). 26 | 27 | ## Issues 28 | 29 | In case of any issues, comments, or questions, please feel free to [open an issue]! 30 | 31 | [netfox]: https://github.com/foxssake/netfox 32 | [source]: https://github.com/foxssake/netfox/archive/refs/heads/main.zip 33 | [open an issue]: https://github.com/foxssake/netfox/issues 34 | 35 | -------------------------------------------------------------------------------- /scenes/goal.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=4 format=3 uid="uid://bg5hxbj5ib07n"] 2 | 3 | [ext_resource type="Script" uid="uid://cau6jor3sbnjm" path="res://scripts/goal.gd" id="1_jbojv"] 4 | 5 | [sub_resource type="BoxShape3D" id="BoxShape3D_tefeu"] 6 | size = Vector3(15, 7, 4.4282227) 7 | 8 | [sub_resource type="BoxMesh" id="BoxMesh_xl315"] 9 | size = Vector3(15.365, 7.25, 4) 10 | 11 | [node name="Area3D" type="Area3D"] 12 | collision_layer = 8 13 | collision_mask = 4 14 | script = ExtResource("1_jbojv") 15 | 16 | [node name="CollisionShape3D" type="CollisionShape3D" parent="."] 17 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1.7141113) 18 | shape = SubResource("BoxShape3D_tefeu") 19 | 20 | [node name="MeshInstance3D" type="MeshInstance3D" parent="."] 21 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1.6367207) 22 | visible = false 23 | mesh = SubResource("BoxMesh_xl315") 24 | -------------------------------------------------------------------------------- /scripts/camera_3d.gd: -------------------------------------------------------------------------------- 1 | extends Camera3D 2 | 3 | @export var target: Node3D 4 | @export var height: float = 1.5 5 | @export var target_distance: float = 5.0 6 | @export var smooth_speed: float = 5.0 7 | 8 | var current_position: Vector3 9 | var current_rotation: Basis 10 | 11 | func _process(delta): 12 | if !target: 13 | return 14 | 15 | var target_pos = target.position 16 | var target_direction = target.global_transform.basis.z 17 | var desired_position = target_pos - target_direction * target_distance 18 | 19 | desired_position.y = target_pos.y + height 20 | current_position = current_position.lerp(desired_position, smooth_speed * delta) 21 | position = current_position 22 | 23 | var desired_rotation = Transform3D().looking_at(target_pos - position, Vector3.UP).basis 24 | current_rotation = current_rotation.slerp(desired_rotation, 2. * smooth_speed * delta) 25 | global_transform.basis = current_rotation 26 | -------------------------------------------------------------------------------- /icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scenes/wheel.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=4 format=3 uid="uid://d2sn826pqsfs5"] 2 | 3 | [ext_resource type="Script" uid="uid://c8tk028y8ex3g" path="res://scripts/wheel.gd" id="1_hm1pg"] 4 | 5 | [sub_resource type="CylinderMesh" id="CylinderMesh_hm1pg"] 6 | 7 | [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_hm1pg"] 8 | albedo_color = Color(0.0078223, 0.00212023, 0.00175743, 1) 9 | metallic = 0.18 10 | roughness = 0.67 11 | 12 | [node name="RayWheel" type="RayCast3D"] 13 | script = ExtResource("1_hm1pg") 14 | is_front_wheel = true 15 | metadata/_custom_type_script = ExtResource("1_hm1pg") 16 | 17 | [node name="Wheel" type="Node3D" parent="."] 18 | 19 | [node name="MeshInstance3D" type="MeshInstance3D" parent="Wheel"] 20 | transform = Transform3D(-2.18557e-08, 0.1, -2.18557e-08, 0, -4.37114e-09, -0.5, -0.5, -4.37114e-09, 9.55343e-16, 0, 0, 0) 21 | mesh = SubResource("CylinderMesh_hm1pg") 22 | surface_material_override/0 = SubResource("StandardMaterial3D_hm1pg") 23 | -------------------------------------------------------------------------------- /addons/netfox/properties/property-history-buffer.gd: -------------------------------------------------------------------------------- 1 | extends _HistoryBuffer 2 | class_name _PropertyHistoryBuffer 3 | 4 | func get_snapshot(tick: int) -> _PropertySnapshot: 5 | if _buffer.has(tick): 6 | return _buffer[tick] 7 | else: 8 | return _PropertySnapshot.new() 9 | 10 | func set_snapshot(tick: int, data) -> void: 11 | if data is Dictionary: 12 | var snapshot := _PropertySnapshot.from_dictionary(data) 13 | super(tick, snapshot) 14 | elif data is _PropertySnapshot: 15 | super(tick, data) 16 | else: 17 | push_error("Data not a PropertSnapshot! %s" % [data]) 18 | 19 | func get_history(tick: int) -> _PropertySnapshot: 20 | var snapshot = super(tick) 21 | 22 | return snapshot if snapshot else _PropertySnapshot.new() 23 | 24 | func trim(earliest_tick_to_keep: int = NetworkRollback.history_start) -> void: 25 | super(earliest_tick_to_keep) 26 | 27 | func merge(data: _PropertySnapshot, tick:int) -> void: 28 | set_snapshot(tick, get_snapshot(tick).merge(data)) 29 | -------------------------------------------------------------------------------- /materials/wall_orm.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://do01ofepfaxw7" 6 | path.s3tc="res://.godot/imported/wall_orm.png-9cb994fd10e71375a45c01f4089091a6.s3tc.ctex" 7 | metadata={ 8 | "imported_formats": ["s3tc_bptc"], 9 | "vram_texture": true 10 | } 11 | 12 | [deps] 13 | 14 | source_file="res://materials/wall_orm.png" 15 | dest_files=["res://.godot/imported/wall_orm.png-9cb994fd10e71375a45c01f4089091a6.s3tc.ctex"] 16 | 17 | [params] 18 | 19 | compress/mode=2 20 | compress/high_quality=false 21 | compress/lossy_quality=0.7 22 | compress/hdr_compression=1 23 | compress/normal_map=0 24 | compress/channel_pack=0 25 | mipmaps/generate=true 26 | mipmaps/limit=-1 27 | roughness/mode=0 28 | roughness/src_normal="" 29 | process/fix_alpha_border=true 30 | process/premult_alpha=false 31 | process/normal_map_invert_y=false 32 | process/hdr_as_srgb=false 33 | process/hdr_clamp_exposure=false 34 | process/size_limit=0 35 | detect_3d/compress_to=0 36 | -------------------------------------------------------------------------------- /materials/ball_albedo.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://cc4mhe4g0ymsv" 6 | path.s3tc="res://.godot/imported/ball_albedo.png-23bc2e478e4a5041db669e7b2c7af09b.s3tc.ctex" 7 | metadata={ 8 | "imported_formats": ["s3tc_bptc"], 9 | "vram_texture": true 10 | } 11 | 12 | [deps] 13 | 14 | source_file="res://materials/ball_albedo.png" 15 | dest_files=["res://.godot/imported/ball_albedo.png-23bc2e478e4a5041db669e7b2c7af09b.s3tc.ctex"] 16 | 17 | [params] 18 | 19 | compress/mode=2 20 | compress/high_quality=false 21 | compress/lossy_quality=0.7 22 | compress/hdr_compression=1 23 | compress/normal_map=0 24 | compress/channel_pack=0 25 | mipmaps/generate=true 26 | mipmaps/limit=-1 27 | roughness/mode=0 28 | roughness/src_normal="" 29 | process/fix_alpha_border=true 30 | process/premult_alpha=false 31 | process/normal_map_invert_y=false 32 | process/hdr_as_srgb=false 33 | process/hdr_clamp_exposure=false 34 | process/size_limit=0 35 | detect_3d/compress_to=0 36 | -------------------------------------------------------------------------------- /materials/wall_albedo.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://c5mr1vlj31pmx" 6 | path.s3tc="res://.godot/imported/wall_albedo.png-92df8abddd5c146147a53f5a4eb63c26.s3tc.ctex" 7 | metadata={ 8 | "imported_formats": ["s3tc_bptc"], 9 | "vram_texture": true 10 | } 11 | 12 | [deps] 13 | 14 | source_file="res://materials/wall_albedo.png" 15 | dest_files=["res://.godot/imported/wall_albedo.png-92df8abddd5c146147a53f5a4eb63c26.s3tc.ctex"] 16 | 17 | [params] 18 | 19 | compress/mode=2 20 | compress/high_quality=false 21 | compress/lossy_quality=0.7 22 | compress/hdr_compression=1 23 | compress/normal_map=0 24 | compress/channel_pack=0 25 | mipmaps/generate=true 26 | mipmaps/limit=-1 27 | roughness/mode=0 28 | roughness/src_normal="" 29 | process/fix_alpha_border=true 30 | process/premult_alpha=false 31 | process/normal_map_invert_y=false 32 | process/hdr_as_srgb=false 33 | process/hdr_clamp_exposure=false 34 | process/size_limit=0 35 | detect_3d/compress_to=0 36 | -------------------------------------------------------------------------------- /icon.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://j8o8c4022asy" 6 | path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://icon.svg" 14 | dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /materials/grass_emission.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://c5xh5cxrpjwps" 6 | path.s3tc="res://.godot/imported/grass_emission.png-66e51b9b1c972bc85fe0f9dea90941b9.s3tc.ctex" 7 | metadata={ 8 | "imported_formats": ["s3tc_bptc"], 9 | "vram_texture": true 10 | } 11 | 12 | [deps] 13 | 14 | source_file="res://materials/grass_emission.png" 15 | dest_files=["res://.godot/imported/grass_emission.png-66e51b9b1c972bc85fe0f9dea90941b9.s3tc.ctex"] 16 | 17 | [params] 18 | 19 | compress/mode=2 20 | compress/high_quality=false 21 | compress/lossy_quality=0.7 22 | compress/hdr_compression=1 23 | compress/normal_map=0 24 | compress/channel_pack=0 25 | mipmaps/generate=true 26 | mipmaps/limit=-1 27 | roughness/mode=0 28 | roughness/src_normal="" 29 | process/fix_alpha_border=true 30 | process/premult_alpha=false 31 | process/normal_map_invert_y=false 32 | process/hdr_as_srgb=false 33 | process/hdr_clamp_exposure=false 34 | process/size_limit=0 35 | detect_3d/compress_to=0 36 | -------------------------------------------------------------------------------- /materials/ball_orm.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://dyh6m3p6ofsrk" 6 | path.s3tc="res://.godot/imported/ball_orm.png-23e20c1f54827495554478d51b200a69.s3tc.ctex" 7 | metadata={ 8 | "imported_formats": ["s3tc_bptc"], 9 | "vram_texture": true 10 | } 11 | 12 | [deps] 13 | 14 | source_file="res://materials/ball_orm.png" 15 | dest_files=["res://.godot/imported/ball_orm.png-23e20c1f54827495554478d51b200a69.s3tc.ctex"] 16 | 17 | [params] 18 | 19 | compress/mode=2 20 | compress/high_quality=false 21 | compress/lossy_quality=0.7 22 | compress/hdr_compression=1 23 | compress/normal_map=0 24 | compress/channel_pack=0 25 | mipmaps/generate=true 26 | mipmaps/limit=-1 27 | roughness/mode=8 28 | roughness/src_normal="res://materials/ball_normal.png" 29 | process/fix_alpha_border=true 30 | process/premult_alpha=false 31 | process/normal_map_invert_y=false 32 | process/hdr_as_srgb=false 33 | process/hdr_clamp_exposure=false 34 | process/size_limit=0 35 | detect_3d/compress_to=0 36 | -------------------------------------------------------------------------------- /materials/wall_sss.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://bhlkiifjxmqv0" 6 | path.s3tc="res://.godot/imported/wall_sss.png-2f58901e00eae34ea094f9c92f8bdef3.s3tc.ctex" 7 | metadata={ 8 | "imported_formats": ["s3tc_bptc"], 9 | "vram_texture": true 10 | } 11 | 12 | [deps] 13 | 14 | source_file="res://materials/wall_sss.png" 15 | dest_files=["res://.godot/imported/wall_sss.png-2f58901e00eae34ea094f9c92f8bdef3.s3tc.ctex"] 16 | 17 | [params] 18 | 19 | compress/mode=2 20 | compress/high_quality=false 21 | compress/lossy_quality=0.7 22 | compress/hdr_compression=1 23 | compress/normal_map=0 24 | compress/channel_pack=0 25 | mipmaps/generate=true 26 | mipmaps/limit=-1 27 | roughness/mode=8 28 | roughness/src_normal="res://materials/wall_normal.png" 29 | process/fix_alpha_border=true 30 | process/premult_alpha=false 31 | process/normal_map_invert_y=false 32 | process/hdr_as_srgb=false 33 | process/hdr_clamp_exposure=false 34 | process/size_limit=0 35 | detect_3d/compress_to=0 36 | -------------------------------------------------------------------------------- /addons/netfox.noray/protocol-handler.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name NorayProtocolHandler 3 | ## This class parses incoming data from noray's protocol. 4 | ## 5 | ## Unless you're writing your own noray integration, [Noray] should cover most 6 | ## use cases. 7 | 8 | ## Emitted for every command parsed during a [method ingest] call. 9 | signal on_command(command: String, data: String) 10 | 11 | var _strbuf: String = "" 12 | 13 | ## Resets the parser. 14 | func reset(): 15 | _strbuf = "" 16 | 17 | ## Parse an incoming piece of data. 18 | func ingest(data: String): 19 | _strbuf += data 20 | if not _strbuf.contains("\n"): 21 | return 22 | 23 | var idx = _strbuf.rfind("\n") 24 | var lines = _strbuf.substr(0, idx).split("\n", false) 25 | _strbuf = _strbuf.erase(0, idx + 1) 26 | 27 | for line in lines: 28 | if not line.contains(" "): 29 | on_command.emit(line, "") 30 | else: 31 | var parts = line.split(" ") 32 | var command = parts[0] 33 | var param = parts[1] 34 | on_command.emit(command, param) 35 | -------------------------------------------------------------------------------- /materials/grass_orm.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://dys4dhkhk3pw4" 6 | path.s3tc="res://.godot/imported/grass_orm.png-84269e06661ad64981d8df14f60aab6f.s3tc.ctex" 7 | metadata={ 8 | "imported_formats": ["s3tc_bptc"], 9 | "vram_texture": true 10 | } 11 | 12 | [deps] 13 | 14 | source_file="res://materials/grass_orm.png" 15 | dest_files=["res://.godot/imported/grass_orm.png-84269e06661ad64981d8df14f60aab6f.s3tc.ctex"] 16 | 17 | [params] 18 | 19 | compress/mode=2 20 | compress/high_quality=false 21 | compress/lossy_quality=0.7 22 | compress/hdr_compression=1 23 | compress/normal_map=0 24 | compress/channel_pack=0 25 | mipmaps/generate=true 26 | mipmaps/limit=-1 27 | roughness/mode=8 28 | roughness/src_normal="res://materials/grass_normal.png" 29 | process/fix_alpha_border=true 30 | process/premult_alpha=false 31 | process/normal_map_invert_y=false 32 | process/hdr_as_srgb=false 33 | process/hdr_clamp_exposure=false 34 | process/size_limit=0 35 | detect_3d/compress_to=0 36 | -------------------------------------------------------------------------------- /materials/ball_normal.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://d3u7ar71ppy7g" 6 | path.s3tc="res://.godot/imported/ball_normal.png-252e7d9ccda3b792119d325f025d4a29.s3tc.ctex" 7 | metadata={ 8 | "imported_formats": ["s3tc_bptc"], 9 | "vram_texture": true 10 | } 11 | 12 | [deps] 13 | 14 | source_file="res://materials/ball_normal.png" 15 | dest_files=["res://.godot/imported/ball_normal.png-252e7d9ccda3b792119d325f025d4a29.s3tc.ctex"] 16 | 17 | [params] 18 | 19 | compress/mode=2 20 | compress/high_quality=false 21 | compress/lossy_quality=0.7 22 | compress/hdr_compression=1 23 | compress/normal_map=1 24 | compress/channel_pack=0 25 | mipmaps/generate=true 26 | mipmaps/limit=-1 27 | roughness/mode=1 28 | roughness/src_normal="res://materials/ball_normal.png" 29 | process/fix_alpha_border=true 30 | process/premult_alpha=false 31 | process/normal_map_invert_y=false 32 | process/hdr_as_srgb=false 33 | process/hdr_clamp_exposure=false 34 | process/size_limit=0 35 | detect_3d/compress_to=0 36 | -------------------------------------------------------------------------------- /materials/grass_normal.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://mr7pu5igx84m" 6 | path.s3tc="res://.godot/imported/grass_normal.png-9ca7d478cc42ea67cb84e4cb5cb3502c.s3tc.ctex" 7 | metadata={ 8 | "imported_formats": ["s3tc_bptc"], 9 | "vram_texture": true 10 | } 11 | 12 | [deps] 13 | 14 | source_file="res://materials/grass_normal.png" 15 | dest_files=["res://.godot/imported/grass_normal.png-9ca7d478cc42ea67cb84e4cb5cb3502c.s3tc.ctex"] 16 | 17 | [params] 18 | 19 | compress/mode=2 20 | compress/high_quality=false 21 | compress/lossy_quality=0.7 22 | compress/hdr_compression=1 23 | compress/normal_map=1 24 | compress/channel_pack=0 25 | mipmaps/generate=true 26 | mipmaps/limit=-1 27 | roughness/mode=1 28 | roughness/src_normal="res://materials/grass_normal.png" 29 | process/fix_alpha_border=true 30 | process/premult_alpha=false 31 | process/normal_map_invert_y=false 32 | process/hdr_as_srgb=false 33 | process/hdr_clamp_exposure=false 34 | process/size_limit=0 35 | detect_3d/compress_to=0 36 | -------------------------------------------------------------------------------- /materials/wall_normal.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://cx2hrsvj4gts6" 6 | path.s3tc="res://.godot/imported/wall_normal.png-76cd383eb0ce2ba8c55283da7c556b73.s3tc.ctex" 7 | metadata={ 8 | "imported_formats": ["s3tc_bptc"], 9 | "vram_texture": true 10 | } 11 | 12 | [deps] 13 | 14 | source_file="res://materials/wall_normal.png" 15 | dest_files=["res://.godot/imported/wall_normal.png-76cd383eb0ce2ba8c55283da7c556b73.s3tc.ctex"] 16 | 17 | [params] 18 | 19 | compress/mode=2 20 | compress/high_quality=false 21 | compress/lossy_quality=0.7 22 | compress/hdr_compression=1 23 | compress/normal_map=1 24 | compress/channel_pack=0 25 | mipmaps/generate=true 26 | mipmaps/limit=-1 27 | roughness/mode=1 28 | roughness/src_normal="res://materials/wall_normal.png" 29 | process/fix_alpha_border=true 30 | process/premult_alpha=false 31 | process/normal_map_invert_y=false 32 | process/hdr_as_srgb=false 33 | process/hdr_clamp_exposure=false 34 | process/size_limit=0 35 | detect_3d/compress_to=0 36 | -------------------------------------------------------------------------------- /materials/ball_heightmap.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://6y5nxpmq7pwg" 6 | path.s3tc="res://.godot/imported/ball_heightmap.png-f5c751a3e170d9d66278679e5d878fab.s3tc.ctex" 7 | metadata={ 8 | "imported_formats": ["s3tc_bptc"], 9 | "vram_texture": true 10 | } 11 | 12 | [deps] 13 | 14 | source_file="res://materials/ball_heightmap.png" 15 | dest_files=["res://.godot/imported/ball_heightmap.png-f5c751a3e170d9d66278679e5d878fab.s3tc.ctex"] 16 | 17 | [params] 18 | 19 | compress/mode=2 20 | compress/high_quality=false 21 | compress/lossy_quality=0.7 22 | compress/hdr_compression=1 23 | compress/normal_map=0 24 | compress/channel_pack=0 25 | mipmaps/generate=true 26 | mipmaps/limit=-1 27 | roughness/mode=7 28 | roughness/src_normal="res://materials/ball_normal.png" 29 | process/fix_alpha_border=true 30 | process/premult_alpha=false 31 | process/normal_map_invert_y=false 32 | process/hdr_as_srgb=false 33 | process/hdr_clamp_exposure=false 34 | process/size_limit=0 35 | detect_3d/compress_to=0 36 | -------------------------------------------------------------------------------- /materials/wall_heightmap.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://cl1qimhntjbv0" 6 | path.s3tc="res://.godot/imported/wall_heightmap.png-200fd14a8b01844a53e52bae6eebb625.s3tc.ctex" 7 | metadata={ 8 | "imported_formats": ["s3tc_bptc"], 9 | "vram_texture": true 10 | } 11 | 12 | [deps] 13 | 14 | source_file="res://materials/wall_heightmap.png" 15 | dest_files=["res://.godot/imported/wall_heightmap.png-200fd14a8b01844a53e52bae6eebb625.s3tc.ctex"] 16 | 17 | [params] 18 | 19 | compress/mode=2 20 | compress/high_quality=false 21 | compress/lossy_quality=0.7 22 | compress/hdr_compression=1 23 | compress/normal_map=0 24 | compress/channel_pack=0 25 | mipmaps/generate=true 26 | mipmaps/limit=-1 27 | roughness/mode=7 28 | roughness/src_normal="res://materials/wall_normal.png" 29 | process/fix_alpha_border=true 30 | process/premult_alpha=false 31 | process/normal_map_invert_y=false 32 | process/hdr_as_srgb=false 33 | process/hdr_clamp_exposure=false 34 | process/size_limit=0 35 | detect_3d/compress_to=0 36 | -------------------------------------------------------------------------------- /materials/grass_heightmap.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://cx8yeg0tx1e4h" 6 | path.s3tc="res://.godot/imported/grass_heightmap.png-1b10dd55170cbc5fe138803a9260c0ce.s3tc.ctex" 7 | metadata={ 8 | "imported_formats": ["s3tc_bptc"], 9 | "vram_texture": true 10 | } 11 | 12 | [deps] 13 | 14 | source_file="res://materials/grass_heightmap.png" 15 | dest_files=["res://.godot/imported/grass_heightmap.png-1b10dd55170cbc5fe138803a9260c0ce.s3tc.ctex"] 16 | 17 | [params] 18 | 19 | compress/mode=2 20 | compress/high_quality=false 21 | compress/lossy_quality=0.7 22 | compress/hdr_compression=1 23 | compress/normal_map=0 24 | compress/channel_pack=0 25 | mipmaps/generate=true 26 | mipmaps/limit=-1 27 | roughness/mode=7 28 | roughness/src_normal="res://materials/grass_normal.png" 29 | process/fix_alpha_border=true 30 | process/premult_alpha=false 31 | process/normal_map_invert_y=false 32 | process/hdr_as_srgb=false 33 | process/hdr_clamp_exposure=false 34 | process/size_limit=0 35 | detect_3d/compress_to=0 36 | -------------------------------------------------------------------------------- /addons/netfox.internals/plugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | var SETTINGS = [ 5 | _NetfoxLogger.make_setting("netfox/logging/log_level") 6 | ] 7 | 8 | func _enter_tree(): 9 | for setting in SETTINGS: 10 | add_setting(setting) 11 | 12 | func _exit_tree(): 13 | if ProjectSettings.get_setting("netfox/general/clear_settings", false): 14 | for setting in SETTINGS: 15 | remove_setting(setting) 16 | 17 | func add_setting(setting: Dictionary): 18 | if ProjectSettings.has_setting(setting.name): 19 | return 20 | 21 | ProjectSettings.set_setting(setting.name, setting.value) 22 | ProjectSettings.set_initial_value(setting.name, setting.value) 23 | ProjectSettings.add_property_info({ 24 | "name": setting.get("name"), 25 | "type": setting.get("type"), 26 | "hint": setting.get("hint", PROPERTY_HINT_NONE), 27 | "hint_string": setting.get("hint_string", "") 28 | }) 29 | 30 | func remove_setting(setting: Dictionary): 31 | if not ProjectSettings.has_setting(setting.name): 32 | return 33 | 34 | ProjectSettings.clear(setting.name) 35 | -------------------------------------------------------------------------------- /materials/ball.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="StandardMaterial3D" load_steps=5 format=3 uid="uid://bp1lxtywe5u3c"] 2 | 3 | [ext_resource type="Texture2D" uid="uid://cc4mhe4g0ymsv" path="res://materials/ball_albedo.png" id="1"] 4 | [ext_resource type="Texture2D" uid="uid://dyh6m3p6ofsrk" path="res://materials/ball_orm.png" id="2"] 5 | [ext_resource type="Texture2D" uid="uid://d3u7ar71ppy7g" path="res://materials/ball_normal.png" id="3"] 6 | [ext_resource type="Texture2D" uid="uid://6y5nxpmq7pwg" path="res://materials/ball_heightmap.png" id="4"] 7 | 8 | [resource] 9 | albedo_texture = ExtResource("1") 10 | metallic = 1.0 11 | metallic_texture = ExtResource("2") 12 | metallic_texture_channel = 2 13 | roughness_texture = ExtResource("2") 14 | roughness_texture_channel = 1 15 | normal_enabled = true 16 | normal_texture = ExtResource("3") 17 | ao_enabled = true 18 | ao_texture = ExtResource("2") 19 | heightmap_enabled = true 20 | heightmap_scale = 12.5 21 | heightmap_deep_parallax = true 22 | heightmap_min_layers = 8 23 | heightmap_max_layers = 32 24 | heightmap_texture = ExtResource("4") 25 | -------------------------------------------------------------------------------- /addons/netfox/icons/rewindable-action.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://dy3qj4jbh0enm" 6 | path="res://.godot/imported/rewindable-action.svg-2eb32b564a2fe4ecb8b77ec5b55683ab.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/netfox/icons/rewindable-action.svg" 14 | dest_files=["res://.godot/imported/rewindable-action.svg-2eb32b564a2fe4ecb8b77ec5b55683ab.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/netfox/icons/tick-interpolator.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://cpaqxcohxtb68" 6 | path="res://.godot/imported/tick-interpolator.svg-c60124cd7d287f516c89a6022efef330.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/netfox/icons/tick-interpolator.svg" 14 | dest_files=["res://.godot/imported/tick-interpolator.svg-c60124cd7d287f516c89a6022efef330.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/netfox/icons/state-synchronizer.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://ogbi1hffcoyh" 6 | path="res://.godot/imported/state-synchronizer.svg-9cb9447ba79f114a58e468a24d17b860.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/netfox/icons/state-synchronizer.svg" 14 | dest_files=["res://.godot/imported/state-synchronizer.svg-9cb9447ba79f114a58e468a24d17b860.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/netfox.extras/icons/rewindable-state.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://dsxkv3ufy1r2q" 6 | path="res://.godot/imported/rewindable-state.svg-b534e5a6f5de20b3fe0b63d612bd36bf.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/netfox.extras/icons/rewindable-state.svg" 14 | dest_files=["res://.godot/imported/rewindable-state.svg-b534e5a6f5de20b3fe0b63d612bd36bf.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/netfox/icons/rollback-synchronizer.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://csg26ysqqb4xe" 6 | path="res://.godot/imported/rollback-synchronizer.svg-99c6071e1009de5a35a481b2f486c380.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/netfox/icons/rollback-synchronizer.svg" 14 | dest_files=["res://.godot/imported/rollback-synchronizer.svg-99c6071e1009de5a35a481b2f486c380.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/netfox/time/network-clocks.gd: -------------------------------------------------------------------------------- 1 | extends Object 2 | class_name NetworkClocks 3 | 4 | class SystemClock: 5 | var offset: float = 0. 6 | 7 | func get_raw_time() -> float: 8 | return Time.get_unix_time_from_system() 9 | 10 | func get_time() -> float: 11 | return get_raw_time() + offset 12 | 13 | func adjust(p_offset: float) -> void: 14 | offset += p_offset 15 | 16 | func set_time(p_time: float) -> void: 17 | offset = p_time - get_raw_time() 18 | 19 | class SteppingClock: 20 | var time: float = 0. 21 | var last_step: float = get_raw_time() 22 | 23 | func get_raw_time() -> float: 24 | return Time.get_unix_time_from_system() 25 | 26 | func get_time() -> float: 27 | return time 28 | 29 | func adjust(p_offset: float) -> void: 30 | time += p_offset 31 | 32 | func set_time(p_time: float) -> void: 33 | last_step = get_raw_time() 34 | time = p_time 35 | 36 | func step(p_multiplier: float = 1.) -> void: 37 | var current_step := get_raw_time() 38 | var step_duration := current_step - last_step 39 | last_step = current_step 40 | 41 | adjust(step_duration * p_multiplier) 42 | -------------------------------------------------------------------------------- /addons/netfox/icons/rollback-synchronizer.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /addons/netfox.extras/icons/network-rigid-body-2d.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://bhm6yivew52yv" 6 | path="res://.godot/imported/network-rigid-body-2d.svg-c7ef7df16c4383a80b842fa966aa7aea.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/netfox.extras/icons/network-rigid-body-2d.svg" 14 | dest_files=["res://.godot/imported/network-rigid-body-2d.svg-c7ef7df16c4383a80b842fa966aa7aea.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/netfox.extras/icons/network-rigid-body-3d.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://b2lri7ofmq6a7" 6 | path="res://.godot/imported/network-rigid-body-3d.svg-325e3a4834ddab37f186912ac93fa449.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/netfox.extras/icons/network-rigid-body-3d.svg" 14 | dest_files=["res://.godot/imported/network-rigid-body-3d.svg-325e3a4834ddab37f186912ac93fa449.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/netfox/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Gálffy Tamás 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /addons/netfox.extras/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Gálffy Tamás 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /addons/netfox.noray/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Gálffy Tamás 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /addons/netfox.extras/icons/rewindable-state-machine.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://cx8j75i6acsic" 6 | path="res://.godot/imported/rewindable-state-machine.svg-87ca202891a7e2319363a5e2f5387494.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/netfox.extras/icons/rewindable-state-machine.svg" 14 | dest_files=["res://.godot/imported/rewindable-state-machine.svg-87ca202891a7e2319363a5e2f5387494.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/netfox.internals/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Gálffy Tamás 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Alberto Klocker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /addons/debug_draw_3d/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 DmitriySalnikov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the Software), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, andor sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /addons/netfox.extras/weapon/network-weapon-proxy.gd: -------------------------------------------------------------------------------- 1 | extends NetworkWeapon 2 | class_name _NetworkWeaponProxy 3 | 4 | var c_can_fire: Callable 5 | var c_can_peer_use: Callable 6 | var c_after_fire: Callable 7 | var c_spawn: Callable 8 | var c_get_data: Callable 9 | var c_apply_data: Callable 10 | var c_is_reconcilable: Callable 11 | var c_reconcile: Callable 12 | 13 | func _can_fire() -> bool: 14 | return c_can_fire.call() 15 | 16 | func _can_peer_use(peer_id: int) -> bool: 17 | return c_can_peer_use.call(peer_id) 18 | 19 | func _after_fire(projectile: Node): 20 | c_after_fire.call(projectile) 21 | 22 | func _spawn() -> Node: 23 | return c_spawn.call() 24 | 25 | func _get_data(projectile: Node) -> Dictionary: 26 | return c_get_data.call(projectile) 27 | 28 | func _apply_data(projectile: Node, data: Dictionary): 29 | c_apply_data.call(projectile, data) 30 | 31 | func _is_reconcilable(projectile: Node, request_data: Dictionary, local_data: Dictionary) -> bool: 32 | return c_is_reconcilable.call(projectile, request_data, local_data) 33 | 34 | func _reconcile(projectile: Node, local_data: Dictionary, remote_data: Dictionary): 35 | c_reconcile.call(projectile, local_data, remote_data) 36 | -------------------------------------------------------------------------------- /addons/netfox/README.md: -------------------------------------------------------------------------------- 1 | # netfox 2 | 3 | The core addon of [netfox], providing responsive multiplayer features for the 4 | [Godot Engine]. 5 | 6 | ## Features 7 | 8 | * ⏲️ Synchronized time 9 | * Runs game logic at a fixed, configurable tickrate 10 | * Time synchronized to game host 11 | * 🧈 State interpolation 12 | * Render 24fps tickrate at buttery smooth 60fps or more 13 | * Add a `TickInterpolator` node and it just works 14 | * 💨 Lag compensation with CSP 15 | * Implement responsive player motion with little to no extra code 16 | * Just use the `RollbackSynchronizer` node for state synchronization 17 | 18 | ## Install 19 | 20 | See the root [README](../../README.md). 21 | 22 | ## Usage 23 | 24 | See the [docs](https://foxssake.github.io/netfox/). 25 | 26 | ## License 27 | 28 | netfox is under the [MIT license](LICENSE). 29 | 30 | ## Issues 31 | 32 | In case of any issues, comments, or questions, please feel free to [open an issue]! 33 | 34 | [netfox]: https://github.com/foxssake/netfox 35 | [source]: https://github.com/foxssake/netfox/archive/refs/heads/main.zip 36 | [Godot engine]: https://godotengine.org/ 37 | [open an issue]: https://github.com/foxssake/netfox/issues 38 | -------------------------------------------------------------------------------- /materials/grass_albedo.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://cicithvrtycgj" 6 | path.s3tc="res://.godot/imported/grass_albedo.png-57e8badbbc8384a3f2937b01be12939e.s3tc.ctex" 7 | metadata={ 8 | "imported_formats": ["s3tc_bptc"], 9 | "vram_texture": true 10 | } 11 | 12 | [deps] 13 | 14 | source_file="res://materials/grass_albedo.png" 15 | dest_files=["res://.godot/imported/grass_albedo.png-57e8badbbc8384a3f2937b01be12939e.s3tc.ctex"] 16 | 17 | [params] 18 | 19 | compress/mode=2 20 | compress/high_quality=false 21 | compress/lossy_quality=0.7 22 | compress/uastc_level=0 23 | compress/rdo_quality_loss=0.0 24 | compress/hdr_compression=1 25 | compress/normal_map=0 26 | compress/channel_pack=0 27 | mipmaps/generate=true 28 | mipmaps/limit=-1 29 | roughness/mode=0 30 | roughness/src_normal="" 31 | process/channel_remap/red=0 32 | process/channel_remap/green=1 33 | process/channel_remap/blue=2 34 | process/channel_remap/alpha=3 35 | process/fix_alpha_border=true 36 | process/premult_alpha=false 37 | process/normal_map_invert_y=false 38 | process/hdr_as_srgb=false 39 | process/hdr_clamp_exposure=false 40 | process/size_limit=0 41 | detect_3d/compress_to=0 42 | -------------------------------------------------------------------------------- /addons/netfox/icons/state-synchronizer.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /addons/debug_draw_3d/libs/libdd3d.macos.editor.universal.framework/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleInfoDictionaryVersion 6 | 6.0 7 | CFBundleDevelopmentRegion 8 | en 9 | CFBundleExecutable 10 | libdd3d.macos.editor.universal.dylib 11 | CFBundleName 12 | Debug Draw 3D 13 | CFBundleDisplayName 14 | Debug Draw 3D 15 | CFBundleIdentifier 16 | ru.dmitriysalnikov.dd3d 17 | NSHumanReadableCopyright 18 | Copyright (c) Dmitriy Salnikov. 19 | CFBundleVersion 20 | 1.4.5 21 | CFBundleShortVersionString 22 | 1.4.5 23 | CFBundlePackageType 24 | FMWK 25 | CSResourcesFileMapped 26 | 27 | DTPlatformName 28 | macosx 29 | LSMinimumSystemVersion 30 | 10.14 31 | 32 | 33 | -------------------------------------------------------------------------------- /addons/debug_draw_3d/libs/libdd3d.macos.template_release.universal.framework/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleInfoDictionaryVersion 6 | 6.0 7 | CFBundleDevelopmentRegion 8 | en 9 | CFBundleExecutable 10 | libdd3d.macos.template_release.universal.dylib 11 | CFBundleName 12 | Debug Draw 3D 13 | CFBundleDisplayName 14 | Debug Draw 3D 15 | CFBundleIdentifier 16 | ru.dmitriysalnikov.dd3d 17 | NSHumanReadableCopyright 18 | Copyright (c) Dmitriy Salnikov. 19 | CFBundleVersion 20 | 1.4.5 21 | CFBundleShortVersionString 22 | 1.4.5 23 | CFBundlePackageType 24 | FMWK 25 | CSResourcesFileMapped 26 | 27 | DTPlatformName 28 | macosx 29 | LSMinimumSystemVersion 30 | 10.14 31 | 32 | 33 | -------------------------------------------------------------------------------- /addons/netfox/serializers/tickset-serializer.gd: -------------------------------------------------------------------------------- 1 | extends Object 2 | class_name _TicksetSerializer 3 | 4 | static func serialize(earliest_tick: int, latest_tick: int, active_ticks: _Set) -> PackedByteArray: 5 | var buffer := StreamPeerBuffer.new() 6 | var tickset_duration = latest_tick - earliest_tick 7 | 8 | assert(latest_tick >= earliest_tick, "Tickset ends before it starts!") 9 | assert(tickset_duration <= 255, "Tickset covers more than supported 255 ticks!") 10 | 11 | buffer.put_u32(earliest_tick) 12 | buffer.put_u8(tickset_duration) 13 | 14 | for tick in active_ticks: 15 | assert(tick <= latest_tick, "Trying to serialize ticks beyond latest !") 16 | buffer.put_u8(tick - earliest_tick) 17 | 18 | return buffer.data_array 19 | 20 | static func deserialize(bytes: PackedByteArray) -> Array: 21 | var buffer := StreamPeerBuffer.new() 22 | buffer.data_array = bytes 23 | 24 | var earliest_tick := buffer.get_u32() 25 | var tickset_duration := buffer.get_u8() 26 | var latest_tick := earliest_tick + tickset_duration 27 | 28 | var active_ticks := _Set.new() 29 | 30 | while buffer.get_available_bytes() > 0: 31 | var tick = earliest_tick + buffer.get_u8() 32 | active_ticks.add(tick) 33 | 34 | return [earliest_tick, latest_tick, active_ticks] 35 | -------------------------------------------------------------------------------- /addons/debug_draw_3d/libs/libdd3d.macos.template_release.universal.enabled.framework/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleInfoDictionaryVersion 6 | 6.0 7 | CFBundleDevelopmentRegion 8 | en 9 | CFBundleExecutable 10 | libdd3d.macos.template_release.universal.enabled.dylib 11 | CFBundleName 12 | Debug Draw 3D 13 | CFBundleDisplayName 14 | Debug Draw 3D 15 | CFBundleIdentifier 16 | ru.dmitriysalnikov.dd3d 17 | NSHumanReadableCopyright 18 | Copyright (c) Dmitriy Salnikov. 19 | CFBundleVersion 20 | 1.4.5 21 | CFBundleShortVersionString 22 | 1.4.5 23 | CFBundlePackageType 24 | FMWK 25 | CSResourcesFileMapped 26 | 27 | DTPlatformName 28 | macosx 29 | LSMinimumSystemVersion 30 | 10.14 31 | 32 | 33 | -------------------------------------------------------------------------------- /addons/netfox/icons/tick-interpolator.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /addons/netfox/properties/property-config.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name _PropertyConfig 3 | 4 | var _properties: Array[PropertyEntry] = [] 5 | var _auth_properties: Dictionary = {} # Peer (int) to owned properties (Array[PropertyEntry]) 6 | 7 | var local_peer_id: int 8 | 9 | func clear() -> void: 10 | _properties.clear() 11 | _auth_properties.clear() 12 | 13 | func set_properties(p_properties: Array[PropertyEntry]) -> void: 14 | clear() 15 | _properties.assign(p_properties) 16 | 17 | func set_properties_from_paths(property_paths: Array[String], property_cache: PropertyCache) -> void: 18 | clear() 19 | for path in property_paths: 20 | _properties.append(property_cache.get_entry(path)) 21 | 22 | func get_properties() -> Array[PropertyEntry]: 23 | return _properties 24 | 25 | func get_owned_properties() -> Array[PropertyEntry]: 26 | return get_properties_owned_by(local_peer_id) 27 | 28 | func get_properties_owned_by(peer: int) -> Array[PropertyEntry]: 29 | if not _auth_properties.has(peer): 30 | var owned_properties: Array[PropertyEntry] = [] 31 | for property_entry in _properties: 32 | if property_entry.node.get_multiplayer_authority() == peer: 33 | owned_properties.append(property_entry) 34 | _auth_properties[peer] = owned_properties 35 | 36 | return _auth_properties[peer] 37 | -------------------------------------------------------------------------------- /addons/netfox.noray/README.md: -------------------------------------------------------------------------------- 1 | # netfox.noray 2 | 3 | Bulletproof your connectivity with [netfox]'s [noray] integration! 4 | 5 | ## Features 6 | 7 | * 🤝 Establish connectivity using NAT punchthrough 8 | * Uses [noray] for orchestration 9 | * Implements a full UDP handshake 10 | * 🛜 Use [noray] as a relay 11 | * Useful in cases where NAT punchthrough fails 12 | * If you can see this repo, you probably can connect through [noray] 13 | 14 | ## Install 15 | 16 | See the root [README](../../README.md). 17 | 18 | > *Note*, that while *netfox.noray* is part of the *netfox* suite, it can be 19 | > used alone, without installing *netfox* itself. 20 | 21 | ## Usage 22 | 23 | See the [docs](https://foxssake.github.io/netfox/netfox.noray/guides/noray/). 24 | 25 | For a full example, see [noray-bootstrapper.gd]. 26 | 27 | ## License 28 | 29 | netfox.noray is under the [MIT license](LICENSE). 30 | 31 | ## Issues 32 | 33 | In case of any issues, comments, or questions, please feel free to [open an issue]! 34 | 35 | [netfox]: https://github.com/foxssake/netfox 36 | [source]: https://github.com/foxssake/netfox/archive/refs/heads/main.zip 37 | [noray]: https://github.com/foxssake/noray 38 | [noray-bootstrapper.gd]: ../../examples/shared/scripts/noray-bootstrapper.gd 39 | [open an issue]: https://github.com/foxssake/netfox/issues 40 | -------------------------------------------------------------------------------- /addons/netfox.internals/set.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name _Set 3 | 4 | var _data: Dictionary = {} 5 | var _iterator_idx: int = -1 6 | 7 | static func of(items: Array) -> _Set: 8 | var result := _Set.new() 9 | for item in items: 10 | result.add(item) 11 | return result 12 | 13 | func add(value): 14 | _data[value] = true 15 | 16 | func has(value) -> bool: 17 | return _data.has(value) 18 | 19 | func size() -> int: 20 | return _data.size() 21 | 22 | func is_empty() -> bool: 23 | return _data.is_empty() 24 | 25 | func erase(value): 26 | return _data.erase(value) 27 | 28 | func clear(): 29 | _data.clear() 30 | 31 | func values() -> Array: 32 | return _data.keys() 33 | 34 | func min(): 35 | return _data.keys().min() 36 | 37 | func max(): 38 | return _data.keys().max() 39 | 40 | func equals(other) -> bool: 41 | if not other or not other is _Set: 42 | return false 43 | 44 | return values() == other.values() 45 | 46 | func _to_string(): 47 | return "Set" + str(values()) 48 | 49 | func _iter_init(arg) -> bool: 50 | _iterator_idx = 0 51 | return _can_iterate() 52 | 53 | func _iter_next(arg) -> bool: 54 | _iterator_idx += 1 55 | return _can_iterate() 56 | 57 | func _iter_get(arg): 58 | return _data.keys()[_iterator_idx] 59 | 60 | func _can_iterate() -> bool: 61 | if _data.is_empty() or _iterator_idx >= _data.size(): 62 | _iterator_idx = -1 63 | return false 64 | return true 65 | -------------------------------------------------------------------------------- /addons/netfox/properties/property-entry.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name PropertyEntry 3 | 4 | var _path: String 5 | var node: Node 6 | var property: String 7 | 8 | static var _logger := _NetfoxLogger.for_netfox("PropertyEntry") 9 | 10 | func get_value() -> Variant: 11 | return node.get_indexed(property) 12 | 13 | func set_value(value): 14 | node.set_indexed(property, value) 15 | 16 | func is_valid() -> bool: 17 | if not node or not is_instance_valid(node): 18 | # Node is invalid 19 | return false 20 | 21 | # Return true if node has given property 22 | return node.get_property_list()\ 23 | .any(func(it): return it["name"] == property) 24 | 25 | func _to_string() -> String: 26 | return _path 27 | 28 | static func parse(root: Node, path: String) -> PropertyEntry: 29 | var result = PropertyEntry.new() 30 | result.node = root.get_node(NodePath(path)) 31 | result.property = path.erase(0, path.find(":") + 1) 32 | result._path = path 33 | return result 34 | 35 | static func make_path(root: Node, node: Variant, property: String) -> String: 36 | var node_path := "" 37 | 38 | if node is String: 39 | node_path = node 40 | elif node is NodePath: 41 | node_path = str(node) 42 | elif node is Node: 43 | node_path = str(root.get_path_to(node)) 44 | else: 45 | _logger.error("Can't stringify node reference: %s", [node]) 46 | return "" 47 | 48 | if node_path == ".": 49 | node_path = "" 50 | 51 | return "%s:%s" % [node_path, property] 52 | -------------------------------------------------------------------------------- /addons/netfox.noray/netfox-noray.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | const ROOT = "res://addons/netfox.noray" 5 | 6 | var SETTINGS = [ 7 | _NetfoxLogger.make_setting("netfox/logging/netfox_noray_log_level") 8 | ] 9 | 10 | const AUTOLOADS = [ 11 | { 12 | "name": "Noray", 13 | "path": ROOT + "/noray.gd" 14 | }, 15 | { 16 | "name": "PacketHandshake", 17 | "path": ROOT + "/packet-handshake.gd" 18 | } 19 | ] 20 | 21 | func _enter_tree(): 22 | for setting in SETTINGS: 23 | add_setting(setting) 24 | 25 | for autoload in AUTOLOADS: 26 | add_autoload_singleton(autoload.name, autoload.path) 27 | 28 | func _exit_tree(): 29 | if ProjectSettings.get_setting("netfox/general/clear_settings", false): 30 | for setting in SETTINGS: 31 | remove_setting(setting) 32 | 33 | for autoload in AUTOLOADS: 34 | remove_autoload_singleton(autoload.name) 35 | 36 | func add_setting(setting: Dictionary): 37 | if ProjectSettings.has_setting(setting.name): 38 | return 39 | 40 | ProjectSettings.set_setting(setting.name, setting.value) 41 | ProjectSettings.set_initial_value(setting.name, setting.value) 42 | ProjectSettings.add_property_info({ 43 | "name": setting.get("name"), 44 | "type": setting.get("type"), 45 | "hint": setting.get("hint", PROPERTY_HINT_NONE), 46 | "hint_string": setting.get("hint_string", "") 47 | }) 48 | 49 | func remove_setting(setting: Dictionary): 50 | if not ProjectSettings.has_setting(setting.name): 51 | return 52 | 53 | ProjectSettings.clear(setting.name) 54 | -------------------------------------------------------------------------------- /addons/netfox.internals/bimap.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name _BiMap 3 | 4 | # Maps one-to-one associations in a bidirectional way 5 | 6 | var _keys_to_values := {} 7 | var _values_to_keys := {} 8 | 9 | func put(key: Variant, value: Variant) -> void: 10 | var old_value = _keys_to_values.get(key) 11 | var old_key = _values_to_keys.get(value) 12 | 13 | if old_value != null: _values_to_keys.erase(old_value) 14 | if old_key != null: _keys_to_values.erase(old_key) 15 | 16 | _keys_to_values[key] = value 17 | _values_to_keys[value] = key 18 | 19 | func get_by_key(key: Variant) -> Variant: 20 | return _keys_to_values.get(key) 21 | 22 | func get_by_value(value: Variant) -> Variant: 23 | return _values_to_keys.get(value) 24 | 25 | func has_key(key: Variant) -> bool: 26 | return _keys_to_values.has(key) 27 | 28 | func has_value(value: Variant) -> bool: 29 | return _values_to_keys.has(value) 30 | 31 | func erase_key(key: Variant) -> bool: 32 | if not has_key(key): 33 | return false 34 | 35 | var value = get_by_key(key) 36 | _values_to_keys.erase(value) 37 | _keys_to_values.erase(key) 38 | return true 39 | 40 | func erase_value(value: Variant) -> bool: 41 | if not has_value(value): 42 | return false 43 | 44 | var key = get_by_value(value) 45 | _values_to_keys.erase(value) 46 | _keys_to_values.erase(key) 47 | return true 48 | 49 | func size() -> int: 50 | return _keys_to_values.size() 51 | 52 | func keys() -> Array: 53 | return _keys_to_values.keys() 54 | 55 | func values() -> Array: 56 | return _values_to_keys.keys() 57 | -------------------------------------------------------------------------------- /materials/grass.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="StandardMaterial3D" load_steps=6 format=3 uid="uid://bnqudedf7vlb0"] 2 | 3 | [ext_resource type="Texture2D" uid="uid://cicithvrtycgj" path="res://materials/grass_albedo.png" id="1"] 4 | [ext_resource type="Texture2D" uid="uid://dys4dhkhk3pw4" path="res://materials/grass_orm.png" id="2"] 5 | [ext_resource type="Texture2D" uid="uid://mr7pu5igx84m" path="res://materials/grass_normal.png" id="3"] 6 | [ext_resource type="Texture2D" uid="uid://cx8yeg0tx1e4h" path="res://materials/grass_heightmap.png" id="4"] 7 | [ext_resource type="Texture2D" uid="uid://c5xh5cxrpjwps" path="res://materials/grass_emission.png" id="5"] 8 | 9 | [resource] 10 | albedo_color = Color(0.27340657, 0.5222743, 0.27338237, 1) 11 | albedo_texture = ExtResource("1") 12 | metallic = 0.62 13 | metallic_specular = 0.24 14 | metallic_texture = ExtResource("2") 15 | metallic_texture_channel = 2 16 | roughness = 0.9 17 | roughness_texture = ExtResource("2") 18 | roughness_texture_channel = 1 19 | emission_enabled = true 20 | emission_texture = ExtResource("5") 21 | normal_enabled = true 22 | normal_texture = ExtResource("3") 23 | ao_enabled = true 24 | ao_light_affect = 0.62 25 | ao_texture = ExtResource("2") 26 | heightmap_scale = 4.402 27 | heightmap_texture = ExtResource("4") 28 | subsurf_scatter_enabled = true 29 | subsurf_scatter_strength = 0.26 30 | backlight = Color(0.368117, 0.41861, 0.25716, 1) 31 | uv1_triplanar = true 32 | uv1_triplanar_sharpness = 1.4142138 33 | texture_filter = 5 34 | proximity_fade_enabled = true 35 | distance_fade_mode = 1 36 | -------------------------------------------------------------------------------- /models/car.glb.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="scene" 4 | importer_version=1 5 | type="PackedScene" 6 | uid="uid://dtoa53rtwixhg" 7 | path="res://.godot/imported/car.glb-6d45222c70795b114333c5c84f1bce6b.scn" 8 | 9 | [deps] 10 | 11 | source_file="res://models/car.glb" 12 | dest_files=["res://.godot/imported/car.glb-6d45222c70795b114333c5c84f1bce6b.scn"] 13 | 14 | [params] 15 | 16 | nodes/root_type="" 17 | nodes/root_name="" 18 | nodes/root_script=null 19 | nodes/apply_root_scale=true 20 | nodes/root_scale=1.0 21 | nodes/import_as_skeleton_bones=false 22 | nodes/use_name_suffixes=true 23 | nodes/use_node_type_suffixes=true 24 | meshes/ensure_tangents=true 25 | meshes/generate_lods=true 26 | meshes/create_shadow_meshes=true 27 | meshes/light_baking=1 28 | meshes/lightmap_texel_size=0.2 29 | meshes/force_disable_compression=false 30 | skins/use_named_skins=true 31 | animation/import=true 32 | animation/fps=30 33 | animation/trimming=false 34 | animation/remove_immutable_tracks=true 35 | animation/import_rest_as_RESET=false 36 | import_script/path="" 37 | materials/extract=0 38 | materials/extract_format=0 39 | materials/extract_path="" 40 | _subresources={ 41 | "materials": { 42 | "Material.001": { 43 | "use_external/enabled": true, 44 | "use_external/fallback_path": "res://materials/car_paint.tres", 45 | "use_external/path": "uid://b6yeasaavorsj" 46 | }, 47 | "Material.002": { 48 | "use_external/enabled": true, 49 | "use_external/fallback_path": "res://materials/car_window.tres", 50 | "use_external/path": "uid://d0ug2v7jto7a4" 51 | } 52 | } 53 | } 54 | gltf/naming_version=2 55 | gltf/embedded_image_handling=1 56 | -------------------------------------------------------------------------------- /models/field.glb.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="scene" 4 | importer_version=1 5 | type="PackedScene" 6 | uid="uid://blu8uqh0vbrhi" 7 | path="res://.godot/imported/field.glb-af956a1e9b913de5f46376b9c4da8c26.scn" 8 | 9 | [deps] 10 | 11 | source_file="res://models/field.glb" 12 | dest_files=["res://.godot/imported/field.glb-af956a1e9b913de5f46376b9c4da8c26.scn"] 13 | 14 | [params] 15 | 16 | nodes/root_type="" 17 | nodes/root_name="" 18 | nodes/root_script=null 19 | nodes/apply_root_scale=true 20 | nodes/root_scale=1.0 21 | nodes/import_as_skeleton_bones=false 22 | nodes/use_name_suffixes=true 23 | nodes/use_node_type_suffixes=true 24 | meshes/ensure_tangents=true 25 | meshes/generate_lods=true 26 | meshes/create_shadow_meshes=true 27 | meshes/light_baking=1 28 | meshes/lightmap_texel_size=0.2 29 | meshes/force_disable_compression=false 30 | skins/use_named_skins=true 31 | animation/import=true 32 | animation/fps=30 33 | animation/trimming=false 34 | animation/remove_immutable_tracks=true 35 | animation/import_rest_as_RESET=false 36 | import_script/path="" 37 | materials/extract=0 38 | materials/extract_format=0 39 | materials/extract_path="" 40 | _subresources={ 41 | "materials": { 42 | "Material.001": { 43 | "use_external/enabled": true, 44 | "use_external/fallback_path": "res://materials/wall.tres", 45 | "use_external/path": "uid://uo7itoedkenq" 46 | }, 47 | "Material.002": { 48 | "use_external/enabled": true, 49 | "use_external/fallback_path": "res://materials/grass.tres", 50 | "use_external/path": "uid://bnqudedf7vlb0" 51 | } 52 | } 53 | } 54 | gltf/naming_version=2 55 | gltf/embedded_image_handling=1 56 | -------------------------------------------------------------------------------- /addons/netfox.internals/history-buffer.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name _HistoryBuffer 3 | 4 | # Maps ticks (int) to arbitrary data 5 | var _buffer: Dictionary = {} 6 | 7 | func get_snapshot(tick: int): 8 | if _buffer.has(tick): 9 | return _buffer[tick] 10 | else: 11 | return null 12 | 13 | func set_snapshot(tick: int, data): 14 | _buffer[tick] = data 15 | 16 | func get_buffer() -> Dictionary: 17 | return _buffer 18 | 19 | func get_earliest_tick() -> int: 20 | return _buffer.keys().min() 21 | 22 | func get_latest_tick() -> int: 23 | return _buffer.keys().max() 24 | 25 | func get_closest_tick(tick: int) -> int: 26 | if _buffer.has(tick): 27 | return tick 28 | 29 | if _buffer.is_empty(): 30 | return -1 31 | 32 | var earliest_tick = get_earliest_tick() 33 | if tick < earliest_tick: 34 | return earliest_tick 35 | 36 | var latest_tick = get_latest_tick() 37 | if tick > latest_tick: 38 | return latest_tick 39 | 40 | return _buffer.keys() \ 41 | .filter(func (key): return key < tick) \ 42 | .max() 43 | 44 | func get_history(tick: int): 45 | var closest_tick = get_closest_tick(tick) 46 | return _buffer.get(closest_tick) 47 | 48 | func trim(earliest_tick_to_keep: int): 49 | var ticks := _buffer.keys() 50 | for tick in ticks: 51 | if tick < earliest_tick_to_keep: 52 | _buffer.erase(tick) 53 | 54 | func clear(): 55 | _buffer.clear() 56 | 57 | func size() -> int: 58 | return _buffer.size() 59 | 60 | func is_empty() -> bool: 61 | return _buffer.is_empty() 62 | 63 | func has(tick) -> bool: 64 | return _buffer.has(tick) 65 | 66 | func ticks() -> Array: 67 | return _buffer.keys() 68 | 69 | func erase(tick): 70 | _buffer.erase(tick) 71 | -------------------------------------------------------------------------------- /addons/netfox.extras/physics/network-rigid-body-2d.gd: -------------------------------------------------------------------------------- 1 | @icon("res://addons/netfox.extras/icons/network-rigid-body-2d.svg") 2 | extends RigidBody2D 3 | class_name NetworkRigidBody2D 4 | 5 | ## A rollback / state synchronizer class for RigidBody2D. 6 | ## Set state property path to physics_state to synchronize the state of this body. 7 | 8 | @onready var direct_state = PhysicsServer2D.body_get_direct_state(get_rid()) 9 | 10 | var physics_state: Array: 11 | get: return get_state() 12 | set(v): set_state(v) 13 | 14 | enum { 15 | ORIGIN, 16 | ROT, 17 | LIN_VEL, 18 | ANG_VEL, 19 | SLEEPING 20 | } 21 | 22 | func _notification(notification: int): 23 | if notification == NOTIFICATION_READY: 24 | add_to_group("network_rigid_body") 25 | 26 | func get_state() -> Array: 27 | var body_state: Array = [Vector3.ZERO, Quaternion.IDENTITY, Vector3.ZERO, Vector3.ZERO, false] 28 | body_state[ORIGIN] = direct_state.transform.origin 29 | body_state[ROT] = direct_state.transform.get_rotation() 30 | body_state[LIN_VEL] = direct_state.linear_velocity 31 | body_state[ANG_VEL] = direct_state.angular_velocity 32 | body_state[SLEEPING] = direct_state.sleeping 33 | return body_state 34 | 35 | func set_state(remote_state: Array) -> void: 36 | direct_state.transform = Transform2D(remote_state[ROT], remote_state[ORIGIN]) 37 | direct_state.linear_velocity = remote_state[LIN_VEL] 38 | direct_state.angular_velocity = remote_state[ANG_VEL] 39 | direct_state.sleeping = remote_state[SLEEPING] 40 | 41 | ## Override and apply any logic, forces or impulses to the rigid body as you would in physics_process 42 | ## The physics engine will run its simulation during rollback_tick with other nodes 43 | func _physics_rollback_tick(_delta, _tick): 44 | pass 45 | -------------------------------------------------------------------------------- /addons/netfox.internals/editor-utils.gd: -------------------------------------------------------------------------------- 1 | extends Object 2 | class_name _NetfoxEditorUtils 3 | 4 | static func gather_properties(root: Node, callback_name: String, handler: Callable) -> Array[String]: 5 | var result: Array[String] = [] 6 | 7 | var nodes: Array[Node] = root.find_children("*") 8 | nodes.push_front(root) 9 | for node in nodes: 10 | if not node.has_method(callback_name): 11 | continue 12 | 13 | var readable_node_name := "\"%s\" (\"%s\")" % [node.name, root.get_path_to(node)] 14 | if node.get(callback_name) == null: 15 | result.push_back("Can't grab method \"%s\" from node %s! Is it a @tool?" % [callback_name, readable_node_name]) 16 | continue 17 | 18 | var props = node.get(callback_name).call() 19 | if not props is Array: 20 | result.push_back("Node %s didn't return an array on calling \"%s\"" % [readable_node_name, callback_name]) 21 | continue 22 | 23 | for prop in props: 24 | if prop is String: 25 | # Property is a string, meaning property path relative to node 26 | handler.call(node, prop) 27 | elif prop is Array and prop.size() >= 2: 28 | # Property is a node-property tuple 29 | var prop_node: Node = null 30 | 31 | # Node can be a String, NodePath, or an actual Node 32 | if prop[0] is String or prop[0] is NodePath: 33 | prop_node = node.get_node(prop[0]) 34 | elif prop[0] is Node: 35 | prop_node = prop[0] 36 | else: 37 | result.push_back("Node %s specified invalid node in \"%s\": %s" % [readable_node_name, callback_name, prop]) 38 | continue 39 | 40 | handler.call(prop_node, prop[1]) 41 | else: 42 | result.push_back("Node %s specified invalid property in \"%s\": %s" % [readable_node_name, callback_name, prop]) 43 | 44 | return result 45 | -------------------------------------------------------------------------------- /scripts/main.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | #const player_scene = preload("res://scenes/player.tscn") 4 | const player_scene = preload("res://scenes/player_rb.tscn") 5 | const ADDRESS = "localhost" 6 | const PORT = 9999 7 | var enet_peer = ENetMultiplayerPeer.new() 8 | 9 | func _ready(): 10 | if OS.has_feature("editor"): 11 | var status = _host() 12 | if status == Error.ERR_CANT_CREATE: 13 | _join() 14 | 15 | func _host(): 16 | var status = enet_peer.create_server(PORT) 17 | if status > 0: 18 | return status 19 | 20 | enet_peer.host.compress(ENetConnection.COMPRESS_RANGE_CODER) 21 | 22 | multiplayer.multiplayer_peer = enet_peer 23 | multiplayer.peer_connected.connect(client_connected) 24 | multiplayer.peer_disconnected.connect(remove_player) 25 | add_player(multiplayer.get_unique_id()) 26 | 27 | return status 28 | 29 | func client_connected(peer_id): 30 | print("Client connected: ", peer_id) 31 | add_player(peer_id) 32 | 33 | func _join(): 34 | enet_peer.create_client(ADDRESS, PORT) 35 | enet_peer.host.compress(ENetConnection.COMPRESS_RANGE_CODER) 36 | 37 | multiplayer.multiplayer_peer = enet_peer 38 | multiplayer.connected_to_server.connect(connected) 39 | 40 | func _unhandled_input(_event): 41 | if Input.is_action_just_pressed("quit"): 42 | get_tree().quit() 43 | 44 | func add_player(peer_id): 45 | var player = player_scene.instantiate() 46 | player.name = str(peer_id) 47 | $Players.add_child(player) 48 | player.team = multiplayer.get_peers().size() % 2 49 | 50 | 51 | func connected(): 52 | print(multiplayer.get_unique_id(), " Connected to server") 53 | await NetworkTime.after_sync 54 | 55 | func remove_player(peer_id): 56 | var player = get_node_or_null(str(peer_id)) 57 | if player: 58 | player.queue_free() 59 | -------------------------------------------------------------------------------- /addons/netfox.extras/physics/network-rigid-body-3d.gd: -------------------------------------------------------------------------------- 1 | @icon("res://addons/netfox.extras/physics/network-rigid-body-3d.gd") 2 | extends RigidBody3D 3 | class_name NetworkRigidBody3D 4 | 5 | ## A rollback / state synchronizer class for RigidBody3D. 6 | ## Set state property path to physics_state to synchronize the state of this body. 7 | 8 | @onready var direct_state = PhysicsServer3D.body_get_direct_state(get_rid()) 9 | 10 | var physics_state: Array: 11 | get: return get_state() 12 | set(v): set_state(v) 13 | 14 | enum { 15 | ORIGIN, 16 | QUAT, 17 | LIN_VEL, 18 | ANG_VEL, 19 | SLEEPING 20 | } 21 | 22 | func _notification(notification: int): 23 | if notification == NOTIFICATION_READY: 24 | add_to_group("network_rigid_body") 25 | 26 | func get_state() -> Array: 27 | var body_state: Array = [Vector3.ZERO, Quaternion.IDENTITY, Vector3.ZERO, Vector3.ZERO, false] 28 | body_state[ORIGIN] = direct_state.transform.origin 29 | body_state[QUAT] = direct_state.transform.basis.get_rotation_quaternion() 30 | body_state[LIN_VEL] = direct_state.linear_velocity 31 | body_state[ANG_VEL] = direct_state.angular_velocity 32 | body_state[SLEEPING] = direct_state.sleeping 33 | return body_state 34 | 35 | func set_state(remote_state: Array) -> void: 36 | direct_state.transform.origin = remote_state[ORIGIN] 37 | direct_state.transform.basis = Basis(remote_state[QUAT]) 38 | direct_state.linear_velocity = remote_state[LIN_VEL] 39 | direct_state.angular_velocity = remote_state[ANG_VEL] 40 | direct_state.sleeping = remote_state[SLEEPING] 41 | 42 | 43 | ## Override and apply any logic, forces or impulses to the rigid body as you would in physics_process 44 | ## The physics engine will run its simulation during rollback_tick with other nodes 45 | func _physics_rollback_tick(_delta, _tick): 46 | pass 47 | -------------------------------------------------------------------------------- /models/car.blend.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="scene" 4 | importer_version=1 5 | type="PackedScene" 6 | uid="uid://mwlmhv68da6e" 7 | path="res://.godot/imported/car.blend-085967aaf200b38a9dad83df45cec0ff.scn" 8 | 9 | [deps] 10 | 11 | source_file="res://models/car.blend" 12 | dest_files=["res://.godot/imported/car.blend-085967aaf200b38a9dad83df45cec0ff.scn"] 13 | 14 | [params] 15 | 16 | nodes/root_type="" 17 | nodes/root_name="" 18 | nodes/root_script=null 19 | nodes/apply_root_scale=true 20 | nodes/root_scale=1.0 21 | nodes/import_as_skeleton_bones=false 22 | nodes/use_name_suffixes=true 23 | nodes/use_node_type_suffixes=true 24 | meshes/ensure_tangents=true 25 | meshes/generate_lods=true 26 | meshes/create_shadow_meshes=true 27 | meshes/light_baking=1 28 | meshes/lightmap_texel_size=0.2 29 | meshes/force_disable_compression=false 30 | skins/use_named_skins=true 31 | animation/import=true 32 | animation/fps=30 33 | animation/trimming=false 34 | animation/remove_immutable_tracks=true 35 | animation/import_rest_as_RESET=false 36 | import_script/path="" 37 | materials/extract=0 38 | materials/extract_format=0 39 | materials/extract_path="" 40 | _subresources={} 41 | blender/nodes/visible=0 42 | blender/nodes/active_collection_only=false 43 | blender/nodes/punctual_lights=true 44 | blender/nodes/cameras=true 45 | blender/nodes/custom_properties=true 46 | blender/nodes/modifiers=1 47 | blender/meshes/colors=false 48 | blender/meshes/uvs=true 49 | blender/meshes/normals=true 50 | blender/meshes/export_geometry_nodes_instances=false 51 | blender/meshes/tangents=true 52 | blender/meshes/skins=2 53 | blender/meshes/export_bones_deforming_mesh_only=false 54 | blender/materials/unpack_enabled=true 55 | blender/materials/export_materials=1 56 | blender/animation/limit_playback=true 57 | blender/animation/always_sample=true 58 | blender/animation/group_tracks=true 59 | gltf/naming_version=0 60 | -------------------------------------------------------------------------------- /models/field.blend.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="scene" 4 | importer_version=1 5 | type="PackedScene" 6 | uid="uid://th8ke8d8f3dl" 7 | path="res://.godot/imported/field.blend-670b00f385322c26e0048e9d940b2503.scn" 8 | 9 | [deps] 10 | 11 | source_file="res://models/field.blend" 12 | dest_files=["res://.godot/imported/field.blend-670b00f385322c26e0048e9d940b2503.scn"] 13 | 14 | [params] 15 | 16 | nodes/root_type="" 17 | nodes/root_name="" 18 | nodes/root_script=null 19 | nodes/apply_root_scale=true 20 | nodes/root_scale=1.0 21 | nodes/import_as_skeleton_bones=false 22 | nodes/use_name_suffixes=true 23 | nodes/use_node_type_suffixes=true 24 | meshes/ensure_tangents=true 25 | meshes/generate_lods=true 26 | meshes/create_shadow_meshes=true 27 | meshes/light_baking=1 28 | meshes/lightmap_texel_size=0.2 29 | meshes/force_disable_compression=false 30 | skins/use_named_skins=true 31 | animation/import=true 32 | animation/fps=30 33 | animation/trimming=false 34 | animation/remove_immutable_tracks=true 35 | animation/import_rest_as_RESET=false 36 | import_script/path="" 37 | materials/extract=0 38 | materials/extract_format=0 39 | materials/extract_path="" 40 | _subresources={} 41 | blender/nodes/visible=0 42 | blender/nodes/active_collection_only=false 43 | blender/nodes/punctual_lights=true 44 | blender/nodes/cameras=true 45 | blender/nodes/custom_properties=true 46 | blender/nodes/modifiers=1 47 | blender/meshes/colors=false 48 | blender/meshes/uvs=true 49 | blender/meshes/normals=true 50 | blender/meshes/export_geometry_nodes_instances=false 51 | blender/meshes/tangents=true 52 | blender/meshes/skins=2 53 | blender/meshes/export_bones_deforming_mesh_only=false 54 | blender/materials/unpack_enabled=true 55 | blender/materials/export_materials=1 56 | blender/animation/limit_playback=true 57 | blender/animation/always_sample=true 58 | blender/animation/group_tracks=true 59 | gltf/naming_version=0 60 | -------------------------------------------------------------------------------- /scenes/ball.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=8 format=3 uid="uid://dpm8cgj63eh0s"] 2 | 3 | [ext_resource type="Script" uid="uid://c0s6il6j1o06a" path="res://scripts/ball.gd" id="1_cxlvu"] 4 | [ext_resource type="Material" uid="uid://bp1lxtywe5u3c" path="res://materials/ball.tres" id="3_4dx81"] 5 | [ext_resource type="Script" uid="uid://dour8fehaaugp" path="res://addons/netfox/tick-interpolator.gd" id="3_f7cbr"] 6 | [ext_resource type="Script" uid="uid://d350u8evihs1u" path="res://addons/netfox/rollback/rollback-synchronizer.gd" id="4_4dx81"] 7 | 8 | [sub_resource type="PhysicsMaterial" id="PhysicsMaterial_0xm2m"] 9 | friction = 0.7 10 | bounce = 0.7 11 | 12 | [sub_resource type="SphereShape3D" id="SphereShape3D_0xm2m"] 13 | margin = 0.1 14 | radius = 1.0 15 | 16 | [sub_resource type="SphereMesh" id="SphereMesh_0xm2m"] 17 | radius = 1.0 18 | height = 2.0 19 | 20 | [node name="Ball" type="RigidBody3D"] 21 | collision_layer = 4 22 | collision_mask = 3 23 | physics_material_override = SubResource("PhysicsMaterial_0xm2m") 24 | script = ExtResource("1_cxlvu") 25 | 26 | [node name="CollisionShape3D" type="CollisionShape3D" parent="."] 27 | shape = SubResource("SphereShape3D_0xm2m") 28 | 29 | [node name="MeshInstance3D" type="MeshInstance3D" parent="."] 30 | mesh = SubResource("SphereMesh_0xm2m") 31 | surface_material_override/0 = ExtResource("3_4dx81") 32 | 33 | [node name="TickInterpolator" type="Node" parent="." node_paths=PackedStringArray("root")] 34 | script = ExtResource("3_f7cbr") 35 | root = NodePath("..") 36 | properties = Array[String]([":global_position"]) 37 | metadata/_custom_type_script = "uid://dour8fehaaugp" 38 | 39 | [node name="RollbackSynchronizer" type="Node" parent="." node_paths=PackedStringArray("root")] 40 | script = ExtResource("4_4dx81") 41 | root = NodePath("..") 42 | state_properties = Array[String]([":physics_state", ":entered_goal"]) 43 | metadata/_custom_type_script = "uid://d350u8evihs1u" 44 | -------------------------------------------------------------------------------- /scripts/ball.gd: -------------------------------------------------------------------------------- 1 | extends NetworkRigidBody3D 2 | 3 | class_name Ball 4 | 5 | signal goal_scored(team: int) 6 | 7 | var STARTING_POSITION: Vector3 = Vector3(6.0, 8.0, 0) 8 | 9 | var confetti: PackedScene = preload("res://scenes/confetti.tscn") 10 | 11 | var entered_goal: bool = false 12 | var announced_goal: bool = false 13 | var entered_goal_tick: int = 0 14 | var reset_tick : int = 0 15 | var goal_team: int = 0 16 | 17 | 18 | func _ready(): 19 | set_multiplayer_authority(1, true) 20 | contact_monitor = true 21 | max_contacts_reported = 8 22 | NetworkTime.on_tick.connect(on_tick) 23 | $RollbackSynchronizer.process_settings() 24 | 25 | func is_confirmed_tick(tick: int) -> bool: 26 | var players = get_tree().get_nodes_in_group("players") 27 | for player in players: 28 | if tick > NetworkRollback.get_latest_input_tick(player): 29 | return false 30 | return true 31 | 32 | 33 | func _physics_rollback_tick(_delta: float, tick: int) -> void: 34 | if tick == reset_tick: 35 | reset() 36 | return 37 | 38 | func entered_goal_area(area: Area3D, tick: int) -> void: 39 | entered_goal = true 40 | entered_goal_tick = tick 41 | goal_team = int(area.name.right(1)) 42 | 43 | func on_tick(_delta: float, tick: int) -> void: 44 | 45 | if entered_goal and not announced_goal: 46 | # Check that no player actions can alter the outcome 47 | if is_confirmed_tick(entered_goal_tick): 48 | 49 | goal_scored.emit(goal_team) 50 | entered_goal = false 51 | announced_goal = true 52 | 53 | # Spawn confetti and hide the ball 54 | var confetti_instance = confetti.instantiate() 55 | get_tree().root.add_child(confetti_instance) 56 | confetti_instance.global_position = global_position 57 | hide() 58 | 59 | reset_tick = tick + NetworkTime.seconds_to_ticks(5.) 60 | 61 | 62 | func reset() -> void: 63 | show() 64 | entered_goal = false 65 | announced_goal = false 66 | direct_state.transform.origin = STARTING_POSITION 67 | direct_state.linear_velocity = Vector3.ZERO 68 | direct_state.angular_velocity = Vector3.ZERO 69 | -------------------------------------------------------------------------------- /scripts/ui_info.gd: -------------------------------------------------------------------------------- 1 | extends Label 2 | 3 | var game : Game = null 4 | 5 | func _ready(): 6 | NetworkTime.on_tick.connect(_tick) 7 | game = get_node("/root/World/Game") 8 | 9 | 10 | var data_sent_rate = 0.0 11 | var data_received_rate = 0.0 12 | var current_time = 0.0 13 | var last_time = 0.0 14 | 15 | func perf(type: String): 16 | match type: 17 | "fps": 18 | return Performance.get_monitor(Performance.TIME_FPS) 19 | "process": 20 | return Performance.get_monitor(Performance.TIME_PROCESS) 21 | "draw_calls": 22 | return Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME) 23 | 24 | func _tick(_delta: float, _t: int): 25 | var type = "Server" 26 | if not multiplayer.is_server(): 27 | type = "Client" 28 | 29 | text = "%s - %s" % [type, multiplayer.get_unique_id()] 30 | text += "\nFPS: %s " % perf("fps") 31 | text += "\ntick: %s " % NetworkTime.tick 32 | 33 | if not multiplayer.is_server(): 34 | # Grab latency to server and display 35 | var enet = multiplayer.multiplayer_peer as ENetMultiplayerPeer 36 | if enet == null: 37 | return 38 | 39 | var server = enet.get_peer(1) 40 | var _mean_rtt = server.get_statistic(ENetPacketPeer.PEER_ROUND_TRIP_TIME) 41 | 42 | current_time = Time.get_ticks_msec() / 1000.0 43 | 44 | if int(current_time) > int(last_time): 45 | data_sent_rate = enet.get_host().pop_statistic(ENetConnection.HostStatistic.HOST_TOTAL_SENT_DATA) 46 | data_received_rate = enet.get_host().pop_statistic(ENetConnection.HostStatistic.HOST_TOTAL_RECEIVED_DATA) 47 | last_time = current_time 48 | 49 | text += "\nRTT: %s " % _mean_rtt 50 | text += "\nData - up: %s Kbps | down: %s Kbps" % [int(data_sent_rate * 0.008), int(data_received_rate * 0.008)] 51 | 52 | # # View input submission status 53 | # var players = get_tree().get_nodes_in_group("players") 54 | # var submissions = NetworkRollback.get_input_submissions() 55 | # for player in players: 56 | # text += "\n%s - last input: %s - %s" % [player.name, submissions.get(player), NetworkRollback.has_input_for_tick(player, NetworkRollback.tick)] 57 | -------------------------------------------------------------------------------- /addons/netfox.extras/icons/network-rigid-body-2d.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 33 | 38 | 41 | 46 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /addons/netfox.extras/icons/network-rigid-body-3d.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 33 | 38 | 41 | 46 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /scripts/car_ai.gd: -------------------------------------------------------------------------------- 1 | extends Node3D 2 | 3 | @onready var inputs: PlayerInput = get_parent().get_node("Input") 4 | @onready var ball: Node3D = get_tree().get_root().get_node("World/Ball") 5 | 6 | # Turning circle compensation 7 | const MIN_DISTANCE_FROM_BALL: float = 30.0 8 | const ALIGNMENT_THRESHOLD: float = 0.5 9 | 10 | # Stuff to get unstuck 11 | const STUCK_THRESHOLD: float = 0.2 12 | const STUCK_TIME: float = 4.0 13 | const REVERSE_DURATION: float = 0.5 # 14 | var last_position: Vector3 = Vector3() 15 | var stuck_timer: float = 0.0 16 | var reverse_timer: float = 0.0 17 | 18 | func _ready() -> void: 19 | inputs.ai_enabled = true 20 | NetworkTime.on_tick.connect(_on_tick) 21 | 22 | 23 | func _input(event: InputEvent) -> void: 24 | # Disable AI if the player presses any key 25 | if event is InputEventKey and event.pressed: 26 | inputs.ai_enabled = false 27 | 28 | 29 | func _on_tick(_delta: float, _tick: int) -> void: 30 | if not ball: 31 | return 32 | 33 | if stuck_check(_delta): 34 | return 35 | 36 | # Calculate direction and distance to the ball 37 | var car_direction = global_transform.basis.z.normalized() 38 | var direction_to_ball = (ball.global_position - global_position).normalized() 39 | var cross = car_direction.cross(direction_to_ball).y 40 | 41 | var steering = 0 42 | if cross > 0.1: 43 | steering = -1 # Steer right 44 | elif cross < -0.1: 45 | steering = 1 # Steer left 46 | 47 | inputs.ai_motion = Vector2(steering, 1) 48 | 49 | func stuck_check(_delta) -> bool: 50 | # Check if the car is stuck 51 | var distance_moved = global_position.distance_to(last_position) 52 | if distance_moved < STUCK_THRESHOLD: 53 | stuck_timer += _delta 54 | else: 55 | stuck_timer = 0.0 # Reset the timer if were moving 56 | 57 | last_position = global_position 58 | 59 | # reverse if stuck 60 | if stuck_timer >= STUCK_TIME: 61 | reverse_timer = REVERSE_DURATION 62 | stuck_timer = min(stuck_timer, STUCK_TIME) 63 | 64 | inputs.ai_jumping = false 65 | if reverse_timer > 0.0: 66 | reverse_timer -= _delta 67 | inputs.ai_motion = Vector2(0, -1) # Reverse out 68 | inputs.ai_jumping = true # Jump as well, why not. 69 | return true 70 | 71 | return false 72 | -------------------------------------------------------------------------------- /scripts/game.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | class_name Game 4 | 5 | var kicking_off: bool = false 6 | var kickoff_tick: int = 0 7 | var kickoff_positions = {} 8 | 9 | var scores = [0, 0] 10 | 11 | @onready var score_board : RichTextLabel = $"../TopCentre/ScoreBoard" 12 | @onready var count_down: RichTextLabel = $"../TopCentre/CountDown" 13 | @onready var ball = $"../Ball" 14 | @onready var markers = $"../Markers" 15 | 16 | func _ready() -> void: 17 | ball.goal_scored.connect(on_goal_scored) 18 | NetworkTime.after_tick_loop.connect(update_scoreboard) 19 | NetworkTime.on_tick.connect(on_tick) 20 | multiplayer.peer_connected.connect(player_joined) 21 | update_scoreboard() 22 | queue_kickoff() 23 | 24 | func on_goal_scored(team: int) -> void: 25 | scores[team] += 1 26 | queue_kickoff() 27 | 28 | func on_tick(_delta: float, tick: int) -> void: 29 | if kicking_off and tick >= kickoff_tick: 30 | kicking_off = false 31 | 32 | func update_scoreboard() -> void: 33 | 34 | score_board.text = "[outline_size=5][outline_color=black][color=red]%d[/color] - [color=blue]%d[/color][/outline_color][/outline_size]" % [scores[0], scores[1]] 35 | 36 | count_down.text = "" 37 | if kicking_off: 38 | count_down.text = "[outline_size=10][outline_color=black]%d[/outline_color][/outline_size]" % max(0, NetworkTime.ticks_to_seconds(kickoff_tick - NetworkTime.tick)) 39 | 40 | func queue_kickoff() -> void: 41 | if not multiplayer.is_server(): 42 | return 43 | 44 | await get_tree().create_timer(3.0).timeout 45 | kicking_off = true 46 | kickoff_tick = NetworkTime.tick + NetworkTime.seconds_to_ticks(3.5) 47 | assign_kickoff_positions() 48 | 49 | func player_joined(_peer : int) -> void: 50 | await get_tree().create_timer(1.0).timeout 51 | assign_kickoff_positions() 52 | 53 | func assign_kickoff_positions() -> void: 54 | 55 | if not multiplayer.is_server(): 56 | return 57 | 58 | var players = get_tree().get_nodes_in_group("players") 59 | 60 | kickoff_positions.clear() 61 | var red_index = 1 62 | var blue_index = 1 63 | for player in players: 64 | if player.team == 0: 65 | kickoff_positions[player.name] = markers.get_node("r%d" % red_index).global_position 66 | red_index += 1 67 | else: 68 | kickoff_positions[player.name] = markers.get_node("b%d" % blue_index).global_position 69 | blue_index += 1 70 | 71 | 72 | -------------------------------------------------------------------------------- /addons/netfox/encoder/snapshot-history-encoder.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name _SnapshotHistoryEncoder 3 | 4 | var _history: _PropertyHistoryBuffer 5 | var _property_cache: PropertyCache 6 | var _properties: Array[PropertyEntry] 7 | 8 | var _version := -1 9 | var _has_received := false 10 | 11 | static var _logger := _NetfoxLogger.for_netfox("_SnapshotHistoryEncoder") 12 | 13 | func _init(p_history: _PropertyHistoryBuffer, p_property_cache: PropertyCache): 14 | _history = p_history 15 | _property_cache = p_property_cache 16 | 17 | func set_properties(properties: Array[PropertyEntry]) -> void: 18 | if _properties != properties: 19 | _version = (_version + 1) % 256 20 | _properties = properties.duplicate() 21 | 22 | func encode(tick: int, properties: Array[PropertyEntry]) -> Array: 23 | var snapshot := _history.get_snapshot(tick) 24 | var data := [] 25 | data.resize(properties.size()) 26 | 27 | for i in range(properties.size()): 28 | data[i] = snapshot.get_value(properties[i].to_string()) 29 | data.append(_version) 30 | 31 | return data 32 | 33 | func decode(data: Array, properties: Array[PropertyEntry]) -> _PropertySnapshot: 34 | var result := _PropertySnapshot.new() 35 | var packet_version = data.pop_back() 36 | 37 | if packet_version != _version: 38 | if not _has_received: 39 | # First packet, assume version is OK 40 | _version = packet_version 41 | else: 42 | # Version mismatch, can't parse 43 | _logger.warning("Version mismatch! own: %d, received: %s", [_version, packet_version]) 44 | return result 45 | 46 | if properties.size() != data.size(): 47 | _logger.warning("Received snapshot with %d entries, with %d known - parsing as much as possible", [data.size(), properties.size()]) 48 | 49 | for i in range(0, mini(data.size(), properties.size())): 50 | result.set_value(properties[i].to_string(), data[i]) 51 | 52 | _has_received = true 53 | 54 | return result 55 | 56 | func apply(tick: int, snapshot: _PropertySnapshot, sender: int = -1) -> bool: 57 | if tick < NetworkRollback.history_start: 58 | # State too old! 59 | _logger.error("Received full snapshot for %s, rejecting because older than %s frames", [tick, NetworkRollback.history_limit]) 60 | return false 61 | 62 | if sender > 0: 63 | snapshot.sanitize(sender, _property_cache) 64 | if snapshot.is_empty(): return false 65 | 66 | _history.set_snapshot(tick, snapshot) 67 | return true 68 | -------------------------------------------------------------------------------- /scenes/player.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=9 format=3 uid="uid://stkxyqefx03l"] 2 | 3 | [ext_resource type="Script" uid="uid://cgwxygf25ypdb" path="res://scripts/player.gd" id="1_benb6"] 4 | [ext_resource type="Script" uid="uid://d350u8evihs1u" path="res://addons/netfox/rollback/rollback-synchronizer.gd" id="2_5abps"] 5 | [ext_resource type="Script" uid="uid://cbmc5r8tb2ub7" path="res://scripts/input.gd" id="3_bokmb"] 6 | [ext_resource type="Script" uid="uid://dour8fehaaugp" path="res://addons/netfox/tick-interpolator.gd" id="4_e5kxk"] 7 | 8 | [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_gr2wk"] 9 | albedo_color = Color(0.189389, 0.212656, 0.78031, 1) 10 | 11 | [sub_resource type="BoxMesh" id="BoxMesh_nrvd0"] 12 | material = SubResource("StandardMaterial3D_gr2wk") 13 | size = Vector3(0.5, 0.5, 1) 14 | 15 | [sub_resource type="PrismMesh" id="PrismMesh_dqkch"] 16 | size = Vector3(0.5, 0.5, 0.1) 17 | 18 | [sub_resource type="BoxShape3D" id="BoxShape3D_dqkch"] 19 | size = Vector3(1, 0.456177, 1) 20 | 21 | [node name="Player" type="CharacterBody3D"] 22 | collision_layer = 3 23 | collision_mask = 3 24 | script = ExtResource("1_benb6") 25 | 26 | [node name="MeshInstance3D" type="MeshInstance3D" parent="."] 27 | mesh = SubResource("BoxMesh_nrvd0") 28 | 29 | [node name="MeshInstance3D2" type="MeshInstance3D" parent="."] 30 | transform = Transform3D(1, 0, 0, 0, -4.37114e-08, -1, 0, 1, -4.37114e-08, 0, 0.265293, 0) 31 | mesh = SubResource("PrismMesh_dqkch") 32 | 33 | [node name="CollisionShape3D" type="CollisionShape3D" parent="."] 34 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.0264282, 0) 35 | shape = SubResource("BoxShape3D_dqkch") 36 | 37 | [node name="RollbackSynchronizer" type="Node" parent="." node_paths=PackedStringArray("root")] 38 | script = ExtResource("2_5abps") 39 | root = NodePath("..") 40 | state_properties = Array[String]([":global_transform", ":velocity"]) 41 | input_properties = Array[String](["Input:motion"]) 42 | 43 | [node name="Input" type="Node" parent="."] 44 | script = ExtResource("3_bokmb") 45 | 46 | [node name="TickInterpolator" type="Node" parent="." node_paths=PackedStringArray("root")] 47 | script = ExtResource("4_e5kxk") 48 | root = NodePath("..") 49 | properties = Array[String]([":global_transform"]) 50 | 51 | [node name="Label3D" type="Label3D" parent="."] 52 | visible = false 53 | billboard = 1 54 | shaded = true 55 | no_depth_test = true 56 | text = "Test 57 | " 58 | font_size = 60 59 | -------------------------------------------------------------------------------- /addons/netfox.extras/weapon/network-weapon-2d.gd: -------------------------------------------------------------------------------- 1 | extends Node2D 2 | class_name NetworkWeapon2D 3 | 4 | ## A 2D-specific implementation of [NetworkWeapon]. 5 | 6 | ## Distance to consider too large during reconciliation checks. 7 | @export var distance_threshold: float = 1.0 8 | 9 | var _weapon: _NetworkWeaponProxy 10 | 11 | func can_fire() -> bool: 12 | return _weapon.can_fire() 13 | 14 | func fire() -> Node2D: 15 | return _weapon.fire() 16 | 17 | func get_fired_tick() -> int: 18 | return _weapon.get_fired_tick() 19 | 20 | func _init(): 21 | _weapon = _NetworkWeaponProxy.new() 22 | add_child(_weapon, true, INTERNAL_MODE_BACK) 23 | _weapon.owner = self 24 | 25 | _weapon.c_can_fire = _can_fire 26 | _weapon.c_can_peer_use = _can_peer_use 27 | _weapon.c_after_fire = _after_fire 28 | _weapon.c_spawn = _spawn 29 | _weapon.c_get_data = _get_data 30 | _weapon.c_apply_data = _apply_data 31 | _weapon.c_is_reconcilable = _is_reconcilable 32 | _weapon.c_reconcile = _reconcile 33 | 34 | 35 | ## See [NetworkWeapon] 36 | func _can_fire() -> bool: 37 | return false 38 | 39 | ## See [NetworkWeapon] 40 | func _can_peer_use(peer_id: int) -> bool: 41 | return true 42 | 43 | ## See [NetworkWeapon] 44 | func _after_fire(projectile: Node2D): 45 | pass 46 | 47 | ## See [NetworkWeapon] 48 | func _spawn() -> Node2D: 49 | return null 50 | 51 | func _get_data(projectile: Node2D) -> Dictionary: 52 | return { 53 | "global_transform": projectile.global_transform 54 | } 55 | 56 | func _apply_data(projectile: Node2D, data: Dictionary): 57 | projectile.global_transform = data["global_transform"] 58 | 59 | func _is_reconcilable(projectile: Node2D, request_data: Dictionary, local_data: Dictionary) -> bool: 60 | var req_transform = request_data["global_transform"] as Transform2D 61 | var loc_transform = local_data["global_transform"] as Transform2D 62 | 63 | var request_pos = req_transform.origin 64 | var local_pos = loc_transform.origin 65 | 66 | return request_pos.distance_to(local_pos) < distance_threshold 67 | 68 | func _reconcile(projectile: Node2D, local_data: Dictionary, remote_data: Dictionary): 69 | var local_transform = local_data["global_transform"] as Transform2D 70 | var remote_transform = remote_data["global_transform"] as Transform2D 71 | 72 | var relative_transform = projectile.global_transform * local_transform.inverse() 73 | var final_transform = remote_transform * relative_transform 74 | 75 | projectile.global_transform = final_transform 76 | -------------------------------------------------------------------------------- /addons/netfox.extras/weapon/network-weapon-3d.gd: -------------------------------------------------------------------------------- 1 | extends Node3D 2 | class_name NetworkWeapon3D 3 | 4 | ## A 3D-specific implementation of [NetworkWeapon]. 5 | 6 | ## Distance to consider too large during reconciliation checks. 7 | @export var distance_threshold: float = 1.0 8 | 9 | var _weapon: _NetworkWeaponProxy 10 | 11 | func can_fire() -> bool: 12 | return _weapon.can_fire() 13 | 14 | func fire() -> Node3D: 15 | return _weapon.fire() 16 | 17 | func get_fired_tick() -> int: 18 | return _weapon.get_fired_tick() 19 | 20 | func _init(): 21 | _weapon = _NetworkWeaponProxy.new() 22 | add_child(_weapon, true, INTERNAL_MODE_BACK) 23 | _weapon.owner = self 24 | 25 | _weapon.c_can_fire = _can_fire 26 | _weapon.c_can_peer_use = _can_peer_use 27 | _weapon.c_after_fire = _after_fire 28 | _weapon.c_spawn = _spawn 29 | _weapon.c_get_data = _get_data 30 | _weapon.c_apply_data = _apply_data 31 | _weapon.c_is_reconcilable = _is_reconcilable 32 | _weapon.c_reconcile = _reconcile 33 | 34 | 35 | ## See [NetworkWeapon] 36 | func _can_fire() -> bool: 37 | return false 38 | 39 | ## See [NetworkWeapon] 40 | func _can_peer_use(peer_id: int) -> bool: 41 | return true 42 | 43 | ## See [NetworkWeapon] 44 | func _after_fire(projectile: Node3D): 45 | pass 46 | 47 | ## See [NetworkWeapon] 48 | func _spawn() -> Node3D: 49 | return null 50 | 51 | func _get_data(projectile: Node3D) -> Dictionary: 52 | return { 53 | "global_transform": projectile.global_transform 54 | } 55 | 56 | func _apply_data(projectile: Node3D, data: Dictionary): 57 | projectile.global_transform = data["global_transform"] 58 | 59 | func _is_reconcilable(projectile: Node3D, request_data: Dictionary, local_data: Dictionary) -> bool: 60 | var req_transform = request_data["global_transform"] as Transform3D 61 | var loc_transform = local_data["global_transform"] as Transform3D 62 | 63 | var request_pos = req_transform.origin 64 | var local_pos = loc_transform.origin 65 | 66 | return request_pos.distance_to(local_pos) < distance_threshold 67 | 68 | func _reconcile(projectile: Node3D, local_data: Dictionary, remote_data: Dictionary): 69 | var local_transform = local_data["global_transform"] as Transform3D 70 | var remote_transform = remote_data["global_transform"] as Transform3D 71 | 72 | var relative_transform = projectile.global_transform * local_transform.inverse() 73 | var final_transform = remote_transform * relative_transform 74 | 75 | projectile.global_transform = final_transform 76 | -------------------------------------------------------------------------------- /addons/netfox.extras/rewindable-random-number-generator.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name RewindableRandomNumberGenerator 3 | 4 | ## Provides methods for generating pseudo-random numbers in the rollback tick 5 | ## loop. 6 | ## 7 | ## Using a regular [RandomNumberGenerator] in [code]_rollback_tick()[/code] 8 | ## would generate different numbers on each peer. It also generates different 9 | ## numbers when resimulating the same tick. 10 | ## [br][br] 11 | ## This class solves all of the above, making it suitable for use during 12 | ## rollback. 13 | ## [br][br] 14 | ## The seed must be provided on instantiation, and must be the same on all peers 15 | ## for the random number generator to work properly. 16 | ## 17 | ## @tutorial(RewindableRandomNumberGenerator Guide): https://foxssake.github.io/netfox/latest/netfox.extras/guides/rewindable-random-number-generator/ 18 | 19 | var _rng: RandomNumberGenerator 20 | var _last_reset_tick := -1 21 | var _last_reset_rollback_tick := -1 22 | 23 | static var _logger := _NetfoxLogger.for_extras("RewindableRandomNumberGenerator") 24 | 25 | func _init(p_seed: int): 26 | _rng = RandomNumberGenerator.new() 27 | _rng.set_seed(p_seed) 28 | 29 | ## Returns a pseudo-random float between [code]0.0[/code] and [code]1.0[/code] 30 | ## (inclusive). 31 | func randf() -> float: 32 | _ensure_state() 33 | return _rng.randf() 34 | 35 | ## Returns a pseudo-random float between [code]from[/code] and [code]to[/code] 36 | ## (inclusive). 37 | func randf_range(from: float, to: float) -> float: 38 | _ensure_state() 39 | return _rng.randf_range(from, to) 40 | 41 | ## Returns a normally-distributed, pseudo-random floating-point number from the 42 | ## specified [code]mean[/code] and a standard [code]deviation[/code]. This is 43 | ## also known as a Gaussian distribution. 44 | func randfn(mean: float = 0.0, deviation: float = 1.0) -> float: 45 | _ensure_state() 46 | return _rng.randfn(mean, deviation) 47 | 48 | ## Returns a pseudo-random 32-bit unsigned integer between [code]0[/code] and 49 | ## [code]4294967295[/code] (inclusive). 50 | func randi() -> int: 51 | _ensure_state() 52 | return _rng.randi() 53 | 54 | ## Returns a pseudo-random 32-bit unsigned integer between [code]from[/code] and 55 | ## [code]to[/code] (inclusive). 56 | func randi_range(from: int, to: int) -> int: 57 | _ensure_state() 58 | return _rng.randi_range(from, to) 59 | 60 | func _ensure_state() -> void: 61 | if NetworkTime.tick == _last_reset_tick and NetworkRollback.tick == _last_reset_rollback_tick: 62 | # State already has been set 63 | return 64 | 65 | if NetworkRollback.is_rollback(): 66 | _rng.state = hash([_rng.seed, NetworkRollback.tick]) 67 | else: 68 | _rng.state = hash([_rng.seed, NetworkTime.tick]) 69 | 70 | _last_reset_rollback_tick = NetworkRollback.tick 71 | _last_reset_tick = NetworkTime.tick 72 | -------------------------------------------------------------------------------- /addons/netfox/properties/property-snapshot.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name _PropertySnapshot 3 | 4 | # Maps property paths to their values 5 | # Dictionary[String, Variant] 6 | var _snapshot: Dictionary = {} 7 | 8 | static var _logger := _NetfoxLogger.for_netfox("PropertySnapshot") 9 | 10 | func as_dictionary() -> Dictionary: 11 | return _snapshot.duplicate() 12 | 13 | static func from_dictionary(data: Dictionary) -> _PropertySnapshot: 14 | return _PropertySnapshot.new(data) 15 | 16 | func set_value(property_path: String, data: Variant) -> void: 17 | _snapshot[property_path] = data 18 | 19 | func get_value(property_path: String) -> Variant: 20 | return _snapshot.get(property_path) 21 | 22 | func properties() -> Array: 23 | return _snapshot.keys() 24 | 25 | func has(property_path: String) -> bool: 26 | return _snapshot.has(property_path) 27 | 28 | func size() -> int: 29 | return _snapshot.size() 30 | 31 | func equals(other: _PropertySnapshot): 32 | return _snapshot == other._snapshot 33 | 34 | func is_empty() -> bool: 35 | return _snapshot.is_empty() 36 | 37 | func apply(cache: PropertyCache) -> void: 38 | for property_path in _snapshot: 39 | var property_entry := cache.get_entry(property_path) 40 | var value = _snapshot[property_path] 41 | property_entry.set_value(value) 42 | 43 | func merge(data: _PropertySnapshot) -> _PropertySnapshot: 44 | var result := _snapshot.duplicate() 45 | for key in data.as_dictionary(): 46 | result[key] = data._snapshot[key] 47 | 48 | return _PropertySnapshot.from_dictionary(result) 49 | 50 | func make_patch(data: _PropertySnapshot) -> _PropertySnapshot: 51 | var result := {} 52 | 53 | for property_path in data.properties(): 54 | var old_property = get_value(property_path) 55 | var new_property = data.get_value(property_path) 56 | 57 | if old_property != new_property: 58 | result[property_path] = new_property 59 | 60 | return _PropertySnapshot.from_dictionary(result) 61 | 62 | func sanitize(sender: int, property_cache: PropertyCache) -> void: 63 | var sanitized := {} 64 | 65 | for property in _snapshot.keys(): 66 | var property_entry := property_cache.get_entry(property) 67 | var authority := property_entry.node.get_multiplayer_authority() 68 | 69 | if authority == sender: 70 | sanitized[property] = _snapshot[property] 71 | else: 72 | _logger.warning( 73 | "Received data for property %s, owned by %s, from sender %s", 74 | [ property, authority, sender ] 75 | ) 76 | 77 | _snapshot = sanitized 78 | 79 | static func extract(properties: Array[PropertyEntry]) -> _PropertySnapshot: 80 | var result = {} 81 | for property in properties: 82 | result[property.to_string()] = property.get_value() 83 | return _PropertySnapshot.from_dictionary(result) 84 | 85 | func _init(p_snapshot: Dictionary = {}) -> void: 86 | _snapshot = p_snapshot 87 | -------------------------------------------------------------------------------- /addons/netfox.extras/physics/physics-driver-toggles.gd: -------------------------------------------------------------------------------- 1 | extends Object 2 | 3 | class PhysicsDriverToggle: 4 | const INACTIVE_SUFFIX := ".off" 5 | 6 | func get_name() -> String: 7 | return "???" 8 | 9 | func get_files() -> Array[String]: 10 | return [] 11 | 12 | func get_error_messages() -> Array[String]: 13 | return [] 14 | 15 | func is_enabled() -> bool: 16 | return get_files().any(func(it): return FileAccess.file_exists(it)) 17 | 18 | func toggle() -> Array[String]: 19 | var errors := get_error_messages() 20 | if not errors.is_empty(): 21 | return errors 22 | 23 | var enable := not is_enabled() 24 | 25 | var uid_files := get_files().map(func(it): return it + ".uid") 26 | var renames = (get_files() + uid_files).map(func(it): 27 | if enable: return [it + INACTIVE_SUFFIX, it] 28 | else: return [it, it + INACTIVE_SUFFIX] 29 | ) 30 | 31 | for rename in renames: 32 | var result := DirAccess.rename_absolute(rename[0], rename[1]) 33 | if result != OK: 34 | errors.append( 35 | "Failed rename \"%s\" -> \"%s\"; reason: %s" % 36 | [rename[0], rename[1], error_string(result)] 37 | ) 38 | return errors 39 | 40 | class Rapier2DPhysicsDriverToggle extends PhysicsDriverToggle: 41 | func get_name() -> String: 42 | return "Rapier2D" 43 | 44 | func get_files() -> Array[String]: 45 | return [ 46 | "res://addons/netfox.extras/physics/rapier_driver_2d.gd", 47 | ] 48 | 49 | func get_error_messages() -> Array[String]: 50 | if not ClassDB.class_exists("RapierPhysicsServer2D"): 51 | return ["Rapier physics is not available! Is the extension installed?"] 52 | return [] 53 | 54 | class Rapier3DPhysicsDriverToggle extends PhysicsDriverToggle: 55 | func get_name() -> String: 56 | return "Rapier3D" 57 | 58 | func get_files() -> Array[String]: 59 | return [ 60 | "res://addons/netfox.extras/physics/rapier_driver_3d.gd", 61 | ] 62 | 63 | func get_error_messages() -> Array[String]: 64 | if not ClassDB.class_exists("RapierPhysicsServer3D"): 65 | return ["Rapier physics is not available! Is the extension installed?"] 66 | return [] 67 | 68 | class GodotPhysicsDriverToggle extends PhysicsDriverToggle: 69 | func get_name() -> String: 70 | return "Godot" 71 | 72 | func get_files() -> Array[String]: 73 | return [ 74 | "res://addons/netfox.extras/physics/godot_driver_3d.gd", 75 | "res://addons/netfox.extras/physics/godot_driver_2d.gd" 76 | ] 77 | 78 | func get_error_messages() -> Array[String]: 79 | if not PhysicsServer3D.has_method("space_step") or not PhysicsServer2D.has_method("space_step"): 80 | return ["Physics stepping is not available! Is this the right Godot build?"] 81 | return [] 82 | 83 | static func all() -> Array[PhysicsDriverToggle]: 84 | return [ 85 | Rapier2DPhysicsDriverToggle.new(), 86 | Rapier3DPhysicsDriverToggle.new(), 87 | GodotPhysicsDriverToggle.new() 88 | ] 89 | -------------------------------------------------------------------------------- /addons/netfox.extras/physics/physics_driver.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | class_name PhysicsDriver 4 | 5 | # Physics driver based on netfox ticks 6 | # Step physics in time with netfox and participates in rollback 7 | 8 | var physics_space: RID 9 | var snapshots: Dictionary = {} 10 | 11 | # Number of physics steps to take per network tick 12 | @export var physics_factor: int = 2 13 | # Snapshot and Rollback entire physics space. 14 | @export var rollback_physics_space: bool = true 15 | 16 | func _enter_tree(): 17 | #regular ticks 18 | NetworkTime.before_tick.connect(before_tick) 19 | NetworkTime.after_tick_loop.connect(after_tick_loop) 20 | 21 | #rollback ticks 22 | if rollback_physics_space: 23 | NetworkRollback.on_prepare_tick.connect(on_prepare_tick) 24 | NetworkRollback.on_process_tick.connect(on_process_tick) 25 | 26 | func _exit_tree(): 27 | NetworkTime.before_tick.disconnect(before_tick) 28 | NetworkTime.after_tick_loop.disconnect(after_tick_loop) 29 | 30 | #rollback ticks 31 | if NetworkRollback.on_prepare_tick.is_connected(on_prepare_tick): 32 | NetworkRollback.on_prepare_tick.disconnect(on_prepare_tick) 33 | NetworkRollback.on_process_tick.disconnect(on_process_tick) 34 | 35 | func _ready() -> void: 36 | _init_physics_space() 37 | 38 | # Emitted before a tick is run. 39 | func before_tick(_delta: float, tick: int) -> void: 40 | _snapshot_space(tick) 41 | step_physics(_delta) 42 | 43 | func on_prepare_tick(tick: int) -> void: 44 | if NetworkRollback._rollback_from == tick: 45 | # First tick of rollback loop, rewind 46 | _rollback_space(tick) 47 | else: 48 | # Subsequent ticks are re-writing history. 49 | _snapshot_space(tick) 50 | 51 | func on_process_tick(_tick: int) -> void: 52 | step_physics(NetworkTime.ticktime) 53 | 54 | func after_tick_loop() -> void: 55 | # Remove old snapshots 56 | for i in snapshots.keys(): 57 | if i < NetworkRollback.history_start: 58 | snapshots.erase(i) 59 | 60 | func step_physics(_delta: float) -> void: 61 | # Break up physics into smaller steps if needed 62 | var frac_delta = _delta / physics_factor 63 | var rollback_participants = get_tree().get_nodes_in_group("network_rigid_body") 64 | for i in range(physics_factor): 65 | for net_rigid_body in rollback_participants: 66 | net_rigid_body._physics_rollback_tick(frac_delta, NetworkTime.tick) 67 | 68 | _physics_step(frac_delta) 69 | 70 | ## Override this method to initialize the physics space. 71 | func _init_physics_space() -> void: 72 | pass 73 | 74 | ## Override this method to take one step in the physics space. 75 | ## [br][br] 76 | ## It should also flush and update all Godot nodes 77 | func _physics_step(_delta) -> void: 78 | pass 79 | 80 | ## Override this method to record the current state of the physics space. 81 | func _snapshot_space(_tick: int) -> void: 82 | pass 83 | 84 | ## Override this method to restore the physics space to a previous state. 85 | func _rollback_space(_tick) -> void: 86 | pass 87 | -------------------------------------------------------------------------------- /addons/netfox.extras/physics/godot_driver_3d.gd: -------------------------------------------------------------------------------- 1 | extends PhysicsDriver 2 | class_name PhysicsDriver3D 3 | 4 | # Physics driver based on netfox ticks 5 | # Requires a custom build of Godot with https://github.com/godotengine/godot/pull/76462 6 | 7 | # Maps ticks ( int ) to global snapshots ( Dictionary ) 8 | var scene_collision_objects: Array = [] 9 | 10 | func _init_physics_space() -> void: 11 | physics_space = get_viewport().world_3d.space 12 | PhysicsServer3D.space_set_active(physics_space, false) 13 | 14 | get_tree().node_added.connect(node_added) 15 | get_tree().node_removed.connect(node_removed) 16 | scan_tree() 17 | 18 | func _physics_step(delta) -> void: 19 | PhysicsServer3D.space_flush_queries(physics_space) 20 | PhysicsServer3D.space_step(physics_space, delta) 21 | 22 | func _snapshot_space(tick: int) -> void: 23 | # Maps RIDs to physics state ( Array ) 24 | var rid_states := {} 25 | for element in scene_collision_objects: 26 | var rid = element.get_rid() 27 | rid_states[rid] = get_body_states(rid) 28 | 29 | snapshots[tick] = rid_states 30 | 31 | func _rollback_space(tick) -> void: 32 | if snapshots.has(tick): 33 | var rid_states = snapshots[tick] 34 | for rid in rid_states.keys(): 35 | set_body_states(rid, rid_states[rid]) 36 | 37 | for body in scene_collision_objects: 38 | if body is CharacterBody3D or body is AnimatableBody3D: 39 | body.force_update_transform() 40 | 41 | 42 | func get_body_states(rid: RID) -> Array: 43 | var body_state: Array = [Vector3.ZERO, Quaternion.IDENTITY, Vector3.ZERO, Vector3.ZERO] 44 | body_state[0] = PhysicsServer3D.body_get_state(rid, PhysicsServer3D.BODY_STATE_TRANSFORM) 45 | body_state[1] = PhysicsServer3D.body_get_state(rid, PhysicsServer3D.BODY_STATE_LINEAR_VELOCITY) 46 | body_state[2] = PhysicsServer3D.body_get_state(rid, PhysicsServer3D.BODY_STATE_ANGULAR_VELOCITY) 47 | body_state[3] = PhysicsServer3D.body_get_state(rid, PhysicsServer3D.BODY_STATE_SLEEPING) 48 | return body_state 49 | 50 | func set_body_states(rid: RID, body_state: Array) -> void: 51 | PhysicsServer3D.body_set_state(rid, PhysicsServer3D.BODY_STATE_TRANSFORM, body_state[0]) 52 | PhysicsServer3D.body_set_state(rid, PhysicsServer3D.BODY_STATE_LINEAR_VELOCITY, body_state[1]) 53 | PhysicsServer3D.body_set_state(rid, PhysicsServer3D.BODY_STATE_ANGULAR_VELOCITY, body_state[2]) 54 | PhysicsServer3D.body_set_state(rid, PhysicsServer3D.BODY_STATE_SLEEPING, body_state[3]) 55 | 56 | func scan_tree(): 57 | scene_collision_objects.clear() 58 | scene_collision_objects = get_all_children(get_node('/root')) 59 | 60 | func get_all_children(in_node: Node) -> Array: 61 | var nodes = [] 62 | nodes = in_node.find_children("*", "PhysicsBody3D", true, false) 63 | return nodes 64 | 65 | func node_added(node: Node) -> void: 66 | if node is PhysicsBody3D: 67 | scene_collision_objects.append(node) 68 | 69 | func node_removed(node: Node) -> void: 70 | if node is PhysicsBody3D: 71 | scene_collision_objects.erase(node) 72 | -------------------------------------------------------------------------------- /scenes/confetti.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=14 format=3 uid="uid://ds50rqn0gl2jl"] 2 | 3 | [ext_resource type="Script" uid="uid://bxr4tbk4dq8v0" path="res://scripts/confetti.gd" id="1_xjk0n"] 4 | 5 | [sub_resource type="Curve" id="Curve_du648"] 6 | _data = [Vector2(0.0107817, 1), 0.0, 0.0, 0, 0, Vector2(0.762803, 0.801996), 0.0, 0.0, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0] 7 | point_count = 3 8 | 9 | [sub_resource type="CurveTexture" id="CurveTexture_xjk0n"] 10 | curve = SubResource("Curve_du648") 11 | 12 | [sub_resource type="Gradient" id="Gradient_xejpt"] 13 | colors = PackedColorArray(0.779291, 0.779291, 0.779291, 0.329412, 1, 1, 1, 1) 14 | 15 | [sub_resource type="GradientTexture1D" id="GradientTexture1D_2hbhx"] 16 | gradient = SubResource("Gradient_xejpt") 17 | 18 | [sub_resource type="Gradient" id="Gradient_e8opl"] 19 | offsets = PackedFloat32Array(0, 0.501449, 1) 20 | colors = PackedColorArray(1, 0, 0, 1, 0.38556, 0.57763, 1, 1, 1, 1, 0, 1) 21 | 22 | [sub_resource type="GradientTexture1D" id="GradientTexture1D_v30ou"] 23 | gradient = SubResource("Gradient_e8opl") 24 | 25 | [sub_resource type="Curve" id="Curve_k3xkt"] 26 | _data = [Vector2(0, 0.624475), 0.0, 0.0, 0, 0, Vector2(0.490566, 1), 0.0, 0.0, 0, 0, Vector2(1, 0.63813), 0.0, 0.0, 0, 0] 27 | point_count = 3 28 | 29 | [sub_resource type="CurveTexture" id="CurveTexture_lnt6a"] 30 | curve = SubResource("Curve_k3xkt") 31 | 32 | [sub_resource type="ParticleProcessMaterial" id="ParticleProcessMaterial_du648"] 33 | particle_flag_align_y = true 34 | angle_min = -360.0 35 | angle_max = 360.0 36 | direction = Vector3(0, 0, 0) 37 | spread = 180.0 38 | initial_velocity_min = 5.0 39 | initial_velocity_max = 20.0 40 | linear_accel_min = 1.2 41 | linear_accel_max = 14.94 42 | color_ramp = SubResource("GradientTexture1D_v30ou") 43 | color_initial_ramp = SubResource("GradientTexture1D_2hbhx") 44 | alpha_curve = SubResource("CurveTexture_xjk0n") 45 | emission_curve = SubResource("CurveTexture_lnt6a") 46 | hue_variation_min = -0.12 47 | hue_variation_max = 0.14 48 | 49 | [sub_resource type="Curve" id="Curve_xjk0n"] 50 | _data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0] 51 | point_count = 2 52 | 53 | [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_xjk0n"] 54 | vertex_color_use_as_albedo = true 55 | emission = Color(1, 1, 1, 1) 56 | emission_energy_multiplier = 4.15 57 | emission_operator = 1 58 | billboard_keep_scale = true 59 | 60 | [sub_resource type="RibbonTrailMesh" id="RibbonTrailMesh_xejpt"] 61 | material = SubResource("StandardMaterial3D_xjk0n") 62 | size = 0.1 63 | sections = 6 64 | curve = SubResource("Curve_xjk0n") 65 | 66 | [node name="Confetti" type="GPUParticles3D"] 67 | emitting = false 68 | amount = 300 69 | lifetime = 3.0 70 | one_shot = true 71 | explosiveness = 1.0 72 | process_material = SubResource("ParticleProcessMaterial_du648") 73 | draw_pass_1 = SubResource("RibbonTrailMesh_xejpt") 74 | script = ExtResource("1_xjk0n") 75 | 76 | [connection signal="finished" from="." to="." method="_on_finished"] 77 | -------------------------------------------------------------------------------- /addons/netfox/icons/rewindable-action.svg: -------------------------------------------------------------------------------- 1 | 2 | 34 | 43 | 44 | 45 | 46 | 52 | 56 | 60 | 90 | -------------------------------------------------------------------------------- /addons/netfox.extras/physics/godot_driver_2d.gd: -------------------------------------------------------------------------------- 1 | extends PhysicsDriver 2 | 3 | class_name PhysicsDriver2D 4 | 5 | # Physics driver based on netfox ticks 6 | # Requires a custom build of Godot with https://github.com/godotengine/godot/pull/76462 7 | 8 | var scene_collision_objects: Array = [] 9 | var collision_objects_snapshots: Dictionary[int, Dictionary] = {} 10 | 11 | func _init_physics_space() -> void: 12 | physics_space = get_viewport().world_2d.space 13 | PhysicsServer2D.space_set_active(physics_space, false) 14 | 15 | get_tree().node_added.connect(node_added) 16 | get_tree().node_removed.connect(node_removed) 17 | scan_tree() 18 | 19 | func _physics_step(delta) -> void: 20 | PhysicsServer2D.space_flush_queries(physics_space) 21 | PhysicsServer2D.space_step(physics_space, delta) 22 | 23 | func _snapshot_space(tick: int) -> void: 24 | var rid_states: Dictionary[RID, Array] = {} 25 | for element in scene_collision_objects: 26 | if element is CharacterBody2D: 27 | element.force_update_transform() # force colliders to update 28 | 29 | var rid = element.get_rid() 30 | rid_states[rid] = get_body_states(rid) 31 | snapshots[tick] = rid_states 32 | 33 | func _rollback_space(tick) -> void: 34 | if snapshots.has(tick): 35 | var rid_states = snapshots[tick] 36 | for rid in rid_states.keys(): 37 | set_body_states(rid, rid_states[rid]) 38 | 39 | for body in scene_collision_objects: 40 | if body is CharacterBody2D or body is AnimatableBody2D: 41 | body.force_update_transform() # force colliders to update 42 | 43 | func get_body_states(rid: RID) -> Array: 44 | var body_state: Array = [Vector3.ZERO, Quaternion.IDENTITY, Vector3.ZERO, Vector3.ZERO] 45 | body_state[0] = PhysicsServer2D.body_get_state(rid, PhysicsServer2D.BODY_STATE_TRANSFORM) 46 | body_state[1] = PhysicsServer2D.body_get_state(rid, PhysicsServer2D.BODY_STATE_LINEAR_VELOCITY) 47 | body_state[2] = PhysicsServer2D.body_get_state(rid, PhysicsServer2D.BODY_STATE_ANGULAR_VELOCITY) 48 | body_state[3] = PhysicsServer2D.body_get_state(rid, PhysicsServer2D.BODY_STATE_SLEEPING) 49 | return body_state 50 | 51 | func set_body_states(rid: RID, body_state: Array) -> void: 52 | PhysicsServer2D.body_set_state(rid, PhysicsServer2D.BODY_STATE_TRANSFORM, body_state[0]) 53 | PhysicsServer2D.body_set_state(rid, PhysicsServer2D.BODY_STATE_LINEAR_VELOCITY, body_state[1]) 54 | PhysicsServer2D.body_set_state(rid, PhysicsServer2D.BODY_STATE_ANGULAR_VELOCITY, body_state[2]) 55 | PhysicsServer2D.body_set_state(rid, PhysicsServer2D.BODY_STATE_SLEEPING, body_state[3]) 56 | 57 | func scan_tree(): 58 | scene_collision_objects.clear() 59 | scene_collision_objects = get_all_children(get_node('/root')) 60 | 61 | func get_all_children(in_node: Node) -> Array: 62 | var nodes = [] 63 | nodes = in_node.find_children("*", "PhysicsBody2D", true, false) 64 | return nodes 65 | 66 | func node_added(node: Node) -> void: 67 | if node is PhysicsBody2D: 68 | scene_collision_objects.append(node) 69 | 70 | func node_removed(node: Node) -> void: 71 | if node is PhysicsBody2D: 72 | scene_collision_objects.erase(node) 73 | -------------------------------------------------------------------------------- /addons/netfox.extras/icons/rewindable-state-machine.svg: -------------------------------------------------------------------------------- 1 | 2 | 40 | 48 | 49 | 65 | 66 | 67 | 90 | -------------------------------------------------------------------------------- /addons/netfox/time/network-tickrate-handshake.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | class_name NetworkTickrateHandshake 3 | 4 | ## Internal class to manage the tickrate handshake. 5 | ## 6 | ## Whenever a new peer joins, they exchange their configured tickrate with the 7 | ## host. If the tickrate mismatches, a warning is emitted by default, as this is 8 | ## assumed to be a developer mistake. 9 | ## [br][br] 10 | ## However, if this is expected, different actions can be configured. 11 | 12 | ## Emit a warning on tickrate mismatch 13 | const WARN := 0 14 | 15 | ## Disconnect peer on tickrate mismatch[br] 16 | ## This is enforced by the host. 17 | const DISCONNECT := 1 18 | 19 | ## Adjust tickrate to the host's on mismatch 20 | const ADJUST := 2 21 | 22 | ## Emit [signal on_tickrate_mismatch] on mismatch[br] 23 | ## This is emitted on both host and client. 24 | const SIGNAL := 3 25 | 26 | ## Configures what happens on a tickrate mismatch.[br] 27 | ## Defaults to [constant WARN], based on project settings. 28 | var mismatch_action: int = ProjectSettings.get_setting(&"netfox/time/tickrate_mismatch_action", WARN) 29 | 30 | static var _logger := _NetfoxLogger.for_netfox("NetworkTickrateHandshake") 31 | 32 | ## Emitted when a tickrate mismatch is encountered, and [member mismatch_action] is set to 33 | ## [constant SIGNAL]. 34 | signal on_tickrate_mismatch(peer: int, tickrate: int) 35 | 36 | ## Run the tickrate handshake. 37 | ## [br][br] 38 | ## This will connect to signals, so that every new peer receives tickrate info 39 | ## from the host. 40 | ## [br][br] 41 | ## Called by [_NetworkTime], no need to call manually. 42 | func run() -> void: 43 | if multiplayer.is_server(): 44 | # Broadcast tickrate 45 | _submit_tickrate.rpc(NetworkTime.tickrate) 46 | 47 | # Submit tickrate to anyone joining 48 | multiplayer.peer_connected.connect(_handle_new_peer) 49 | else: 50 | # Submit tickrate to host 51 | _submit_tickrate.rpc_id(1, NetworkTime.tickrate) 52 | 53 | ## Stop the tickrate handshake. 54 | ## [br][br] 55 | ## Called by [_NetworkTime], no need to call manually. 56 | func stop() -> void: 57 | if multiplayer.is_server(): 58 | multiplayer.peer_connected.disconnect(_handle_new_peer) 59 | 60 | func _ready() -> void: 61 | name = "NetworkTickrateHandshake" 62 | 63 | func _handle_new_peer(peer: int) -> void: 64 | if multiplayer.is_server(): 65 | _submit_tickrate.rpc_id(peer, NetworkTime.tickrate) 66 | 67 | func _handle_tickrate_mismatch(peer: int, tickrate: int) -> void: 68 | match mismatch_action: 69 | WARN: 70 | _logger.warning( 71 | "Local tickrate %dtps differs from tickrate of peer #%d at %dtps! " + 72 | "Make sure that tickrates are correctly configured in the Project settings! " + 73 | "See netfox/Time/Tickrate.", [ 74 | NetworkTime.tickrate, peer, tickrate 75 | ]) 76 | DISCONNECT: 77 | if multiplayer.is_server(): 78 | _logger.warning("Peer #%d's tickrate of %dtps differs from expected %dtps! Disconnecting.", [ 79 | peer, tickrate, NetworkTime.tickrate 80 | ]) 81 | multiplayer.multiplayer_peer.disconnect_peer(peer) 82 | ADJUST: 83 | if not multiplayer.is_server(): 84 | _logger.info("Local tickrate %dtps differs from tickrate of host at %dtps! Adjusting.", [ 85 | NetworkTime.tickrate, tickrate 86 | ]) 87 | # TODO: Make tickrate mutable at user's digression 88 | ProjectSettings.set_setting(&"netfox/time/tickrate", tickrate) 89 | SIGNAL: 90 | on_tickrate_mismatch.emit(peer, tickrate) 91 | 92 | @rpc("any_peer", "reliable", "call_remote") 93 | func _submit_tickrate(tickrate: int) -> void: 94 | var sender := multiplayer.get_remote_sender_id() 95 | _logger.debug("Received tickrate %d from peer %d", [tickrate, sender]) 96 | 97 | if tickrate != NetworkTime.tickrate: 98 | _handle_tickrate_mismatch(sender, tickrate) 99 | -------------------------------------------------------------------------------- /addons/netfox/interpolators.gd: -------------------------------------------------------------------------------- 1 | extends Object 2 | class_name Interpolators 3 | 4 | class Interpolator: 5 | var is_applicable: Callable 6 | var apply: Callable 7 | 8 | static func make(is_applicable: Callable, apply: Callable) -> Interpolator: 9 | var result := Interpolator.new() 10 | result.is_applicable = is_applicable 11 | result.apply = apply 12 | return result 13 | 14 | static var DEFAULT_INTERPOLATOR := Interpolator.make( 15 | func (v): return true, 16 | func (a, b, f): return a if f < 0.5 else b 17 | ) 18 | 19 | static var interpolators: Array[Interpolator] 20 | static var default_apply: Callable = func(a, b, f): a if f < 0.5 else b 21 | 22 | ## Register an interpolator. 23 | ## 24 | ## New interpolators are pushed to the front of the list, making them have 25 | ## precedence over existing ones. This can be useful in case you want to override 26 | ## the built-in interpolators. 27 | static func register(is_applicable: Callable, apply: Callable) -> void: 28 | interpolators.push_front(Interpolator.make(is_applicable, apply)) 29 | 30 | ## Find the appropriate interpolator for the given value. 31 | ## 32 | ## If none was found, the default interpolator is returned. 33 | static func find_for(value) -> Callable: 34 | for interpolator in interpolators: 35 | if interpolator.is_applicable.call(value): 36 | return interpolator.apply 37 | 38 | return DEFAULT_INTERPOLATOR.apply 39 | 40 | ## Interpolate between two values. 41 | ## 42 | ## Note, that it is usually faster to just cache the Callable returned by find_for 43 | ## and call that, instead of calling interpolate repeatedly. The latter will have 44 | ## to lookup the appropriate interpolator on every call. 45 | static func interpolate(a, b, f: float): 46 | return find_for(a).call(a, b, f) 47 | 48 | static func _static_init() -> void: 49 | # Register built-in interpolators 50 | # Float 51 | register( 52 | func(a): return a is float, 53 | func(a: float, b: float, f: float): return lerpf(a, b, f) 54 | ) 55 | 56 | # Int 57 | register( 58 | func(a): return a is int, 59 | func(a: int, b: int, f: float): return int(lerpf(a, b, f)) 60 | ) 61 | 62 | # Vector 63 | register( 64 | func(a): return a is Vector2, 65 | func(a: Vector2, b: Vector2, f: float): return a.lerp(b, f) 66 | ) 67 | register( 68 | func(a): return a is Vector3, 69 | func(a: Vector3, b: Vector3, f: float): return a.lerp(b, f) 70 | ) 71 | register( 72 | func(a): return a is Vector4, 73 | func(a: Vector4, b: Vector4, f: float): return a.lerp(b, f) 74 | ) 75 | 76 | register( 77 | func(a): return a is Vector2i, 78 | func(a: Vector2i, b: Vector2i, f: float): return Vector2i(Vector2(a).lerp(b, f)) 79 | ) 80 | register( 81 | func(a): return a is Vector3i, 82 | func(a: Vector3i, b: Vector3i, f: float): return Vector3i(Vector3(a).lerp(b, f)) 83 | ) 84 | register( 85 | func(a): return a is Vector4i, 86 | func(a: Vector4i, b: Vector4i, f: float): return Vector4i(Vector4(a).lerp(b, f)) 87 | ) 88 | 89 | # Transform 90 | register( 91 | func(a): return a is Transform2D, 92 | func(a: Transform2D, b: Transform2D, f: float): return a.interpolate_with(b, f) 93 | ) 94 | register( 95 | func(a): return a is Transform3D, 96 | func(a: Transform3D, b: Transform3D, f: float): return a.interpolate_with(b, f) 97 | ) 98 | 99 | # Quaternion 100 | register( 101 | func(a): return a is Quaternion, 102 | func(a: Quaternion, b: Quaternion, f: float): return a.slerp(b, f) 103 | ) 104 | 105 | # Basis 106 | register( 107 | func(a): return a is Basis, 108 | func(a: Basis, b: Basis, f: float): return a.slerp(b, f) 109 | ) 110 | 111 | # Color 112 | register( 113 | func(a): return a is Color, 114 | func(a: Color, b: Color, f: float): return a.lerp(b, f) 115 | ) 116 | -------------------------------------------------------------------------------- /project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=5 10 | 11 | [application] 12 | 13 | config/name="Physics Test" 14 | run/main_scene="res://scenes/main.tscn" 15 | config/features=PackedStringArray("4.5", "Forward Plus") 16 | config/icon="res://icon.svg" 17 | 18 | [autoload] 19 | 20 | WindowTiler="*res://addons/netfox.extras/window-tiler.gd" 21 | Noray="*res://addons/netfox.noray/noray.gd" 22 | PacketHandshake="*res://addons/netfox.noray/packet-handshake.gd" 23 | NetworkTime="*res://addons/netfox/network-time.gd" 24 | NetworkTimeSynchronizer="*res://addons/netfox/network-time-synchronizer.gd" 25 | NetworkRollback="*res://addons/netfox/rollback/network-rollback.gd" 26 | NetworkEvents="*res://addons/netfox/network-events.gd" 27 | NetworkPerformance="*res://addons/netfox/network-performance.gd" 28 | NetworkSimulator="*res://addons/netfox.extras/network-simulator.gd" 29 | 30 | [editor_plugins] 31 | 32 | enabled=PackedStringArray("res://addons/netfox.extras/plugin.cfg", "res://addons/netfox.internals/plugin.cfg", "res://addons/netfox/plugin.cfg") 33 | 34 | [global_group] 35 | 36 | players="" 37 | 38 | [input] 39 | 40 | up={ 41 | "deadzone": 0.5, 42 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"location":0,"echo":false,"script":null) 43 | ] 44 | } 45 | down={ 46 | "deadzone": 0.5, 47 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null) 48 | ] 49 | } 50 | left={ 51 | "deadzone": 0.5, 52 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null) 53 | ] 54 | } 55 | right={ 56 | "deadzone": 0.5, 57 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null) 58 | ] 59 | } 60 | quit={ 61 | "deadzone": 0.5, 62 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194305,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) 63 | ] 64 | } 65 | jump={ 66 | "deadzone": 0.2, 67 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null) 68 | ] 69 | } 70 | 71 | [layer_names] 72 | 73 | 3d_physics/layer_1="floor" 74 | 3d_physics/layer_2="player" 75 | 3d_physics/layer_3="ball" 76 | 3d_physics/layer_4="goal" 77 | 78 | [netfox] 79 | 80 | extras/auto_tile_windows=true 81 | extras/borderless=true 82 | extras/screen=1 83 | 84 | [physics] 85 | 86 | common/max_physics_steps_per_frame=64 87 | 3d/physics_engine="Jolt Physics" 88 | -------------------------------------------------------------------------------- /addons/netfox/encoder/redundant-history-encoder.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name _RedundantHistoryEncoder 3 | 4 | var redundancy: int = 4: 5 | get = get_redundancy, 6 | set = set_redundancy 7 | 8 | var _history: _PropertyHistoryBuffer 9 | var _properties: Array[PropertyEntry] 10 | var _property_cache: PropertyCache 11 | 12 | var _version := 0 13 | var _has_received := false 14 | 15 | var _logger := _NetfoxLogger.for_netfox("RedundantHistoryEncoder") 16 | 17 | func get_redundancy() -> int: 18 | return redundancy 19 | 20 | func set_redundancy(p_redundancy: int): 21 | if p_redundancy <= 0: 22 | _logger.warning( 23 | "Attempting to set redundancy to %d, which would send no data!", [p_redundancy] 24 | ) 25 | return 26 | 27 | redundancy = p_redundancy 28 | 29 | func set_properties(properties: Array[PropertyEntry]) -> void: 30 | if _properties != properties: 31 | _version = (_version + 1) % 256 32 | _properties = properties.duplicate() 33 | 34 | func encode(tick: int, properties: Array[PropertyEntry]) -> Array: 35 | if _history.is_empty(): 36 | return [] 37 | var data := [] 38 | 39 | for i in range(mini(redundancy, _history.size())): 40 | var offset_tick := tick - i 41 | if offset_tick < _history.get_earliest_tick(): 42 | break 43 | 44 | var snapshot := _history.get_snapshot(offset_tick) 45 | for property in properties: 46 | data.append(snapshot.get_value(property.to_string())) 47 | 48 | data.append(_version) 49 | return data 50 | 51 | func decode(data: Array, properties: Array[PropertyEntry]) -> Array[_PropertySnapshot]: 52 | if data.is_empty() or properties.is_empty(): 53 | return [] 54 | 55 | var packet_version := data.pop_back() as int 56 | 57 | if packet_version != _version: 58 | if not _has_received: 59 | # First packet, assume version is OK 60 | _version = packet_version 61 | else: 62 | # Version mismatch, can't parse 63 | _logger.warning("Version mismatch! own: %d, received: %s", [_version, packet_version]) 64 | return [] 65 | 66 | var result: Array[_PropertySnapshot] = [] 67 | var redundancy := data.size() / properties.size() 68 | result.assign(range(redundancy) 69 | .map(func(__): return _PropertySnapshot.new()) 70 | ) 71 | 72 | for i in range(data.size()): 73 | var offset_idx := i / properties.size() 74 | var prop_idx := i % properties.size() 75 | 76 | result[offset_idx].set_value(properties[prop_idx].to_string(), data[i]) 77 | 78 | _has_received = true 79 | 80 | return result 81 | 82 | # Returns earliest new tick as int, or -1 if no new ticks applied 83 | func apply(tick: int, snapshots: Array[_PropertySnapshot], sender: int = 0) -> int: 84 | var earliest_new_tick = -1 85 | 86 | for i in range(snapshots.size()): 87 | var offset_tick := tick - i 88 | var snapshot := snapshots[i] 89 | 90 | if offset_tick < NetworkRollback.history_start: 91 | # Data too old 92 | _logger.warning( 93 | "Received data for %s, rejecting because older than %s frames", 94 | [offset_tick, NetworkRollback.history_limit] 95 | ) 96 | continue 97 | 98 | if sender > 0: 99 | snapshot.sanitize(sender, _property_cache) 100 | if snapshot.is_empty(): 101 | # No valid properties ( probably after sanitize ) 102 | _logger.warning("Received invalid data from %d for tick %d", [sender, tick]) 103 | continue 104 | 105 | var known_snapshot := _history.get_snapshot(offset_tick) 106 | if not known_snapshot.equals(snapshot): 107 | # Received a new snapshot, store and emit signal 108 | _history.set_snapshot(offset_tick, snapshot) 109 | earliest_new_tick = offset_tick 110 | 111 | return earliest_new_tick 112 | 113 | 114 | func _init(p_history: _PropertyHistoryBuffer, p_property_cache: PropertyCache): 115 | _history = p_history 116 | _property_cache = p_property_cache 117 | -------------------------------------------------------------------------------- /addons/netfox.extras/weapon/network-weapon-hitscan-3d.gd: -------------------------------------------------------------------------------- 1 | extends Node3D 2 | class_name NetworkWeaponHitscan3D 3 | 4 | ## A 3D-specific implementation of a networked hitscan (raycast) weapon. 5 | 6 | ## Maximum distance to cast the ray 7 | @export var max_distance: float = 1000.0 8 | 9 | ## Mask used to detect raycast hits 10 | @export_flags_3d_physics var collision_mask: int = 0xFFFFFFFF 11 | 12 | ## Colliders excluded from raycast hits 13 | @export var exclude: Array[RID] = [] 14 | 15 | var _weapon: _NetworkWeaponProxy 16 | 17 | ## Try to fire the weapon and return the projectile. 18 | ## [br][br] 19 | ## Returns true if the weapon was fired. 20 | func fire() -> bool: 21 | if not can_fire(): 22 | return false 23 | 24 | _apply_data(_get_data()) 25 | _after_fire() 26 | return true 27 | 28 | ## Check whether this weapon can be fired. 29 | func can_fire() -> bool: 30 | return _weapon.can_fire() 31 | 32 | func _init(): 33 | _weapon = _NetworkWeaponProxy.new() 34 | add_child(_weapon, true, INTERNAL_MODE_BACK) 35 | _weapon.owner = self 36 | 37 | _weapon.c_can_fire = _can_fire 38 | _weapon.c_can_peer_use = _can_peer_use 39 | _weapon.c_after_fire = _after_fire 40 | _weapon.c_spawn = _spawn 41 | _weapon.c_get_data = _get_data 42 | _weapon.c_apply_data = _apply_data 43 | _weapon.c_is_reconcilable = _is_reconcilable 44 | _weapon.c_reconcile = _reconcile 45 | 46 | ## Override this method with your own can fire logic. 47 | ## [br][br] 48 | ## See [NetworkWeapon]. 49 | func _can_fire() -> bool: 50 | return true 51 | 52 | ## Override this method to check if a given peer can use this weapon. 53 | ## [br][br] 54 | ## See [NetworkWeapon]. 55 | func _can_peer_use(peer_id: int) -> bool: 56 | return true 57 | 58 | ## Override this method to run any logic needed after successfully firing the 59 | ## weapon. 60 | ## [br][br] 61 | ## See [NetworkWeapon]. 62 | func _after_fire(): 63 | pass 64 | 65 | func _spawn(): 66 | # No projectile is spawned for a hitscan weapon. 67 | pass 68 | 69 | func _get_data() -> Dictionary: 70 | # Collect data needed to synchronize the firing event. 71 | return { 72 | "origin": global_transform.origin, 73 | "direction": -global_transform.basis.z # Assuming forward direction. 74 | } 75 | 76 | func _apply_data(data: Dictionary): 77 | # Reproduces the firing event on all peers. 78 | var origin = data["origin"] as Vector3 79 | var direction = data["direction"] as Vector3 80 | 81 | # Perform the raycast from origin in the given direction. 82 | var space_state = get_world_3d().direct_space_state 83 | 84 | # Create a PhysicsRayQueryParameters3D object. 85 | var ray_params = PhysicsRayQueryParameters3D.new() 86 | ray_params.from = origin 87 | ray_params.to = origin + direction * max_distance 88 | 89 | # Set collision masks or exclude objects: 90 | ray_params.collision_mask = collision_mask 91 | ray_params.exclude = exclude 92 | 93 | var result = space_state.intersect_ray(ray_params) 94 | 95 | if result: 96 | # Handle the hit result, such as spawning hit effects. 97 | _on_hit(result) 98 | 99 | # Play firing effects on all peers. 100 | _on_fire() 101 | 102 | func _is_reconcilable(request_data: Dictionary, local_data: Dictionary) -> bool: 103 | # Always reconcilable 104 | return true 105 | 106 | func _reconcile(local_data: Dictionary, remote_data: Dictionary): 107 | # Nothing to do on reconcile 108 | pass 109 | 110 | ## Override to implement raycast hit logic. 111 | ## [br][br] 112 | ## The parameter is the result of a 113 | ## [method PhysicsDirectSpaceState3D.intersect_ray] call. 114 | func _on_hit(result: Dictionary): 115 | # Implement hit effect logic here. 116 | # var hit_position = result.position 117 | # var hit_normal = result.normal 118 | # var collider = result.collider 119 | 120 | # For example, you might emit a signal or instantiate a hit effect scene: 121 | # emit_signal("hit_detected", hit_position, hit_normal, collider) 122 | pass 123 | 124 | ## Override to implement firing effects, like muzzle flash or sound. 125 | func _on_fire(): 126 | # Implement firing effect logic here. 127 | pass 128 | -------------------------------------------------------------------------------- /addons/netfox/rollback/composite/rollback-history-recorder.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name _RollbackHistoryRecorder 3 | 4 | # Provided externally by RBS 5 | var _state_history: _PropertyHistoryBuffer 6 | var _input_history: _PropertyHistoryBuffer 7 | 8 | var _state_property_config: _PropertyConfig 9 | var _input_property_config: _PropertyConfig 10 | 11 | var _property_cache: PropertyCache 12 | 13 | var _latest_state_tick: int 14 | var _skipset: _Set 15 | 16 | func configure( 17 | p_state_history: _PropertyHistoryBuffer, p_input_history: _PropertyHistoryBuffer, 18 | p_state_property_config: _PropertyConfig, p_input_property_config: _PropertyConfig, 19 | p_property_cache: PropertyCache, 20 | p_skipset: _Set 21 | ) -> void: 22 | _state_history = p_state_history 23 | _input_history = p_input_history 24 | _state_property_config = p_state_property_config 25 | _input_property_config = p_input_property_config 26 | _property_cache = p_property_cache 27 | _skipset = p_skipset 28 | 29 | func set_latest_state_tick(p_latest_state_tick: int) -> void: 30 | _latest_state_tick = p_latest_state_tick 31 | 32 | func apply_state(tick: int) -> void: 33 | # Apply state for tick 34 | var state = _state_history.get_history(tick) 35 | state.apply(_property_cache) 36 | 37 | func apply_display_state() -> void: 38 | apply_state(NetworkRollback.display_tick) 39 | 40 | func apply_tick(tick: int) -> void: 41 | var state := _state_history.get_history(tick) 42 | var input := _input_history.get_history(tick) 43 | 44 | state.apply(_property_cache) 45 | input.apply(_property_cache) 46 | 47 | func trim_history() -> void: 48 | # Trim history 49 | _state_history.trim() 50 | _input_history.trim() 51 | 52 | func record_input(tick: int) -> void: 53 | # Record input 54 | if not _get_recorded_input_props().is_empty(): 55 | var input = _PropertySnapshot.extract(_get_recorded_input_props()) 56 | var input_tick: int = tick + NetworkRollback.input_delay 57 | _input_history.set_snapshot(input_tick, input) 58 | 59 | func record_state(tick: int) -> void: 60 | # Record state for specified tick ( current + 1 ) 61 | # Check if any of the managed nodes were mutated 62 | var is_mutated := _get_recorded_state_props().any(func(pe): 63 | return NetworkRollback.is_mutated(pe.node, tick - 1)) 64 | 65 | var record_state := _PropertySnapshot.extract(_get_state_props_to_record(tick)) 66 | if record_state.size(): 67 | var merge_state := _state_history.get_history(tick - 1) 68 | _state_history.set_snapshot(tick, merge_state.merge(record_state)) 69 | 70 | func _should_record_tick(tick: int) -> bool: 71 | if _get_recorded_state_props().is_empty(): 72 | # Don't record tick if there's no props to record 73 | return false 74 | 75 | if _get_recorded_state_props().any(func(pe): 76 | return NetworkRollback.is_mutated(pe.node, tick - 1)): 77 | # If there's any node that was mutated, there's something to record 78 | return true 79 | 80 | # Otherwise, record only if we don't have authoritative state for the tick 81 | return tick > _latest_state_tick 82 | 83 | func _get_state_props_to_record(tick: int) -> Array[PropertyEntry]: 84 | if not _should_record_tick(tick): 85 | return [] 86 | if _skipset.is_empty(): 87 | return _get_recorded_state_props() 88 | 89 | return _get_recorded_state_props().filter(func(pe): return _should_record_property(pe, tick)) 90 | 91 | func _should_record_property(property_entry: PropertyEntry, tick: int) -> bool: 92 | if NetworkRollback.is_mutated(property_entry.node, tick - 1): 93 | return true 94 | if _skipset.has(property_entry.node): 95 | return false 96 | return true 97 | 98 | # ============================================================================= 99 | # Shared utils, extract later 100 | 101 | func _get_recorded_state_props() -> Array[PropertyEntry]: 102 | return _state_property_config.get_properties() 103 | 104 | func _get_owned_state_props() -> Array[PropertyEntry]: 105 | return _state_property_config.get_owned_properties() 106 | 107 | func _get_recorded_input_props() -> Array[PropertyEntry]: 108 | return _input_property_config.get_owned_properties() 109 | 110 | func _get_owned_input_props() -> Array[PropertyEntry]: 111 | return _input_property_config.get_owned_properties() 112 | -------------------------------------------------------------------------------- /scripts/player.gd: -------------------------------------------------------------------------------- 1 | extends CharacterBody3D 2 | 3 | class_name Player 4 | 5 | @export var wheel_base: float = 0.6 # distance between front/rear axles 6 | @export var steering_limit: float = 10.0 # front wheel max turning angle (deg) 7 | @export var engine_power: float = -10.0 8 | @export var braking: float = 12.0 9 | @export var friction: float = -2 # friction coefficient 10 | @export var drag: float = -2 # drag coefficient 11 | @export var max_speed_reverse: float = 4 12 | @export var max_speed_forward: float = 15 13 | 14 | # Drifting 15 | @export var slip_speed = 11.0 # lose traction above this speed 16 | @export var traction_slow: float = 0.8 # traction coefficient when not drifting 17 | @export var traction_fast: float = 0.15 # traction coefficient when drifting 18 | 19 | 20 | # Car state properties 21 | @export var acceleration = Vector3.ZERO # current acceleration 22 | @export var drifting = false 23 | @export var direction = 0.0 # current direction, negative is forward 24 | @export var steer_angle = 0.0 # current wheel angle 25 | @export var driver: String = "player" 26 | 27 | var gravity = ProjectSettings.get_setting("physics/3d/default_gravity") 28 | 29 | @onready var inputs: PlayerInput = $Input 30 | @onready var rollback_synchronizer: RollbackSynchronizer = $RollbackSynchronizer 31 | #@onready var physics: RapierDriver = get_node(^"../../RapierDriver") 32 | 33 | func _ready(): 34 | print(multiplayer.get_unique_id(), " - created ", name, " ready") 35 | 36 | global_position = Vector3(randi_range(0, 3), 2, randi_range(0, 3)) 37 | 38 | if str(name).is_valid_int(): 39 | inputs.set_multiplayer_authority(str(name).to_int()) 40 | focus_camera_on(self) 41 | 42 | await get_tree().process_frame 43 | rollback_synchronizer.process_settings() 44 | NetworkTime.on_tick.connect(on_tick) 45 | 46 | func focus_camera_on(cam_target): 47 | if inputs.is_multiplayer_authority(): 48 | var camera = get_tree().get_root().get_camera_3d() 49 | camera.target = cam_target 50 | 51 | func _process(_delta): 52 | $Label3D.text = str(name, "\nmovement: ", str(inputs.motion), "\ninput owner: ", str(inputs.get_multiplayer_authority())) 53 | 54 | func on_tick(delta, _tick): 55 | execute_movement(delta, _tick) 56 | 57 | func _rollback_tick(delta, _tick, _is_fresh): 58 | execute_movement(delta, _tick) 59 | 60 | func execute_movement(delta, _tick): 61 | #update the physics server on where we are this tick. 62 | velocity.y -= gravity * delta 63 | process_input() 64 | 65 | apply_friction(delta) 66 | calculate_steering(delta) 67 | 68 | velocity += acceleration * delta 69 | 70 | move_and_slide() 71 | 72 | func process_input(): 73 | var turn = inputs.motion.x 74 | steer_angle = turn * deg_to_rad(steering_limit) 75 | acceleration = Vector3.ZERO 76 | if inputs.motion.y > 0: 77 | acceleration = - transform.basis.z * engine_power 78 | if inputs.motion.y < 0: 79 | acceleration = - transform.basis.z * braking 80 | 81 | func apply_friction(delta): 82 | if velocity.length() < 0.2 and acceleration.length() == 0: 83 | velocity.x = 0 84 | velocity.z = 0 85 | var friction_force = velocity * friction * delta 86 | var drag_force = velocity * drag * delta 87 | acceleration += drag_force + friction_force 88 | 89 | 90 | func calculate_steering(delta): 91 | var rear_wheel = transform.origin + transform.basis.z * wheel_base / 2.0 92 | var front_wheel = transform.origin - transform.basis.z * wheel_base / 2.0 93 | rear_wheel += velocity * delta 94 | front_wheel += velocity.rotated(transform.basis.y, steer_angle) * delta 95 | var new_heading = rear_wheel.direction_to(front_wheel) 96 | 97 | #Drifting 98 | # traction 99 | if not drifting and velocity.length() > slip_speed: 100 | drifting = true 101 | if drifting and velocity.length() < slip_speed and steer_angle == 0: 102 | drifting = false 103 | var traction = traction_fast if drifting else traction_slow 104 | 105 | direction = new_heading.dot(velocity.normalized()) 106 | 107 | if direction > 0: # reverse 108 | velocity = lerp(velocity, new_heading * min(velocity.length(), max_speed_reverse), traction) 109 | if direction < 0: # forward 110 | velocity = lerp(velocity, -new_heading * min(velocity.length(), max_speed_forward), traction) 111 | 112 | look_at(transform.origin + new_heading, transform.basis.y) 113 | -------------------------------------------------------------------------------- /materials/wall.ptex: -------------------------------------------------------------------------------- 1 | { 2 | "connections": [ 3 | { 4 | "from": "beehive", 5 | "from_port": 0, 6 | "to": "colorize", 7 | "to_port": 0 8 | }, 9 | { 10 | "from": "colorize", 11 | "from_port": 0, 12 | "to": "Material", 13 | "to_port": 0 14 | }, 15 | { 16 | "from": "beehive", 17 | "from_port": 0, 18 | "to": "colorize_2", 19 | "to_port": 0 20 | }, 21 | { 22 | "from": "colorize_2", 23 | "from_port": 0, 24 | "to": "normal_map2", 25 | "to_port": 0 26 | }, 27 | { 28 | "from": "normal_map2", 29 | "from_port": 0, 30 | "to": "Material", 31 | "to_port": 4 32 | }, 33 | { 34 | "from": "colorize_2", 35 | "from_port": 0, 36 | "to": "colorize_3", 37 | "to_port": 0 38 | }, 39 | { 40 | "from": "colorize_3", 41 | "from_port": 0, 42 | "to": "Material", 43 | "to_port": 7 44 | } 45 | ], 46 | "label": "Graph", 47 | "longdesc": "", 48 | "name": "@@322", 49 | "node_position": { 50 | "x": 0, 51 | "y": 0 52 | }, 53 | "nodes": [ 54 | { 55 | "export_last_target": "Godot/Godot 4 Standard", 56 | "export_paths": { 57 | "Godot/Godot 4 Standard": "/home/al/Godot/projects/phys_test/materials/wall" 58 | }, 59 | "name": "Material", 60 | "node_position": { 61 | "x": 953, 62 | "y": 194 63 | }, 64 | "parameters": { 65 | "albedo_color": { 66 | "a": 0.258824, 67 | "b": 1, 68 | "g": 1, 69 | "r": 1, 70 | "type": "Color" 71 | }, 72 | "ao": 1, 73 | "depth_scale": 0.5, 74 | "emission_energy": 1, 75 | "flags_transparent": true, 76 | "metallic": 0, 77 | "normal": 1, 78 | "roughness": 1, 79 | "size": 11, 80 | "sss": 1 81 | }, 82 | "seed_int": 0, 83 | "type": "material" 84 | }, 85 | { 86 | "name": "beehive", 87 | "node_position": { 88 | "x": -316, 89 | "y": 152 90 | }, 91 | "parameters": { 92 | "sx": 8, 93 | "sy": 4 94 | }, 95 | "seed_int": 0, 96 | "type": "beehive2" 97 | }, 98 | { 99 | "name": "colorize", 100 | "node_position": { 101 | "x": -18, 102 | "y": 163 103 | }, 104 | "parameters": { 105 | "gradient": { 106 | "interpolation": 0, 107 | "points": [ 108 | { 109 | "a": 1, 110 | "b": 1, 111 | "g": 1, 112 | "pos": 0, 113 | "r": 1 114 | }, 115 | { 116 | "a": 0.196078, 117 | "b": 0.686275, 118 | "g": 0.262745, 119 | "pos": 0.154545, 120 | "r": 0.184314 121 | } 122 | ], 123 | "type": "Gradient" 124 | } 125 | }, 126 | "seed_int": 0, 127 | "type": "colorize" 128 | }, 129 | { 130 | "name": "colorize_2", 131 | "node_position": { 132 | "x": -1, 133 | "y": 282 134 | }, 135 | "parameters": { 136 | "gradient": { 137 | "interpolation": 1, 138 | "points": [ 139 | { 140 | "a": 1, 141 | "b": 1, 142 | "g": 1, 143 | "pos": 0, 144 | "r": 1 145 | }, 146 | { 147 | "a": 1, 148 | "b": 0, 149 | "g": 0, 150 | "pos": 0.154545, 151 | "r": 0 152 | } 153 | ], 154 | "type": "Gradient" 155 | } 156 | }, 157 | "seed_int": 0, 158 | "type": "colorize" 159 | }, 160 | { 161 | "name": "normal_map2", 162 | "node_position": { 163 | "x": 503.267944, 164 | "y": 462.442596 165 | }, 166 | "parameters": { 167 | "buffer": 1, 168 | "param2": 0, 169 | "size": 10, 170 | "strength": 1 171 | }, 172 | "seed_int": 0, 173 | "type": "normal_map2" 174 | }, 175 | { 176 | "name": "colorize_3", 177 | "node_position": { 178 | "x": 364.863525, 179 | "y": 259.922546 180 | }, 181 | "parameters": { 182 | "gradient": { 183 | "interpolation": 1, 184 | "points": [ 185 | { 186 | "a": 1, 187 | "b": 0, 188 | "g": 0, 189 | "pos": 0, 190 | "r": 0 191 | }, 192 | { 193 | "a": 1, 194 | "b": 0.269531, 195 | "g": 0.269531, 196 | "pos": 1, 197 | "r": 0.269531 198 | } 199 | ], 200 | "type": "Gradient" 201 | } 202 | }, 203 | "seed_int": 0, 204 | "type": "colorize" 205 | } 206 | ], 207 | "parameters": { 208 | 209 | }, 210 | "seed_int": 0, 211 | "shortdesc": "", 212 | "type": "graph" 213 | } -------------------------------------------------------------------------------- /scenes/player_rb.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=10 format=3 uid="uid://rbhb1te77y2d"] 2 | 3 | [ext_resource type="Script" uid="uid://lxgmbbyk3nvi" path="res://scripts/player_rb.gd" id="1_35slv"] 4 | [ext_resource type="Script" uid="uid://d350u8evihs1u" path="res://addons/netfox/rollback/rollback-synchronizer.gd" id="2_1k5lo"] 5 | [ext_resource type="PackedScene" uid="uid://dtoa53rtwixhg" path="res://models/car.glb" id="2_athq8"] 6 | [ext_resource type="Script" uid="uid://cbmc5r8tb2ub7" path="res://scripts/input.gd" id="3_3k3na"] 7 | [ext_resource type="PackedScene" uid="uid://d2sn826pqsfs5" path="res://scenes/wheel.tscn" id="4_1k5lo"] 8 | [ext_resource type="Script" uid="uid://dour8fehaaugp" path="res://addons/netfox/tick-interpolator.gd" id="5_1k5lo"] 9 | [ext_resource type="PackedScene" uid="uid://26d7swkp18du" path="res://scenes/car_ai.tscn" id="7_athq8"] 10 | 11 | [sub_resource type="PhysicsMaterial" id="PhysicsMaterial_athq8"] 12 | friction = 0.75 13 | bounce = 0.2 14 | 15 | [sub_resource type="BoxShape3D" id="BoxShape3D_dqkch"] 16 | size = Vector3(1.75, 1, 3) 17 | 18 | [node name="Player" type="RigidBody3D" groups=["players"]] 19 | collision_layer = 3 20 | collision_mask = 3 21 | mass = 20.0 22 | physics_material_override = SubResource("PhysicsMaterial_athq8") 23 | center_of_mass_mode = 1 24 | center_of_mass = Vector3(0, -0.5, 0) 25 | can_sleep = false 26 | continuous_cd = true 27 | linear_damp = 0.3 28 | angular_damp = 1.0 29 | script = ExtResource("1_35slv") 30 | spring_strength = 100.0 31 | engine_power = 200.0 32 | steering_angle = 25.0 33 | front_tire_grip = 1.5 34 | metadata/_custom_type_script = "uid://lxgmbbyk3nvi" 35 | 36 | [node name="Car_model" parent="." instance=ExtResource("2_athq8")] 37 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.0643204, -0.231174) 38 | 39 | [node name="CollisionShape3D" type="CollisionShape3D" parent="."] 40 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0) 41 | shape = SubResource("BoxShape3D_dqkch") 42 | 43 | [node name="RollbackSynchronizer" type="Node" parent="." node_paths=PackedStringArray("root")] 44 | script = ExtResource("2_1k5lo") 45 | root = NodePath("..") 46 | state_properties = Array[String]([":physics_state", ":previous_spring_lengths", ":team"]) 47 | input_properties = Array[String](["Input:motion", "Input:jumping"]) 48 | 49 | [node name="Input" type="Node" parent="."] 50 | script = ExtResource("3_3k3na") 51 | 52 | [node name="Label3D" type="Label3D" parent="."] 53 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.8478, 0) 54 | visible = false 55 | billboard = 1 56 | shaded = true 57 | no_depth_test = true 58 | text = "Test 59 | " 60 | font_size = 60 61 | 62 | [node name="Wheels" type="Node3D" parent="."] 63 | 64 | [node name="FL_Wheel" parent="Wheels" instance=ExtResource("4_1k5lo")] 65 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.75, 0.291376, 1) 66 | enabled = false 67 | hit_from_inside = true 68 | metadata/_custom_type_script = "uid://c8tk028y8ex3g" 69 | 70 | [node name="FR_Wheel" parent="Wheels" instance=ExtResource("4_1k5lo")] 71 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.75, 0.291376, 1) 72 | enabled = false 73 | hit_from_inside = true 74 | metadata/_custom_type_script = "uid://c8tk028y8ex3g" 75 | 76 | [node name="RL_Wheel" parent="Wheels" instance=ExtResource("4_1k5lo")] 77 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.75, 0.291376, -1) 78 | enabled = false 79 | hit_from_inside = true 80 | is_front_wheel = false 81 | metadata/_custom_type_script = "uid://c8tk028y8ex3g" 82 | 83 | [node name="RR_Wheel" parent="Wheels" instance=ExtResource("4_1k5lo")] 84 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.75, 0.291376, -1) 85 | enabled = false 86 | hit_from_inside = true 87 | is_front_wheel = false 88 | metadata/_custom_type_script = "uid://c8tk028y8ex3g" 89 | 90 | [node name="TickInterpolator" type="Node" parent="." node_paths=PackedStringArray("root")] 91 | script = ExtResource("5_1k5lo") 92 | root = NodePath("..") 93 | properties = Array[String]([":global_transform"]) 94 | metadata/_custom_type_script = "uid://dour8fehaaugp" 95 | 96 | [node name="Car_AI" parent="." instance=ExtResource("7_athq8")] 97 | 98 | [node name="RoofBounce" type="RayCast3D" parent="."] 99 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.9680519, 0) 100 | enabled = false 101 | target_position = Vector3(0, 0.5, 0) 102 | -------------------------------------------------------------------------------- /scripts/wheel.gd: -------------------------------------------------------------------------------- 1 | extends RayCast3D 2 | 3 | class_name RayWheel 4 | 5 | @onready var car: RigidBody3D = get_parent().get_parent() 6 | @onready var wheel = $Wheel 7 | 8 | var previous_spring_length: float = 0.0 9 | 10 | @export var is_front_wheel: bool 11 | 12 | func _ready(): 13 | add_exception(car) 14 | 15 | func wheel_tick(delta, _tick): 16 | force_raycast_update() 17 | 18 | if is_colliding(): 19 | var collision_point = get_collision_point() 20 | 21 | suspension(delta, collision_point) 22 | acceleration(collision_point) 23 | 24 | apply_z_force(collision_point) 25 | apply_x_force(delta, collision_point) 26 | 27 | set_wheel_position(to_local(get_collision_point()).y + car.wheel_radius) 28 | rotate_wheel(delta) 29 | else: 30 | set_wheel_position(-car.suspension_rest_dist) 31 | 32 | 33 | func apply_x_force(delta, collision_point): 34 | var dir: Vector3 = global_basis.x 35 | var state := PhysicsServer3D.body_get_direct_state(car.get_rid()) 36 | var tire_world_vel := state.get_velocity_at_local_position(global_position - car.global_position) 37 | var lateral_vel: float = dir.dot(tire_world_vel) 38 | 39 | var grip = car.rear_tire_grip 40 | 41 | if is_front_wheel: 42 | grip = car.front_tire_grip 43 | 44 | var desired_vel_change: float = - lateral_vel * grip 45 | var x_force = desired_vel_change / delta 46 | 47 | car.apply_force(dir * x_force, collision_point - car.global_position) 48 | 49 | if car.debug: 50 | DebugDraw3D.draw_arrow(global_position, global_position + (dir * x_force / 20), Color.RED, 0.1, true) 51 | 52 | 53 | func apply_z_force(collision_point): 54 | var dir: Vector3 = global_basis.z 55 | var state := PhysicsServer3D.body_get_direct_state(car.get_rid()) 56 | var tire_world_vel := state.get_velocity_at_local_position(global_position - car.global_position) 57 | var z_force = dir.dot(tire_world_vel) * car.mass / 10 58 | 59 | car.apply_force(-dir * z_force, collision_point - car.global_position) 60 | 61 | var point = Vector3(collision_point.x, collision_point.y + car.wheel_radius, collision_point.z) 62 | 63 | if car.debug: 64 | DebugDraw3D.draw_arrow(point, point + (-dir * z_force / 2), Color.BLUE_VIOLET, 0.1, true) 65 | 66 | 67 | func set_wheel_position(new_y_position: float): 68 | wheel.position.y = lerp(wheel.position.y, new_y_position, 0.6) 69 | 70 | 71 | func rotate_wheel(delta: float): 72 | var dir = car.basis.z 73 | var rotation_direction = 1 if car.linear_velocity.dot(dir) > 0 else -1 74 | 75 | wheel.rotate_x(rotation_direction * car.linear_velocity.length() * delta) 76 | 77 | 78 | func acceleration(collision_point): 79 | if is_front_wheel: 80 | return 81 | 82 | var accel_dir = - global_basis.z 83 | 84 | var torque = car.accel_input * car.engine_power 85 | 86 | var point = Vector3(collision_point.x, collision_point.y, collision_point.z) 87 | 88 | car.apply_force(accel_dir * torque, point - car.global_position) 89 | 90 | if car.debug: 91 | DebugDraw3D.draw_arrow(point, point + (accel_dir * torque / 20), Color.BLUE, 0.1, true) 92 | 93 | 94 | func suspension(delta, collision_point): 95 | # the direction the force will be applied 96 | var susp_dir = global_basis.y 97 | 98 | var raycast_origin = global_position 99 | var raycast_dest = collision_point 100 | var distance = raycast_dest.distance_to(raycast_origin) 101 | 102 | var spring_length = clamp(distance - car.wheel_radius, 0, car.suspension_rest_dist) 103 | 104 | var spring_force = car.spring_strength * (car.suspension_rest_dist - spring_length) 105 | 106 | var spring_velocity = (previous_spring_length - spring_length) / delta 107 | 108 | var damper_force = car.spring_damper * spring_velocity 109 | 110 | var suspension_force = basis.y * (spring_force + damper_force) 111 | 112 | previous_spring_length = spring_length 113 | 114 | var point = Vector3(collision_point.x, collision_point.y + car.wheel_radius, collision_point.z) 115 | 116 | car.apply_force(susp_dir * suspension_force, point - car.global_position) 117 | 118 | if car.debug: 119 | DebugDraw3D.draw_arrow(global_position, to_global(position + Vector3(-position.x, (suspension_force.y / 20), -position.z)), Color.GREEN, 0.1, true) 120 | DebugDraw3D.draw_line_hit_offset(global_position, to_global(position + Vector3(-position.x, -1, -position.z)), true, distance, 0.2, Color.RED, Color.RED) 121 | -------------------------------------------------------------------------------- /addons/netfox.internals/logger.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name _NetfoxLogger 3 | 4 | enum { 5 | LOG_MIN, 6 | LOG_TRACE, 7 | LOG_DEBUG, 8 | LOG_INFO, 9 | LOG_WARN, 10 | LOG_ERROR, 11 | LOG_MAX 12 | } 13 | 14 | const DEFAULT_LOG_LEVEL := LOG_DEBUG 15 | 16 | static var log_level: int 17 | static var module_log_level: Dictionary 18 | 19 | static var _tags: Dictionary = {} 20 | static var _ordered_tags: Array[Callable] = [] 21 | 22 | var module: String 23 | var name: String 24 | 25 | const level_prefixes: Array[String] = [ 26 | "", 27 | "TRC", 28 | "DBG", 29 | "INF", 30 | "WRN", 31 | "ERR", 32 | "" 33 | ] 34 | 35 | static func for_netfox(p_name: String) -> _NetfoxLogger: 36 | return _NetfoxLogger.new("netfox", p_name) 37 | 38 | static func for_noray(p_name: String) -> _NetfoxLogger: 39 | return _NetfoxLogger.new("netfox.noray", p_name) 40 | 41 | static func for_extras(p_name: String) -> _NetfoxLogger: 42 | return _NetfoxLogger.new("netfox.extras", p_name) 43 | 44 | static func make_setting(name: String) -> Dictionary: 45 | return { 46 | "name": name, 47 | "value": DEFAULT_LOG_LEVEL, 48 | "type": TYPE_INT, 49 | "hint": PROPERTY_HINT_ENUM, 50 | "hint_string": "All,Trace,Debug,Info,Warning,Error,None" 51 | } 52 | 53 | static func register_tag(tag: Callable, priority: int = 0) -> void: 54 | # Save tag 55 | if not _tags.has(priority): 56 | _tags[priority] = [tag] 57 | else: 58 | _tags[priority].push_back(tag) 59 | 60 | # Recalculate tag order 61 | _ordered_tags.clear() 62 | 63 | var prio_groups = _tags.keys() 64 | prio_groups.sort() 65 | 66 | for prio_group in prio_groups: 67 | var tag_group = _tags[prio_group] 68 | _ordered_tags.append_array(tag_group) 69 | 70 | static func free_tag(tag: Callable) -> void: 71 | for priority in _tags.keys(): 72 | var priority_group := _tags[priority] as Array 73 | priority_group.erase(tag) 74 | 75 | # NOTE: Arrays are passed as reference, no need to re-assign after modifying 76 | if priority_group.is_empty(): 77 | _tags.erase(priority) 78 | 79 | _ordered_tags.erase(tag) 80 | 81 | static func _static_init(): 82 | log_level = ProjectSettings.get_setting(&"netfox/logging/log_level", DEFAULT_LOG_LEVEL) 83 | module_log_level = { 84 | "netfox": ProjectSettings.get_setting(&"netfox/logging/netfox_log_level", DEFAULT_LOG_LEVEL), 85 | "netfox.noray": ProjectSettings.get_setting(&"netfox/logging/netfox_noray_log_level", DEFAULT_LOG_LEVEL), 86 | "netfox.extras": ProjectSettings.get_setting(&"netfox/logging/netfox_extras_log_level", DEFAULT_LOG_LEVEL) 87 | } 88 | 89 | func _init(p_module: String, p_name: String): 90 | module = p_module 91 | name = p_name 92 | 93 | func _check_log_level(level: int) -> bool: 94 | var cmp_level = log_level 95 | if level < cmp_level: 96 | return false 97 | 98 | if module_log_level.has(module): 99 | var module_level = module_log_level.get(module) 100 | return level >= module_level 101 | 102 | return true 103 | 104 | func _format_text(text: String, values: Array, level: int) -> String: 105 | level = clampi(level, LOG_MIN, LOG_MAX) 106 | 107 | var result := PackedStringArray() 108 | 109 | result.append("[%s]" % [level_prefixes[level]]) 110 | for tag in _ordered_tags: 111 | result.append("[%s]" % [tag.call()]) 112 | result.append("[%s::%s] " % [module, name]) 113 | 114 | if values.is_empty(): 115 | result.append(text) 116 | else: 117 | result.append(text % values) 118 | 119 | return "".join(result) 120 | 121 | func _log_text(text: String, values: Array, level: int): 122 | if _check_log_level(level): 123 | print(_format_text(text, values, level)) 124 | 125 | func trace(text: String, values: Array = []): 126 | _log_text(text, values, LOG_TRACE) 127 | 128 | func debug(text: String, values: Array = []): 129 | _log_text(text, values, LOG_DEBUG) 130 | 131 | func info(text: String, values: Array = []): 132 | _log_text(text, values, LOG_INFO) 133 | 134 | func warning(text: String, values: Array = []): 135 | if _check_log_level(LOG_WARN): 136 | var formatted_text = _format_text(text, values, LOG_WARN) 137 | push_warning(formatted_text) 138 | # Print so it shows up in the Output panel too 139 | print(formatted_text) 140 | 141 | func error(text: String, values: Array = []): 142 | if _check_log_level(LOG_ERROR): 143 | var formatted_text = _format_text(text, values, LOG_ERROR) 144 | push_error(formatted_text) 145 | # Print so it shows up in the Output panel too 146 | print(formatted_text) 147 | --------------------------------------------------------------------------------