Skip to content
Draft
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions addons/netfox/icons/input-sender.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 37 additions & 0 deletions addons/netfox/icons/input-sender.svg.import
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
199 changes: 199 additions & 0 deletions addons/netfox/input_sender.gd
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.
Copy link
Copy Markdown
Contributor

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!

Copy link
Copy Markdown
Contributor Author

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

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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That could be done with NetworkHistory. You can add the restore() call in NetworkTime.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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)
24 changes: 23 additions & 1 deletion addons/netfox/netfox.gd
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,23 @@ var SETTINGS: Array[Dictionary] = [
"name": "netfox/events/enabled",
"value": true,
"type": TYPE_BOOL
}
},
# Input Sender
{
"name": "netfox/input_sender/input_redundancy",
"value": 3,
"type" : TYPE_INT
},
{
"name": "netfox/input_sender/history_limit",
"value": 64,
"type" : TYPE_INT
},
{
"name": "netfox/input_sender/enable_input_broadcast",
"value": false,
"type" : TYPE_BOOL
},
]

const AUTOLOADS: Array[Dictionary] = [
Expand Down Expand Up @@ -238,6 +254,12 @@ const TYPES: Array[Dictionary] = [
"script": ROOT + "/rollback/predictive-synchronizer.gd",
"icon": ROOT + "/icons/predictive-synchronizer.svg"
},
{
"name": "InputSender",
"base": "Node",
"script": ROOT + "/input_sender.gd",
"icon": ROOT + "/icons/input-sender.svg"
},
]

func _enter_tree():
Expand Down
2 changes: 2 additions & 0 deletions addons/netfox/network-time.gd
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,8 @@ func _loop() -> void:
before_tick_loop.emit()

before_tick.emit(ticktime, tick)
NetworkHistoryServer._record_input_sender(tick)
NetworkSynchronizationServer._synchronize_input_sender(tick)

on_tick.emit(ticktime, tick)

Expand Down
2 changes: 1 addition & 1 deletion addons/netfox/rollback/rollback-synchronizer.gd
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@ func process_settings() -> void:
for node in _sim_nodes + _state_properties.get_subjects() + _input_properties.get_subjects():
RollbackSimulationServer.deregister_node(node)
_sim_nodes.clear()

process_authority()


# Register nodes for simulation and liveness
var managed_nodes := [root] + _collect_managed_nodes(root)
_logger.debug("Filtering managed nodes: %s", [managed_nodes])
Expand Down
Loading
Loading