-
-
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 all 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,258 @@ | ||
| @tool | ||
| extends Node | ||
| class_name InputSender | ||
|
|
||
| ## Stores inputs and sends them to host. | ||
| ## [br][br] | ||
| ## | ||
| ## [InputSender] is a multi purpose node to use on networked games, | ||
| ## It provides signals to code host and client side logic. | ||
| ## [InputSender] signals are tied and emitted on [signal NetworkTime.on_tick]. | ||
| ## | ||
| ## @experimental: | ||
| ## [InputSender] assumes input snapshots arrive as whole. (atomic), if snapshot | ||
| ## arrives with multiple parts, [InputSender] signals wont be reliable to | ||
| ## code game logic. | ||
|
|
||
| ## Emitted when [InputSender] receives input from remote owner of input_properties. | ||
| ## [InputSender] handles applying received input internally before emitting this signal. | ||
| ## Emitted only if [InputSender] has authority. | ||
| ## Use this signal to code host side logic. | ||
| signal network_input(tick : int) | ||
|
|
||
| ## Emitted for every tick if local peer has authority over input_property nodes. | ||
| ## [InputSender] will apply latest local inputs for this tick internally before | ||
| ## emitting this signal. | ||
| ## Use this signal to code client side logic which doesnt interfere with actual game state. | ||
| ## Examples: Playing a sound, showing a visual effect. | ||
| ## Dont use this signal to code same game logic on client side as it will not likely | ||
| ## be same with remote host machine, it will cause syncing issues if you are already | ||
| ## using some other method to syncronize game state (Syncronizers). | ||
| signal local_input(tick : int) | ||
|
|
||
| ## Emitted when [InputSender] doesnt receive anything from client on [signal NetworkTime.on_tick] | ||
| ## [InputSender] will apply latest known input internally before emitting this signal. | ||
| ## Emitted only if [InputSender] is authority. | ||
| ## Use this signal to code host side prediction logic. | ||
| signal missing_input(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) | ||
|
|
||
| # Applies local snapshot and emits local_input if has authority over input nodes. | ||
| # Then | ||
| # applies new received network snapshots and emits network_input snapshots if | ||
| # [InputSender] is authority, | ||
| # If did not receive new network snapshots, applies latest and emits input_missing | ||
| # with latest snapshot. | ||
| func _on_tick(delta: float, tick: int) -> void: | ||
| # First handle local_input signalling. | ||
| _apply_and_emit_local_inputs(tick) | ||
|
|
||
| # Move on to the network_input and input_missing signalling. | ||
| 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) | ||
| missing_input.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) | ||
| network_input.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) | ||
|
|
||
| # If the local peer has authority over input_property node, apply latest inputs | ||
| # and emit signal local_input. | ||
| func _apply_and_emit_local_inputs(for_tick : int) -> void: | ||
| if not _has_authority_over_input_nodes(): | ||
| return | ||
|
|
||
| var latest_local_snapshot := NetworkHistoryServer._get_input_sender_snapshot(for_tick) | ||
|
|
||
| if latest_local_snapshot: | ||
| _logger.trace("Applying local snapshot and emitting local_inputs: %s", [latest_local_snapshot]) | ||
| _apply_snapshot_for_self(latest_local_snapshot) | ||
| local_input.emit(for_tick) | ||
|
|
||
| # Helper function to determine if InputSender has authority over its input_properties | ||
| # This function iterates over input_properties subjects and checks if they have authority. | ||
| # If none of them has authority or no input_node is configured this will return false, | ||
| # If any of them has authority this will return true instantly. | ||
| # Its developers responsibility to always make sure input_nodes have same configuration. | ||
| # TODO make sure to document this responsibility to developer. | ||
| func _has_authority_over_input_nodes() -> bool: | ||
| for subject in _input_properties.get_subjects(): | ||
|
|
||
| # ObjectPool does not guarentee every subject is node. | ||
| if not subject is Node: | ||
| continue | ||
|
|
||
| # Found input node, check if it has authority | ||
| if subject.is_multiplayer_authority(): | ||
| return true | ||
|
|
||
| # Did not find any node, or none of them has authority. | ||
| return false | ||
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