From 3012b127124158b56dfbc01ecd875226b4636278 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20G=C3=A1lffy?= Date: Tue, 28 Apr 2026 23:49:47 +0200 Subject: [PATCH 1/5] i guess i did it? --- addons/netfox/rollback/network-rollback.gd | 1 + addons/netfox/rollback/rollback-synchronizer.gd | 16 ++++++++++++++-- addons/netfox/servers/network-history-server.gd | 11 +++++++++++ .../servers/rollback-simulation-server.gd | 17 +++++++++++++++-- examples/rollback-fps/characters/player.tscn | 1 + examples/rollback-fps/scripts/player.gd | 5 +++++ project.godot | 2 ++ 7 files changed, 49 insertions(+), 4 deletions(-) diff --git a/addons/netfox/rollback/network-rollback.gd b/addons/netfox/rollback/network-rollback.gd index f48c6e11..376e4f83 100644 --- a/addons/netfox/rollback/network-rollback.gd +++ b/addons/netfox/rollback/network-rollback.gd @@ -432,6 +432,7 @@ func _rollback() -> void: on_record_tick.emit(tick + 1) NetworkHistoryServer._record_rollback_state(tick + 1) NetworkSynchronizationServer._synchronize_state(tick + 1) + NetworkHistoryServer.flush_ignores() # Restore display state _rollback_stage = _STAGE_AFTER diff --git a/addons/netfox/rollback/rollback-synchronizer.gd b/addons/netfox/rollback/rollback-synchronizer.gd index 7e843b02..784a5e5c 100644 --- a/addons/netfox/rollback/rollback-synchronizer.gd +++ b/addons/netfox/rollback/rollback-synchronizer.gd @@ -15,7 +15,11 @@ class_name RollbackSynchronizer ## [br][br] ## Enabling this will run [code]_rollback_tick[/code] on nodes under ## [member root] even if there's no current input available for the tick. -@export var enable_prediction: bool = false +@export var enable_prediction: bool = false: + set(v): + if v != enable_prediction: + _set_prediction_enabled(v) + enable_prediction = v @export_group("State") ## Properties that define the game state. @@ -87,6 +91,7 @@ func process_settings() -> void: for node in managed_nodes: if NetworkRollback.is_rollback_aware(node): RollbackSimulationServer.register(NetworkRollback._get_rollback_method(node)) + RollbackSimulationServer.set_prediction_enabled_for(node, enable_prediction) _sim_nodes.append(node) if NetworkRollback.is_rollback_liveness_aware(node) and not RollbackLivenessServer.is_registered(node): @@ -255,7 +260,10 @@ func ignore_prediction(node: Node) -> void: # predictions. # # This method may see some use again, otherwise it will be deprecated. - pass + + # NOTE: Turns out this is useful - even if mispredictions are not recorded, + # we might not want to display them + NetworkHistoryServer.ignore(node) ## Get the tick of the last known input. ## [br][br] @@ -380,6 +388,10 @@ func _reprocess_settings() -> void: _properties_dirty = false process_settings() +func _set_prediction_enabled(enabled: bool) -> void: + for node in _sim_nodes: + RollbackSimulationServer.set_prediction_enabled_for(node, enabled) + # Find managed nodes recursively from given root, ignoring branches managed by # a different RollbackSynchronizer func _collect_managed_nodes(root: Node) -> Array[Node]: diff --git a/addons/netfox/servers/network-history-server.gd b/addons/netfox/servers/network-history-server.gd index 092ed78d..72b9b6af 100644 --- a/addons/netfox/servers/network-history-server.gd +++ b/addons/netfox/servers/network-history-server.gd @@ -19,6 +19,8 @@ var _sync_state_properties := _PropertyPool.new() var _rb_history_size := NetworkRollback.history_limit var _sync_history_size := ProjectSettings.get_setting("netfox/state_synchronizer/history_limit", 64) as int +var _ignored_subjects := _Set.new() + # Source of truth for history var _rb_input_history := _PerObjectHistory.new(_rb_history_size) var _rb_state_history := _PerObjectHistory.new(_rb_history_size) @@ -74,6 +76,12 @@ func deregister(node: Node) -> void: var snapshot := value as _Snapshot snapshot.erase_subject(node) +func ignore(subject: Node) -> void: + _ignored_subjects.add(subject) + +func flush_ignores() -> void: + _ignored_subjects.clear() + ## Return the latest tick where any of the [param subjects] had rollback ## state data available func get_latest_state_tick_for(subjects: Array, tick: int) -> int: @@ -160,6 +168,9 @@ func _record(tick: int, history: _PerObjectHistory, snapshots: _HistoryBuffer, p for subject in property_pool.get_subjects(): assert(subject is Node, "Only nodes supported for now!") + + if _ignored_subjects.has(subject): + continue var is_auth := auth_filter.call(subject) diff --git a/addons/netfox/servers/rollback-simulation-server.gd b/addons/netfox/servers/rollback-simulation-server.gd index fd34d6dd..789d0488 100644 --- a/addons/netfox/servers/rollback-simulation-server.gd +++ b/addons/netfox/servers/rollback-simulation-server.gd @@ -22,6 +22,7 @@ var _liveness_server: _RollbackLivenessServer var _callbacks := {} # node to callback var _simulated_ticks := {} # node to array of ticks +var _prediction_enabled_nodes := _Set.new() var _input_graph := _Graph.new() # Links inputs to objects controlled by them @@ -63,6 +64,7 @@ func deregister_node(node: Node) -> void: if _callbacks.has(node): deregister(_callbacks[node]) _input_graph.erase(node) + _prediction_enabled_nodes.erase(node) ## Register [param input] as providing input for [param node] func register_rollback_input_for(node: Node, input: Node) -> void: @@ -72,6 +74,15 @@ func register_rollback_input_for(node: Node, input: Node) -> void: func deregister_rollback_input(node: Node, input: Node) -> void: _input_graph.unlink(input, node) +func set_prediction_enabled_for(node: Node, enabled: bool) -> void: + if enabled: + _prediction_enabled_nodes.add(node) + else: + _prediction_enabled_nodes.erase(node) + +func is_prediction_enabled_for(node: Node) -> bool: + return _prediction_enabled_nodes.has(node) + ## Return true if the currently simulated node is being predicted func is_predicting_current() -> bool: if not _current_object or not is_instance_valid(_current_object): @@ -140,8 +151,10 @@ func _get_nodes_to_simulate(input_snapshot: _Snapshot) -> Array[Node]: result.append(node) continue - if not input_snapshot.has_subjects(inputs, true): - # We don't have input for node, don't simulate + if not input_snapshot.has_subjects(inputs, true) and \ + not is_prediction_enabled_for(node): + # We don't have input for node, and input prediction is disabled + # Don't simulate continue result.append(node) diff --git a/examples/rollback-fps/characters/player.tscn b/examples/rollback-fps/characters/player.tscn index b591a367..ec9b2523 100644 --- a/examples/rollback-fps/characters/player.tscn +++ b/examples/rollback-fps/characters/player.tscn @@ -81,6 +81,7 @@ font_size = 16 unique_name_in_owner = true script = ExtResource("9_vik3r") root = NodePath("..") +enable_prediction = true state_properties = Array[String]([":transform", ":velocity", ":health", ":deaths", "Head:transform", "Head/PlayerFPSWeapon:last_fire"]) input_properties = Array[String](["Input:movement", "Input:jump", "Input:fire", "Input:look_angle"]) diff --git a/examples/rollback-fps/scripts/player.gd b/examples/rollback-fps/scripts/player.gd index 422d1e17..b55e2f0c 100644 --- a/examples/rollback-fps/scripts/player.gd +++ b/examples/rollback-fps/scripts/player.gd @@ -9,6 +9,7 @@ extends CharacterBody3D @onready var head := $Head as Node3D @onready var camera := $Head/Camera3D as Camera3D @onready var hit_sfx := $"Hit SFX" as AudioStreamPlayer3D +@onready var rollback_synchronizer := %RollbackSynchronizer as RollbackSynchronizer #+# static var _logger := NetfoxLogger.new("game", "Player") @@ -52,6 +53,10 @@ func _after_tick_loop(): _was_hit = false func _rollback_tick(delta: float, tick: int, is_fresh: bool) -> void: + if rollback_synchronizer.is_predicting(): + rollback_synchronizer.ignore_prediction(self) + return + # Handle respawn if tick == death_tick: global_position = respawn_position diff --git a/project.godot b/project.godot index c04de8ae..e8cd33dd 100644 --- a/project.godot +++ b/project.godot @@ -141,6 +141,8 @@ escape={ general/clear_settings=false time/tickrate=24 extras/auto_tile_windows=true +autoconnect/enabled=true +autoconnect/simulated_latency_ms=250 [rendering] From 0ca80ae5ec055a172269f2c39530a7d450c3e49e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20G=C3=A1lffy?= Date: Wed, 29 Apr 2026 20:22:17 +0200 Subject: [PATCH 2/5] doc comments --- addons/netfox/servers/network-history-server.gd | 10 ++++++++++ addons/netfox/servers/rollback-simulation-server.gd | 8 ++++++++ 2 files changed, 18 insertions(+) diff --git a/addons/netfox/servers/network-history-server.gd b/addons/netfox/servers/network-history-server.gd index 72b9b6af..236114a3 100644 --- a/addons/netfox/servers/network-history-server.gd +++ b/addons/netfox/servers/network-history-server.gd @@ -76,9 +76,19 @@ func deregister(node: Node) -> void: var snapshot := value as _Snapshot snapshot.erase_subject(node) +## Do not record [param subject] +## [br][br] +## Can be used in [code]_rollback_tick()[/code] in case node prediction is +## enabled, but state can't be reasonably predicted. +## [br][br] +## Subjects stay ignored until [method flush_ignores] is called. This is done +## by default after every rollback tick. func ignore(subject: Node) -> void: _ignored_subjects.add(subject) +## Clear the list of ignored subjects +## [br][br] +## Calling this method undoes all previous [method ignore] calls. func flush_ignores() -> void: _ignored_subjects.clear() diff --git a/addons/netfox/servers/rollback-simulation-server.gd b/addons/netfox/servers/rollback-simulation-server.gd index 789d0488..1f54b32a 100644 --- a/addons/netfox/servers/rollback-simulation-server.gd +++ b/addons/netfox/servers/rollback-simulation-server.gd @@ -74,12 +74,20 @@ func register_rollback_input_for(node: Node, input: Node) -> void: func deregister_rollback_input(node: Node, input: Node) -> void: _input_graph.unlink(input, node) +## Toggle the prediction enabled flag for [param node] +## [br][br] +## Prediction in this sense means that the node will be simulated even if there +## is no up to date input available. func set_prediction_enabled_for(node: Node, enabled: bool) -> void: if enabled: _prediction_enabled_nodes.add(node) else: _prediction_enabled_nodes.erase(node) +## Return true if prediction is enabled for [param node] +## [br][br] +## Prediction in this sense means that the node will be simulated even if there +## is no up to date input available. func is_prediction_enabled_for(node: Node) -> bool: return _prediction_enabled_nodes.has(node) From 79ae0d0fc44ca5edea8b397b290c8a40bb098472 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20G=C3=A1lffy?= Date: Wed, 29 Apr 2026 20:22:31 +0200 Subject: [PATCH 3/5] lint --- addons/netfox/servers/network-history-server.gd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/netfox/servers/network-history-server.gd b/addons/netfox/servers/network-history-server.gd index 236114a3..297510e4 100644 --- a/addons/netfox/servers/network-history-server.gd +++ b/addons/netfox/servers/network-history-server.gd @@ -178,7 +178,7 @@ func _record(tick: int, history: _PerObjectHistory, snapshots: _HistoryBuffer, p for subject in property_pool.get_subjects(): assert(subject is Node, "Only nodes supported for now!") - + if _ignored_subjects.has(subject): continue From 6c6bffcda4a724357347d09375231e03cdee5342 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20G=C3=A1lffy?= Date: Wed, 29 Apr 2026 20:22:36 +0200 Subject: [PATCH 4/5] bv --- addons/netfox.extras/plugin.cfg | 2 +- addons/netfox.internals/plugin.cfg | 2 +- addons/netfox.noray/plugin.cfg | 2 +- addons/netfox/plugin.cfg | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/addons/netfox.extras/plugin.cfg b/addons/netfox.extras/plugin.cfg index a1a852ed..29d418db 100644 --- a/addons/netfox.extras/plugin.cfg +++ b/addons/netfox.extras/plugin.cfg @@ -3,5 +3,5 @@ name="netfox.extras" description="Game-specific utilities for Netfox" author="Tamas Galffy and contributors" -version="1.43.0" +version="1.43.1" script="netfox-extras.gd" diff --git a/addons/netfox.internals/plugin.cfg b/addons/netfox.internals/plugin.cfg index 6b5a7e5f..1e23b51f 100644 --- a/addons/netfox.internals/plugin.cfg +++ b/addons/netfox.internals/plugin.cfg @@ -3,5 +3,5 @@ name="netfox.internals" description="Shared internals for netfox addons" author="Tamas Galffy and contributors" -version="1.43.0" +version="1.43.1" script="plugin.gd" diff --git a/addons/netfox.noray/plugin.cfg b/addons/netfox.noray/plugin.cfg index 6f4e0817..62cee560 100644 --- a/addons/netfox.noray/plugin.cfg +++ b/addons/netfox.noray/plugin.cfg @@ -3,5 +3,5 @@ name="netfox.noray" description="Bulletproof your connectivity with noray integration for netfox" author="Tamas Galffy and contributors" -version="1.43.0" +version="1.43.1" script="netfox-noray.gd" diff --git a/addons/netfox/plugin.cfg b/addons/netfox/plugin.cfg index a74f0659..0aad3ce5 100644 --- a/addons/netfox/plugin.cfg +++ b/addons/netfox/plugin.cfg @@ -3,5 +3,5 @@ name="netfox" description="Shared internals for netfox addons" author="Tamas Galffy and contributors" -version="1.43.0" +version="1.43.1" script="netfox.gd" From d9381108694f23581ba4e4b910d4975d58fb4821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20G=C3=A1lffy?= Date: Wed, 29 Apr 2026 20:24:21 +0200 Subject: [PATCH 5/5] cleanup --- examples/rollback-fps/characters/player.tscn | 1 - examples/rollback-fps/scripts/player.gd | 5 ----- project.godot | 2 -- 3 files changed, 8 deletions(-) diff --git a/examples/rollback-fps/characters/player.tscn b/examples/rollback-fps/characters/player.tscn index ec9b2523..b591a367 100644 --- a/examples/rollback-fps/characters/player.tscn +++ b/examples/rollback-fps/characters/player.tscn @@ -81,7 +81,6 @@ font_size = 16 unique_name_in_owner = true script = ExtResource("9_vik3r") root = NodePath("..") -enable_prediction = true state_properties = Array[String]([":transform", ":velocity", ":health", ":deaths", "Head:transform", "Head/PlayerFPSWeapon:last_fire"]) input_properties = Array[String](["Input:movement", "Input:jump", "Input:fire", "Input:look_angle"]) diff --git a/examples/rollback-fps/scripts/player.gd b/examples/rollback-fps/scripts/player.gd index b55e2f0c..422d1e17 100644 --- a/examples/rollback-fps/scripts/player.gd +++ b/examples/rollback-fps/scripts/player.gd @@ -9,7 +9,6 @@ extends CharacterBody3D @onready var head := $Head as Node3D @onready var camera := $Head/Camera3D as Camera3D @onready var hit_sfx := $"Hit SFX" as AudioStreamPlayer3D -@onready var rollback_synchronizer := %RollbackSynchronizer as RollbackSynchronizer #+# static var _logger := NetfoxLogger.new("game", "Player") @@ -53,10 +52,6 @@ func _after_tick_loop(): _was_hit = false func _rollback_tick(delta: float, tick: int, is_fresh: bool) -> void: - if rollback_synchronizer.is_predicting(): - rollback_synchronizer.ignore_prediction(self) - return - # Handle respawn if tick == death_tick: global_position = respawn_position diff --git a/project.godot b/project.godot index e8cd33dd..c04de8ae 100644 --- a/project.godot +++ b/project.godot @@ -141,8 +141,6 @@ escape={ general/clear_settings=false time/tickrate=24 extras/auto_tile_windows=true -autoconnect/enabled=true -autoconnect/simulated_latency_ms=250 [rendering]