Skip to content

Commit d099869

Browse files
feat: PredictiveSynchronizer (#533)
Closes #374 --------- Co-authored-by: Tamás Gálffy <ezittgtx@gmail.com>
1 parent 67d2eb2 commit d099869

File tree

12 files changed

+405
-4
lines changed

12 files changed

+405
-4
lines changed

addons/netfox.extras/plugin.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
name="netfox.extras"
44
description="Game-specific utilities for Netfox"
55
author="Tamas Galffy and contributors"
6-
version="1.36.3"
6+
version="1.37.0"
77
script="netfox-extras.gd"

addons/netfox.internals/plugin.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
name="netfox.internals"
44
description="Shared internals for netfox addons"
55
author="Tamas Galffy and contributors"
6-
version="1.36.3"
6+
version="1.37.0"
77
script="plugin.gd"

addons/netfox.noray/plugin.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
name="netfox.noray"
44
description="Bulletproof your connectivity with noray integration for netfox"
55
author="Tamas Galffy and contributors"
6-
version="1.36.3"
6+
version="1.37.0"
77
script="netfox-noray.gd"
Lines changed: 124 additions & 0 deletions
Loading
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
[remap]
2+
3+
importer="texture"
4+
type="CompressedTexture2D"
5+
uid="uid://dsr2mjcr1lh6v"
6+
path="res://.godot/imported/predictive-synchronizer.svg-c1a256fe714777cb768d64b354a43e2b.ctex"
7+
metadata={
8+
"has_editor_variant": true,
9+
"vram_texture": false
10+
}
11+
12+
[deps]
13+
14+
source_file="res://addons/netfox/icons/predictive-synchronizer.svg"
15+
dest_files=["res://.godot/imported/predictive-synchronizer.svg-c1a256fe714777cb768d64b354a43e2b.ctex"]
16+
17+
[params]
18+
19+
compress/mode=0
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=false
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=1
36+
svg/scale=1.0
37+
editor/scale_with_editor_scale=true
38+
editor/convert_colors_with_editor_theme=true

addons/netfox/netfox.gd

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,12 @@ const TYPES: Array[Dictionary] = [
173173
"script": ROOT + "/rewindable-action.gd",
174174
"icon": ROOT + "/icons/rewindable-action.svg"
175175
},
176+
{
177+
"name": "PredictiveSynchronizer",
178+
"base": "Node",
179+
"script": ROOT + "/rollback/predictive-synchronizer.gd",
180+
"icon": ROOT + "/icons/predictive-synchronizer.svg"
181+
},
176182
]
177183

178184
func _enter_tree():

