-
-
Notifications
You must be signed in to change notification settings - Fork 61
Rollback No More #588
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Rollback No More #588
Changes from 9 commits
347fff3
1ed9f78
12df848
259bfa7
c8e0d12
89f2641
4d01d7c
f91521b
cde4d8f
4fbb124
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| [remap] | ||
|
|
||
| importer="texture" | ||
| type="CompressedTexture2D" | ||
| uid="uid://dd227x8br84rs" | ||
| path="res://.godot/imported/input-sender.svg-7b3cd669dc50ce8229dd58ca509f298a.ctex" | ||
| metadata={ | ||
| "vram_texture": false | ||
| } | ||
|
|
||
| [deps] | ||
|
|
||
| source_file="res://addons/netfox/icons/input-sender.svg" | ||
| dest_files=["res://.godot/imported/input-sender.svg-7b3cd669dc50ce8229dd58ca509f298a.ctex"] | ||
|
|
||
| [params] | ||
|
|
||
| compress/mode=0 | ||
| compress/high_quality=false | ||
| compress/lossy_quality=0.7 | ||
| compress/hdr_compression=1 | ||
| compress/normal_map=0 | ||
| compress/channel_pack=0 | ||
| mipmaps/generate=false | ||
| mipmaps/limit=-1 | ||
| roughness/mode=0 | ||
| roughness/src_normal="" | ||
| process/fix_alpha_border=true | ||
| process/premult_alpha=false | ||
| process/normal_map_invert_y=false | ||
| process/hdr_as_srgb=false | ||
| process/hdr_clamp_exposure=false | ||
| process/size_limit=0 | ||
| detect_3d/compress_to=1 | ||
| svg/scale=1.0 | ||
| editor/scale_with_editor_scale=false | ||
| editor/convert_colors_with_editor_theme=false |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,199 @@ | ||
| @tool | ||
| extends Node | ||
| class_name InputSender | ||
|
|
||
| ## Stores inputs and sends them to server. | ||
| ## [br][br] | ||
| ## [InputSender] can be used alone or with [Simulator]. | ||
|
|
||
| ## Emitted when [InputSender] receives input from client on [signal NetworkTime.on_tick] | ||
| ## [InputSender] handles applying received input internally before emitting this signal. | ||
| ## Emitted only on hosts. | ||
| signal new_input_received(tick : int) | ||
|
|
||
| ## Emitted when [InputSender] doesnt receive anything from client on [signal NetworkTime.on_tick] | ||
| ## [InputSender] handles applying latest known input internally before emitting this signal. | ||
| ## Emitted only on hosts. | ||
| signal input_missing(current_tick : int, latest_known_input_tick : int) | ||
|
|
||
| ## The root node for resolving node paths in inputs. Defaults to the parent node. | ||
| @export var root: Node = get_parent() | ||
|
|
||
| @export_group("Input") | ||
| ## Properties that define the input for the game simulation. | ||
| ## [br][br] | ||
| ## Input properties drive the simulation, which in turn results in updated state | ||
| ## properties. Input is recorded after every network tick. | ||
| @export var input_properties: Array[String] | ||
|
|
||
| # Make sure this exists from the get-go, just not in the scene tree | ||
| ## Decides which peers will receive updates | ||
| var visibility_filter := PeerVisibilityFilter.new() | ||
|
|
||
| var _input_properties := _PropertyPool.new() | ||
| var _properties_dirty: bool = false | ||
| var _last_emitted_tick: int = -1 | ||
| var _logger := NetfoxLogger._for_netfox("InputSender") | ||
|
|
||
| # Flag to connect signals only once. | ||
| var _signals_connected : bool = false | ||
|
|
||
| func _ready() -> void: | ||
| if Engine.is_editor_hint(): | ||
| return | ||
|
|
||
| if not NetworkTime.is_initial_sync_done(): | ||
| # Wait for time sync to complete | ||
| await NetworkTime.after_sync | ||
|
|
||
| func _enter_tree() -> void: | ||
| if Engine.is_editor_hint(): | ||
| return | ||
|
|
||
| if not visibility_filter: | ||
| visibility_filter = PeerVisibilityFilter.new() | ||
|
|
||
| if not visibility_filter.get_parent(): | ||
| add_child(visibility_filter) | ||
|
|
||
| if not NetworkTime.is_initial_sync_done(): | ||
| # Wait for time sync to complete | ||
| await NetworkTime.after_sync | ||
| process_settings.call_deferred() | ||
|
|
||
| ## Process settings. | ||
| ## [br][br] | ||
| ## Call this after any change to configuration. Updates based on authority too | ||
| ## ( calls process_authority ). | ||
| func process_settings() -> void: | ||
| process_authority() | ||
|
|
||
| # Register identifiers | ||
| for node in _input_properties.get_subjects(): | ||
| NetworkIdentityServer.register_node(node) | ||
|
|
||
| # Register visibility filter | ||
| for node in _input_properties.get_subjects(): | ||
| NetworkSynchronizationServer.register_visibility_filter(node, visibility_filter) | ||
|
|
||
| if not _signals_connected: | ||
| _connect_signals() | ||
| _signals_connected = true | ||
|
|
||
| ## Process settings based on authority. | ||
| ## [br][br] | ||
| ## Call this whenever the authority of input node changes. | ||
| ## Make sure to do this at the same time on all peers. | ||
| func process_authority(): | ||
| for node in _input_properties.get_subjects(): | ||
| for property in _input_properties.get_properties_of(node): | ||
| NetworkHistoryServer.deregister_input_sender(node, property) | ||
| NetworkSynchronizationServer.deregister_input_sender(node, property) | ||
|
|
||
| # Process authority | ||
| _input_properties.set_from_paths(root, input_properties) | ||
|
|
||
| # Register new recorded inputs | ||
| for node in _input_properties.get_subjects(): | ||
| for property in _input_properties.get_properties_of(node): | ||
| NetworkHistoryServer.register_input_sender(node, property) | ||
| NetworkSynchronizationServer.register_input_sender(node, property) | ||
|
|
||
| ## Add an input property. | ||
| ## [br][br] | ||
| ## Settings will be automatically updated. The [param node] may be a string or | ||
| ## [NodePath] pointing to a node, or an actual [Node] instance. If the given | ||
| ## property is already tracked, this method does nothing. | ||
| func add_input(node: Variant, property: String) -> void: | ||
| var property_path := PropertyEntry.make_path(root, node, property) | ||
| if not property_path or input_properties.has(property_path): | ||
| return | ||
|
|
||
| input_properties.push_back(property_path) | ||
| _properties_dirty = true | ||
| _reprocess_settings.call_deferred() | ||
|
|
||
| func _notification(what: int) -> void: | ||
| if what == NOTIFICATION_EDITOR_PRE_SAVE: | ||
| update_configuration_warnings() | ||
| elif what == NOTIFICATION_PREDELETE: | ||
| for node in _input_properties.get_subjects(): | ||
| NetworkSynchronizationServer.deregister(node) | ||
| NetworkIdentityServer.deregister_node(node) | ||
| NetworkHistoryServer.deregister(node) | ||
|
|
||
| func _get_configuration_warnings() -> PackedStringArray: | ||
| if not root: | ||
| root = get_parent() | ||
|
|
||
| # Check if root exists. | ||
| if not root: | ||
| return ["No valid root node found!"] | ||
|
|
||
| var result := PackedStringArray() | ||
|
|
||
| result.append_array(_NetfoxEditorUtils.gather_properties(root, "_get_input_sender_input_properties", | ||
| func(node, prop): | ||
| add_input(node, prop) | ||
| )) | ||
|
|
||
| if _input_properties.is_empty() and input_properties.is_empty(): | ||
| return ["Input properties are not configured!"] | ||
|
|
||
| return result | ||
|
|
||
| func _reprocess_settings() -> void: | ||
| if not _properties_dirty or Engine.is_editor_hint(): | ||
| return | ||
|
|
||
| _properties_dirty = false | ||
| process_settings() | ||
|
|
||
| func _connect_signals() -> void: | ||
| NetworkTime.on_tick.connect(_on_tick) | ||
|
|
||
| # Check if [InputSender] received new input from client. | ||
| # Emit new_input_received with new snapshot applied if received input. | ||
| # Emit input_missing with latest snapshot if did not. | ||
| # This function only runs only on authority. | ||
| func _on_tick(delta: float, tick: int) -> void: | ||
| if not is_multiplayer_authority(): | ||
| return | ||
|
|
||
| # Get the latest input data available | ||
| # Known issue: If input sender is configured with multiple input nodes, | ||
| # Any fresh input from one node will trigger re-emitting of other node's inputs? | ||
| # TODO: look at above issue. | ||
| var latest_input_tick := NetworkHistoryServer.get_latest_input_sender_for( | ||
| _input_properties.get_subjects(), tick) | ||
|
|
||
| if latest_input_tick == _last_emitted_tick: | ||
| # There is no new input data available | ||
| var latest_snapshot := NetworkHistoryServer._get_input_sender_snapshot(latest_input_tick) | ||
| if latest_snapshot: | ||
| _logger.trace("No new input is received, will emit input_missing after applying \ | ||
| snapshot: %s", [latest_snapshot]) | ||
|
|
||
| _apply_snapshot_for_self(latest_snapshot) | ||
| input_missing.emit(tick, latest_input_tick) | ||
| else: | ||
| # Iterate over fresh inputs and emit a signal with fresh inputs applied. | ||
| for i in range(_last_emitted_tick + 1, latest_input_tick + 1): | ||
| var snapshot := NetworkHistoryServer._get_input_sender_snapshot(i) | ||
| if snapshot: | ||
| _apply_snapshot_for_self(snapshot) | ||
| new_input_received.emit(i) | ||
| _last_emitted_tick = i | ||
|
|
||
| # Helper function to apply given snapshot for only this node. | ||
| # TODO Applying whole snapshot and iterating over ticks would be nicer | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That could be done with
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. input-sender has to apply matching tick snapshot before emitting its signals. I dont have clear answer to this before doing some work on "simulator?" node. |
||
| # if we decide to have singleton for this | ||
| func _apply_snapshot_for_self(snapshot : _Snapshot) -> void: | ||
| _logger.trace("Applying snapshot for self :%s", [snapshot]) | ||
| for subject in _input_properties.get_subjects(): | ||
| for property in _input_properties.get_properties_of(subject): | ||
|
|
||
| if snapshot.has_property(subject, property): | ||
| var value := snapshot.get_property(subject, property) | ||
| # TODO is this should be node.set_indexed ?? | ||
| subject.set_indexed(property, value) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good shout, I think there's two cases to this.
One is where all of the nodes are owned by the same peer, but the same update arrives in two different batches. That would mean that an input is larger than the MTU ( usually ~1400 bytes ), which probably means that something's off. Let's assume that inputs are atomic in this sense, will add an extra check as part of #556
The other case is where nodes have different owners. My understanding is that that does not matter, only the Input Sender's authority is checked. Which I agree with!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Documenting this possible issue in input_sender's class