addons/netfox/plugin.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
name="netfox"
44
description="Shared internals for netfox addons"
55
author="Tamas Galffy and contributors"
6-
version="1.36.3"
6+
version="1.37.0"
77
script="netfox.gd"
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
@tool
2+
extends Node
3+
class_name PredictiveSynchronizer
4+
5+
## Similar to [RollbackSynchronizer], this class manages local variables in a
6+
## rollback context for predictive simulation without networking.
7+
##
8+
## This is a simplified version that focuses on local state management.
9+
## [br][br]
10+
## Like [RollbackSynchronizer], it automatically discovers nodes
11+
## with a [code]_rollback_tick(delta: float, tick: int)[/code]
12+
## method and calls them during the prediction phase.
13+
14+
## The root node for resolving node paths in properties. Defaults to the parent
15+
## node.
16+
@export var root: Node = get_parent()
17+
18+
@export_group("State")
19+
## Properties that define the game state.
20+
## [br][br]
21+
## State properties are recorded for each tick and restored during rollback.
22+
## State is restored before every rollback tick, and recorded after simulating
23+
## the tick.
24+
@export var state_properties: Array[String]
25+
26+
var _state_property_config: _PropertyConfig = _PropertyConfig.new()
27+
var _property_cache := PropertyCache.new(root)
28+
var _freshness_store := RollbackFreshnessStore.new()
29+
30+
var _states := _PropertyHistoryBuffer.new()
31+
var _nodes: Array[Node] = []
32+
var _skipset: _Set = _Set.new()
33+
34+
var _properties_dirty: bool = false
35+
36+
# Composition
37+
var _history_recorder: _RollbackHistoryRecorder
38+
39+
## Process settings.
40+
##
41+
## Call this after any change to configuration.
42+
func process_settings() -> void:
43+
_property_cache.root = root
44+
_property_cache.clear()
45+
_freshness_store.clear()
46+
47+
_nodes.clear()
48+
_states.clear()
49+
50+
# Gather all prediction-aware nodes to call during prediction ticks
51+
_nodes = root.find_children("*")
52+
_nodes.push_front(root)
53+
_nodes = _nodes.filter(func(n): return NetworkRollback.is_rollback_aware(n))
54+
_nodes.erase(self)
55+
56+
_state_property_config.set_properties_from_paths(state_properties, _property_cache)
57+
58+
if _history_recorder == null:
59+
_history_recorder = _RollbackHistoryRecorder.new()
60+
61+
var _inputs := _PropertyHistoryBuffer.new()
62+
var _input_property_config := _PropertyConfig.new()
63+
_history_recorder.configure(_states, _inputs, _state_property_config, _input_property_config, _property_cache, _skipset)
64+
65+
func _ready() -> void:
66+
if Engine.is_editor_hint():
67+
return
68+
69+
if not NetworkTime.is_initial_sync_done():
70+
# Wait for time sync to complete
71+
await NetworkTime.after_sync
72+
73+
process_settings.call_deferred()
74+
75+
func _connect_signals() -> void:
76+
NetworkTime.before_tick.connect(_before_tick)
77+
NetworkTime.after_tick.connect(_after_tick)
78+
79+
NetworkRollback.on_prepare_tick.connect(_on_prepare_tick)
80+
NetworkRollback.on_process_tick.connect(_run_prediction_tick)
81+
NetworkRollback.on_record_tick.connect(_on_record_tick)
82+
83+
func _disconnect_signals() -> void:
84+
NetworkTime.before_tick.disconnect(_before_tick)
85+
NetworkTime.after_tick.disconnect(_after_tick)
86+
87+
NetworkRollback.on_prepare_tick.disconnect(_on_prepare_tick)
88+
NetworkRollback.on_process_tick.disconnect(_run_prediction_tick)
89+
NetworkRollback.on_record_tick.disconnect(_on_record_tick)
90+
91+
func _before_tick(_dt: float, tick: int) -> void:
92+
_history_recorder.apply_state(tick)
93+
94+
func _after_tick(_dt: float, tick: int) -> void:
95+
_history_recorder.trim_history()
96+
_freshness_store.trim()
97+
98+
func _on_prepare_tick(tick: int) -> void:
99+
_history_recorder.apply_tick(tick)
100+
101+
func _on_record_tick(tick: int) -> void:
102+
_history_recorder.record_state(tick)
103+
104+
func _run_prediction_tick(tick: int) -> void:
105+
for node in _nodes:
106+
var is_fresh := _freshness_store.is_fresh(node, tick)
107+
NetworkRollback.process_rollback(node, NetworkTime.ticktime, tick, is_fresh)
108+
_freshness_store.notify_processed(node, tick)
109+
110+
func _enter_tree() -> void:
111+
if Engine.is_editor_hint():
112+
return
113+
114+
if not NetworkTime.is_initial_sync_done():
115+
# Wait for time sync to complete
116+
await NetworkTime.after_sync
117+
_connect_signals.call_deferred()
118+
process_settings.call_deferred()
119+
120+
func _exit_tree() -> void:
121+
if Engine.is_editor_hint():
122+
return
123+
124+
_disconnect_signals()
125+
126+
func _reprocess_settings() -> void:
127+
if not _properties_dirty or Engine.is_editor_hint():
128+
return
129+
130+
_properties_dirty = false
131+
process_settings()
132+
133+
## Add a state property.
134+
## [br][br]
135+
## Settings will be automatically updated. The [param node] may be a string or
136+
## [NodePath] pointing to a node, or an actual [Node] instance. If the given
137+
## property is already tracked, this method does nothing.
138+
func add_state(node: Variant, property: String):
139+
var property_path := PropertyEntry.make_path(root, node, property)
140+
if not property_path or state_properties.has(property_path):
141+
return
142+
143+
state_properties.push_back(property_path)
144+
_properties_dirty = true
145+
_reprocess_settings.call_deferred()
146+
147+
func _notification(what: int) -> void:
148+
if what == NOTIFICATION_EDITOR_PRE_SAVE:
149+
update_configuration_warnings()
150+
151+
func _get_configuration_warnings() -> PackedStringArray:
152+
if not root:
153+
root = get_parent()
154+
155+
# Explore state properties
156+
if not root:
157+
return ["No valid root node found!"]
158+
159+
var result := PackedStringArray()
160+
result.append_array(_NetfoxEditorUtils.gather_properties(root, "_get_rollback_state_properties",
161+
func(node, prop):
162+
add_state(node, prop)
163+
))
164+
165+
return result
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
uid://ct51w7rsjcxvw
26.8 KB
Loading

0 commit comments

Comments
 (0)