Fresh start: replace with naxIO/netfox-cs-sample foundation
Complete replacement of the tactical-shooter project with the netfox-cs-sample (MIT) — a CS 1.6 inspired multiplayer FPS built with Godot 4 and netfox. ## What's new - Full CS-style gameplay: teams (T/CT), rounds, economy, buy menu - 6 weapons: Knife, Glock, USP, AK-47, M4A1, AWP - Bomb plant/defuse with 2 bombsites - Flashbang & smoke grenades - Proper netfox rollback netcode at 64 tick - Network popup UI for host/join - HUD, crosshair, round timer, scoreboard - All netfox singletons registered as autoloads (works in exported builds) ## Architecture - Listen-server (host from client, no dedicated server binary) - Multiplayer-fps game lives at examples/multiplayer-fps/ - Netfox addons registered as autoloads for exported build compat - Godot 4.7 with Forward+ renderer ## Removed - Old headless-server architecture (client_main, server_main, player.gd, etc.) - Custom netfox bootstrap with ENet fallback - Old ChaffGames FPS template (2,420 lines, 844 KB) - SimulationServer GDExtension stub - Godot-jolt physics (netfox sample uses default Godot physics) - Duplicate weapon_data.gd, anti_cheat.gd, round_manager.gd, etc. - Server browser API Python venv (87 MB) - test_range map and modular assets ## Preserved - Git history - Server config at config/default_server_config.cfg - Windows export preset - Build directory (gitignored) Co-authored-by: naxIO <naxIO@users.noreply.github.com>
This commit is contained in:
@@ -1,31 +0,0 @@
|
||||
# Contributors
|
||||
|
||||
This addon, and the entirety of [netfox] is a shared effort of [Fox's Sake
|
||||
Studio], and the community. The following is the list of community contributors
|
||||
involved with netfox:
|
||||
|
||||
* Alberto Klocker <albertok@gmail.com>
|
||||
* Andrew Davis <jonandrewdavis@gmail.com>
|
||||
* Bryan Lee <42545742+bryanmylee@users.noreply.github.com>
|
||||
* Dan Schuman <danschuman@gmail.com>
|
||||
* Dustie <77035922+DustieDog@users.noreply.github.com>
|
||||
* Eric Volpone <ericvolpone@gmail.com>
|
||||
* Gordon MacPherson <gordon@gordonite.tech>
|
||||
* Jake Cattrall <krazyjakee@gmail.com>
|
||||
* Jon Stevens <1030863+jonstvns@users.noreply.github.com>
|
||||
* Joseph Michael Ware <9at25jnr3@mozmail.com>
|
||||
* Matias Kumpulainen <kumpulainen.matias@gmail.com>
|
||||
* Nicolas Batty <nicolas.batty@gmail.com>
|
||||
* Ricky <77863853+RickyYCheng@users.noreply.github.com>
|
||||
* Riordan-DC <44295008+Riordan-DC@users.noreply.github.com>
|
||||
* Russ <622740+russ-@users.noreply.github.com>
|
||||
* Ryan Roden-Corrent <github@rcorre.net>
|
||||
* TheYellowArchitect <dmalandris@uth.gr>
|
||||
* TheYellowArchitect <hello@theyellowarchitect.com>
|
||||
* camperotactico <alberto.vgdd@gmail.com>
|
||||
* gk98s <89647115+gk98s@users.noreply.github.com>
|
||||
* zibetnu <9at25jnr3@mozmail.com>
|
||||
|
||||
[netfox]: https://github.com/foxssake/netfox
|
||||
[Fox's Sake Studio]: https://github.com/foxssake/
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
extends RefCounted
|
||||
class_name _DiffHistoryEncoder
|
||||
|
||||
var _history: _PropertyHistoryBuffer
|
||||
var _property_cache: PropertyCache
|
||||
|
||||
var _full_snapshot := {}
|
||||
var _encoded_snapshot := {}
|
||||
|
||||
var _property_indexes := _BiMap.new()
|
||||
|
||||
var _version := 0
|
||||
var _has_received := false
|
||||
|
||||
static var _logger := NetfoxLogger._for_netfox("DiffHistoryEncoder")
|
||||
|
||||
func _init(p_history: _PropertyHistoryBuffer, p_property_cache: PropertyCache):
|
||||
_history = p_history
|
||||
_property_cache = p_property_cache
|
||||
|
||||
func add_properties(properties: Array[PropertyEntry]) -> void:
|
||||
var has_new_properties := false
|
||||
|
||||
for property_entry in properties:
|
||||
var is_new := _ensure_property_idx(property_entry.to_string())
|
||||
has_new_properties = has_new_properties or is_new
|
||||
|
||||
# If we added any new properties, increment version
|
||||
if has_new_properties:
|
||||
_version = (_version + 1) % 256
|
||||
|
||||
func encode(tick: int, reference_tick: int, properties: Array[PropertyEntry]) -> PackedByteArray:
|
||||
assert(properties.size() <= 255, "Property indices may not fit into bytes!")
|
||||
|
||||
var snapshot := _history.get_snapshot(tick)
|
||||
var property_strings := properties.map(func(it): return it.to_string())
|
||||
|
||||
var reference_snapshot := _history.get_history(reference_tick)
|
||||
var diff_snapshot := reference_snapshot.make_patch(snapshot)
|
||||
|
||||
_full_snapshot = snapshot.as_dictionary()
|
||||
_encoded_snapshot = diff_snapshot.as_dictionary()
|
||||
|
||||
if diff_snapshot.is_empty():
|
||||
return PackedByteArray()
|
||||
|
||||
var buffer := StreamPeerBuffer.new()
|
||||
buffer.put_u8(_version)
|
||||
|
||||
for property in diff_snapshot.properties():
|
||||
var property_idx := _property_indexes.get_by_value(property) as int
|
||||
var property_value = diff_snapshot.get_value(property)
|
||||
|
||||
buffer.put_u8(property_idx)
|
||||
buffer.put_var(property_value)
|
||||
|
||||
return buffer.data_array
|
||||
|
||||
func decode(data: PackedByteArray, properties: Array[PropertyEntry]) -> _PropertySnapshot:
|
||||
var result := _PropertySnapshot.new()
|
||||
|
||||
if data.is_empty():
|
||||
return result
|
||||
|
||||
var buffer := StreamPeerBuffer.new()
|
||||
buffer.data_array = data
|
||||
|
||||
var packet_version := buffer.get_u8()
|
||||
if packet_version != _version:
|
||||
if not _has_received:
|
||||
# This is the first time we receive data
|
||||
# Assume the version is OK
|
||||
_version = packet_version
|
||||
else:
|
||||
# Since we don't remove entries, only add, we can still parse what
|
||||
# we can
|
||||
_logger.warning("Property config version mismatch - own %d != received %d", [_version, packet_version])
|
||||
|
||||
_has_received = true
|
||||
|
||||
while buffer.get_available_bytes() > 0:
|
||||
var property_idx := buffer.get_u8()
|
||||
var property_value := buffer.get_var()
|
||||
if not _property_indexes.has_key(property_idx):
|
||||
_logger.warning("Received unknown property index %d, ignoring!", [property_idx])
|
||||
continue
|
||||
|
||||
var property_entry := _property_indexes.get_by_key(property_idx)
|
||||
result.set_value(property_entry, property_value)
|
||||
|
||||
return result
|
||||
|
||||
func apply(tick: int, snapshot: _PropertySnapshot, reference_tick: int, sender: int = -1) -> bool:
|
||||
if tick < NetworkRollback.history_start:
|
||||
# State too old!
|
||||
_logger.error(
|
||||
"Received diff snapshot for @%d, rejecting because older than %s frames",
|
||||
[tick, NetworkRollback.history_limit]
|
||||
)
|
||||
return false
|
||||
|
||||
if snapshot.is_empty():
|
||||
return true
|
||||
|
||||
if sender > 0:
|
||||
snapshot.sanitize(sender, _property_cache)
|
||||
if snapshot.is_empty():
|
||||
_logger.warning("Received invalid diff from #%s for @%s", [sender, tick])
|
||||
return false
|
||||
|
||||
if not _history.has(reference_tick):
|
||||
# Reference tick missing, hope for the best
|
||||
_logger.warning("Reference tick %d missing for #%s applying %d", [reference_tick, sender, tick])
|
||||
|
||||
var reference_snapshot := _history.get_snapshot(reference_tick)
|
||||
_history.set_snapshot(tick, reference_snapshot.merge(snapshot))
|
||||
return true
|
||||
|
||||
# TODO: Rework metrics so these are not needed
|
||||
func get_encoded_snapshot() -> Dictionary:
|
||||
return _encoded_snapshot
|
||||
|
||||
func get_full_snapshot() -> Dictionary:
|
||||
return _full_snapshot
|
||||
|
||||
func _ensure_property_idx(property: String) -> bool:
|
||||
if _property_indexes.has_value(property):
|
||||
return false
|
||||
|
||||
assert(_property_indexes.size() < 256, "Property index map is full, can't add new property!")
|
||||
var idx := hash(property) % 256
|
||||
while _property_indexes.has_key(idx):
|
||||
idx = hash(idx + 1) % 256
|
||||
_property_indexes.put(idx, property)
|
||||
|
||||
return true
|
||||
@@ -1 +0,0 @@
|
||||
uid://dc73evbedbmvs
|
||||
@@ -1,116 +0,0 @@
|
||||
extends RefCounted
|
||||
class_name _RedundantHistoryEncoder
|
||||
|
||||
var redundancy: int = 4:
|
||||
get = get_redundancy,
|
||||
set = set_redundancy
|
||||
|
||||
var _history: _PropertyHistoryBuffer
|
||||
var _properties: Array[PropertyEntry]
|
||||
var _property_cache: PropertyCache
|
||||
|
||||
var _version := 0
|
||||
var _has_received := false
|
||||
|
||||
var _logger := NetfoxLogger._for_netfox("RedundantHistoryEncoder")
|
||||
|
||||
func get_redundancy() -> int:
|
||||
return redundancy
|
||||
|
||||
func set_redundancy(p_redundancy: int):
|
||||
if p_redundancy <= 0:
|
||||
_logger.warning(
|
||||
"Attempting to set redundancy to %d, which would send no data!", [p_redundancy]
|
||||
)
|
||||
return
|
||||
|
||||
redundancy = p_redundancy
|
||||
|
||||
func set_properties(properties: Array[PropertyEntry]) -> void:
|
||||
if _properties != properties:
|
||||
_version = (_version + 1) % 256
|
||||
_properties = properties.duplicate()
|
||||
|
||||
func encode(tick: int, properties: Array[PropertyEntry]) -> Array:
|
||||
if _history.is_empty():
|
||||
return []
|
||||
var data := []
|
||||
|
||||
for i in range(mini(redundancy, _history.size())):
|
||||
var offset_tick := tick - i
|
||||
if offset_tick < _history.get_earliest_tick():
|
||||
break
|
||||
|
||||
var snapshot := _history.get_snapshot(offset_tick)
|
||||
for property in properties:
|
||||
data.append(snapshot.get_value(property.to_string()))
|
||||
|
||||
data.append(_version)
|
||||
return data
|
||||
|
||||
func decode(data: Array, properties: Array[PropertyEntry]) -> Array[_PropertySnapshot]:
|
||||
if data.is_empty() or properties.is_empty():
|
||||
return []
|
||||
|
||||
var packet_version := data.pop_back() as int
|
||||
|
||||
if packet_version != _version:
|
||||
if not _has_received:
|
||||
# First packet, assume version is OK
|
||||
_version = packet_version
|
||||
else:
|
||||
# Version mismatch, can't parse
|
||||
_logger.warning("Version mismatch! own: %d, received: %s", [_version, packet_version])
|
||||
return []
|
||||
|
||||
var result: Array[_PropertySnapshot] = []
|
||||
var redundancy := data.size() / properties.size()
|
||||
result.assign(range(redundancy)
|
||||
.map(func(__): return _PropertySnapshot.new())
|
||||
)
|
||||
|
||||
for i in range(data.size()):
|
||||
var offset_idx := i / properties.size()
|
||||
var prop_idx := i % properties.size()
|
||||
|
||||
result[offset_idx].set_value(properties[prop_idx].to_string(), data[i])
|
||||
|
||||
_has_received = true
|
||||
|
||||
return result
|
||||
|
||||
# Returns earliest new tick as int, or -1 if no new ticks applied
|
||||
func apply(tick: int, snapshots: Array[_PropertySnapshot], sender: int = 0) -> int:
|
||||
var earliest_new_tick = -1
|
||||
|
||||
for i in range(snapshots.size()):
|
||||
var offset_tick := tick - i
|
||||
var snapshot := snapshots[i]
|
||||
|
||||
if offset_tick < NetworkRollback.history_start:
|
||||
# Data too old
|
||||
_logger.warning(
|
||||
"Received data for %s, rejecting because older than %s frames",
|
||||
[offset_tick, NetworkRollback.history_limit]
|
||||
)
|
||||
continue
|
||||
|
||||
if sender > 0:
|
||||
snapshot.sanitize(sender, _property_cache)
|
||||
if snapshot.is_empty():
|
||||
# No valid properties ( probably after sanitize )
|
||||
_logger.warning("Received invalid data from %d for tick %d", [sender, tick])
|
||||
continue
|
||||
|
||||
var known_snapshot := _history.get_snapshot(offset_tick)
|
||||
if not known_snapshot.equals(snapshot):
|
||||
# Received a new snapshot, store and emit signal
|
||||
_history.set_snapshot(offset_tick, snapshot)
|
||||
earliest_new_tick = offset_tick
|
||||
|
||||
return earliest_new_tick
|
||||
|
||||
|
||||
func _init(p_history: _PropertyHistoryBuffer, p_property_cache: PropertyCache):
|
||||
_history = p_history
|
||||
_property_cache = p_property_cache
|
||||
@@ -1 +0,0 @@
|
||||
uid://ytn07qahpatv
|
||||
@@ -1,67 +0,0 @@
|
||||
extends RefCounted
|
||||
class_name _SnapshotHistoryEncoder
|
||||
|
||||
var _history: _PropertyHistoryBuffer
|
||||
var _property_cache: PropertyCache
|
||||
var _properties: Array[PropertyEntry]
|
||||
|
||||
var _version := -1
|
||||
var _has_received := false
|
||||
|
||||
static var _logger := NetfoxLogger._for_netfox("_SnapshotHistoryEncoder")
|
||||
|
||||
func _init(p_history: _PropertyHistoryBuffer, p_property_cache: PropertyCache):
|
||||
_history = p_history
|
||||
_property_cache = p_property_cache
|
||||
|
||||
func set_properties(properties: Array[PropertyEntry]) -> void:
|
||||
if _properties != properties:
|
||||
_version = (_version + 1) % 256
|
||||
_properties = properties.duplicate()
|
||||
|
||||
func encode(tick: int, properties: Array[PropertyEntry]) -> Array:
|
||||
var snapshot := _history.get_snapshot(tick)
|
||||
var data := []
|
||||
data.resize(properties.size())
|
||||
|
||||
for i in range(properties.size()):
|
||||
data[i] = snapshot.get_value(properties[i].to_string())
|
||||
data.append(_version)
|
||||
|
||||
return data
|
||||
|
||||
func decode(data: Array, properties: Array[PropertyEntry]) -> _PropertySnapshot:
|
||||
var result := _PropertySnapshot.new()
|
||||
var packet_version = data.pop_back()
|
||||
|
||||
if packet_version != _version:
|
||||
if not _has_received:
|
||||
# First packet, assume version is OK
|
||||
_version = packet_version
|
||||
else:
|
||||
# Version mismatch, can't parse
|
||||
_logger.warning("Version mismatch! own: %d, received: %s", [_version, packet_version])
|
||||
return result
|
||||
|
||||
if properties.size() != data.size():
|
||||
_logger.warning("Received snapshot with %d entries, with %d known - parsing as much as possible", [data.size(), properties.size()])
|
||||
|
||||
for i in range(0, mini(data.size(), properties.size())):
|
||||
result.set_value(properties[i].to_string(), data[i])
|
||||
|
||||
_has_received = true
|
||||
|
||||
return result
|
||||
|
||||
func apply(tick: int, snapshot: _PropertySnapshot, sender: int = -1) -> bool:
|
||||
if tick < NetworkRollback.history_start:
|
||||
# State too old!
|
||||
_logger.error("Received full snapshot for %s, rejecting because older than %s frames", [tick, NetworkRollback.history_limit])
|
||||
return false
|
||||
|
||||
if sender > 0:
|
||||
snapshot.sanitize(sender, _property_cache)
|
||||
if snapshot.is_empty(): return false
|
||||
|
||||
_history.set_snapshot(tick, snapshot)
|
||||
return true
|
||||
@@ -1 +0,0 @@
|
||||
uid://dwhlghjsgdy4o
|
||||
@@ -0,0 +1,124 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 16 16"
|
||||
xml:space="preserve"
|
||||
sodipodi:docname="predictive-synchronizer.svg"
|
||||
width="16"
|
||||
height="16"
|
||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1"><inkscape:path-effect
|
||||
effect="fillet_chamfer"
|
||||
id="path-effect2"
|
||||
is_visible="true"
|
||||
lpeversion="1"
|
||||
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,1,0,1 @ F,0,0,1,0,1,0,1 @ F,0,0,1,0,1,0,1"
|
||||
radius="1"
|
||||
unit="px"
|
||||
method="auto"
|
||||
mode="F"
|
||||
chamfer_steps="1"
|
||||
flexible="false"
|
||||
use_knot_distance="true"
|
||||
apply_no_radius="true"
|
||||
apply_with_radius="true"
|
||||
only_selected="false"
|
||||
hide_knots="false" /><inkscape:path-effect
|
||||
effect="fillet_chamfer"
|
||||
id="path-effect1"
|
||||
is_visible="true"
|
||||
lpeversion="1"
|
||||
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
|
||||
radius="0"
|
||||
unit="px"
|
||||
method="auto"
|
||||
mode="F"
|
||||
chamfer_steps="1"
|
||||
flexible="false"
|
||||
use_knot_distance="true"
|
||||
apply_no_radius="true"
|
||||
apply_with_radius="true"
|
||||
only_selected="false"
|
||||
hide_knots="false" /></defs><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="24.40625"
|
||||
inkscape:cx="8.8297055"
|
||||
inkscape:cy="10.140845"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1368"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Layer_1"><inkscape:grid
|
||||
id="grid1"
|
||||
units="px"
|
||||
originx="0"
|
||||
originy="0"
|
||||
spacingx="1"
|
||||
spacingy="1"
|
||||
empcolor="#0099e5"
|
||||
empopacity="0.30196078"
|
||||
color="#0099e5"
|
||||
opacity="0.14902"
|
||||
empspacing="5"
|
||||
enabled="true"
|
||||
visible="false" /></sodipodi:namedview>
|
||||
<style
|
||||
type="text/css"
|
||||
id="style1">
|
||||
.st0{fill:none;stroke:#E0E0E0;stroke-width:6.875;stroke-linecap:round;stroke-linejoin:round;}
|
||||
.st1{fill:none;stroke:#E0E0E0;stroke-width:6.875;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.66;}
|
||||
.st2{fill:none;stroke:#E0E0E0;stroke-width:6.875;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.33;}
|
||||
.st3{fill:#5FFF97;}
|
||||
.st4{fill:#FF5F5F;}
|
||||
</style>
|
||||
<path
|
||||
id="path6"
|
||||
class="st0"
|
||||
d="M 8,8 10.425,5.575"
|
||||
style="stroke-width:2;stroke-dasharray:none" />
|
||||
<path
|
||||
id="path7"
|
||||
class="st1"
|
||||
d="M 8,8 V 4.575"
|
||||
style="stroke-width:2;stroke-dasharray:none" />
|
||||
<path
|
||||
id="path8"
|
||||
class="st2"
|
||||
d="M 8,8 5.575,5.575"
|
||||
style="stroke-width:2;stroke-dasharray:none" />
|
||||
<circle
|
||||
id="path4"
|
||||
class="st0"
|
||||
cx="8"
|
||||
cy="8"
|
||||
style="stroke-width:2;stroke-dasharray:none"
|
||||
r="5" />
|
||||
|
||||
|
||||
<path
|
||||
style="fill:#6f8a91"
|
||||
id="rect1"
|
||||
width="8"
|
||||
height="1.8"
|
||||
x="4.4037995"
|
||||
y="11.704836"
|
||||
sodipodi:type="rect"
|
||||
d="M 5.4037995,11.704836 H 11.4038 a 1,1 45 0 1 1,1 0.90553849,0.90553849 141.34019 0 1 -1,0.8 H 5.4037995 a 1,1 45 0 1 -1,-1 0.90553849,0.90553849 141.34019 0 1 1,-0.8 z"
|
||||
inkscape:path-effect="#path-effect2"
|
||||
transform="translate(-0.40379953,-2.1992974)" /></svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dsr2mjcr1lh6v"
|
||||
path="res://.godot/imported/predictive-synchronizer.svg-c1a256fe714777cb768d64b354a43e2b.ctex"
|
||||
metadata={
|
||||
"has_editor_variant": true,
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/netfox/icons/predictive-synchronizer.svg"
|
||||
dest_files=["res://.godot/imported/predictive-synchronizer.svg-c1a256fe714777cb768d64b354a43e2b.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
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/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
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=true
|
||||
editor/convert_colors_with_editor_theme=true
|
||||
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dy3qj4jbh0enm"
|
||||
path="res://.godot/imported/rewindable-action.svg-2eb32b564a2fe4ecb8b77ec5b55683ab.ctex"
|
||||
metadata={
|
||||
"has_editor_variant": true,
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/netfox/icons/rewindable-action.svg"
|
||||
dest_files=["res://.godot/imported/rewindable-action.svg-2eb32b564a2fe4ecb8b77ec5b55683ab.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
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/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
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=true
|
||||
editor/convert_colors_with_editor_theme=true
|
||||
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://csg26ysqqb4xe"
|
||||
path="res://.godot/imported/rollback-synchronizer.svg-99c6071e1009de5a35a481b2f486c380.ctex"
|
||||
metadata={
|
||||
"has_editor_variant": true,
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/netfox/icons/rollback-synchronizer.svg"
|
||||
dest_files=["res://.godot/imported/rollback-synchronizer.svg-99c6071e1009de5a35a481b2f486c380.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
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/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
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=true
|
||||
editor/convert_colors_with_editor_theme=true
|
||||
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://ogbi1hffcoyh"
|
||||
path="res://.godot/imported/state-synchronizer.svg-9cb9447ba79f114a58e468a24d17b860.ctex"
|
||||
metadata={
|
||||
"has_editor_variant": true,
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/netfox/icons/state-synchronizer.svg"
|
||||
dest_files=["res://.godot/imported/state-synchronizer.svg-9cb9447ba79f114a58e468a24d17b860.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
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/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
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=true
|
||||
editor/convert_colors_with_editor_theme=true
|
||||
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cpaqxcohxtb68"
|
||||
path="res://.godot/imported/tick-interpolator.svg-c60124cd7d287f516c89a6022efef330.ctex"
|
||||
metadata={
|
||||
"has_editor_variant": true,
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/netfox/icons/tick-interpolator.svg"
|
||||
dest_files=["res://.godot/imported/tick-interpolator.svg-c60124cd7d287f516c89a6022efef330.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
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/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
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=true
|
||||
editor/convert_colors_with_editor_theme=true
|
||||
+73
-7
@@ -10,6 +10,11 @@ var SETTINGS: Array[Dictionary] = [
|
||||
"value": true,
|
||||
"type": TYPE_BOOL
|
||||
},
|
||||
{
|
||||
"name": "netfox/general/use_raw_commands",
|
||||
"value": false,
|
||||
"type": TYPE_BOOL
|
||||
},
|
||||
# Logging
|
||||
NetfoxLogger._make_setting("netfox/logging/netfox_log_level"),
|
||||
# Time settings
|
||||
@@ -112,11 +117,41 @@ var SETTINGS: Array[Dictionary] = [
|
||||
"hint": PROPERTY_HINT_RANGE,
|
||||
"hint_string": "0,4,or_greater"
|
||||
},
|
||||
{
|
||||
"name": "netfox/rollback/enable_input_broadcast",
|
||||
"value": false,
|
||||
"type": TYPE_BOOL
|
||||
},
|
||||
{
|
||||
"name": "netfox/rollback/enable_diff_states",
|
||||
"value": true,
|
||||
"type": TYPE_BOOL
|
||||
},
|
||||
{
|
||||
"name": "netfox/rollback/full_state_interval",
|
||||
"value": 24,
|
||||
"type": TYPE_INT,
|
||||
"hint": PROPERTY_HINT_RANGE,
|
||||
"hint_string": "0,60,or_greater"
|
||||
},
|
||||
# StateSynchronizer
|
||||
{
|
||||
"name": "netfox/state_synchronizer/enable_diff_states",
|
||||
"value": true,
|
||||
"type": TYPE_BOOL
|
||||
},
|
||||
{
|
||||
"name": "netfox/state_synchronizer/history_limit",
|
||||
"value": 64,
|
||||
"type": TYPE_INT
|
||||
},
|
||||
{
|
||||
"name": "netfox/state_synchronizer/full_state_interval",
|
||||
"value": 24,
|
||||
"type": TYPE_INT,
|
||||
"hint": PROPERTY_HINT_RANGE,
|
||||
"hint_string": "0,60,or_greater"
|
||||
},
|
||||
# Events
|
||||
{
|
||||
"name": "netfox/events/enabled",
|
||||
@@ -145,6 +180,26 @@ const AUTOLOADS: Array[Dictionary] = [
|
||||
{
|
||||
"name": "NetworkPerformance",
|
||||
"path": ROOT + "/network-performance.gd"
|
||||
},
|
||||
{
|
||||
"name": "RollbackSimulationServer",
|
||||
"path": ROOT + "/servers/rollback-simulation-server.gd"
|
||||
},
|
||||
{
|
||||
"name": "NetworkHistoryServer",
|
||||
"path": ROOT + "/servers/network-history-server.gd"
|
||||
},
|
||||
{
|
||||
"name": "NetworkSynchronizationServer",
|
||||
"path": ROOT + "/servers/network-synchronization-server.gd"
|
||||
},
|
||||
{
|
||||
"name": "NetworkIdentityServer",
|
||||
"path": ROOT + "/servers/network-identity-server.gd"
|
||||
},
|
||||
{
|
||||
"name": "NetworkCommandServer",
|
||||
"path": ROOT + "/servers/network-command-server.gd"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -173,15 +228,22 @@ const TYPES: Array[Dictionary] = [
|
||||
"script": ROOT + "/rewindable-action.gd",
|
||||
"icon": ROOT + "/icons/rewindable-action.svg"
|
||||
},
|
||||
{
|
||||
"name": "PredictiveSynchronizer",
|
||||
"base": "Node",
|
||||
"script": ROOT + "/rollback/predictive-synchronizer.gd",
|
||||
"icon": ROOT + "/icons/predictive-synchronizer.svg"
|
||||
},
|
||||
]
|
||||
|
||||
func _enter_tree():
|
||||
for setting in SETTINGS:
|
||||
add_setting(setting)
|
||||
|
||||
|
||||
for autoload in AUTOLOADS:
|
||||
add_autoload_singleton(autoload.name, autoload.path)
|
||||
|
||||
if not has_autoload(autoload.name):
|
||||
add_autoload_singleton(autoload.name, autoload.path)
|
||||
|
||||
for type in TYPES:
|
||||
add_custom_type(type.name, type.base, load(type.script), load(type.icon))
|
||||
|
||||
@@ -189,10 +251,11 @@ func _exit_tree() -> void:
|
||||
if ProjectSettings.get_setting(&"netfox/general/clear_settings", false):
|
||||
for setting in SETTINGS:
|
||||
remove_setting(setting)
|
||||
|
||||
|
||||
for autoload in AUTOLOADS:
|
||||
remove_autoload_singleton(autoload.name)
|
||||
|
||||
if has_autoload(autoload.name):
|
||||
remove_autoload_singleton(autoload.name)
|
||||
|
||||
for type in TYPES:
|
||||
remove_custom_type(type.name)
|
||||
|
||||
@@ -212,5 +275,8 @@ func add_setting(setting: Dictionary) -> void:
|
||||
func remove_setting(setting: Dictionary) -> void:
|
||||
if not ProjectSettings.has_setting(setting.name):
|
||||
return
|
||||
|
||||
|
||||
ProjectSettings.clear(setting.name)
|
||||
|
||||
func has_autoload(name: String) -> bool:
|
||||
return ProjectSettings.has_setting("autoload/" + name)
|
||||
|
||||
@@ -106,12 +106,18 @@ func get_sent_state_props_ratio() -> float:
|
||||
func push_full_state(state: Dictionary) -> void:
|
||||
_full_state_props_accum += state.size()
|
||||
|
||||
func push_full_state_props(count: int) -> void:
|
||||
_full_state_props_accum += count
|
||||
|
||||
func push_full_state_broadcast(state: Dictionary) -> void:
|
||||
_full_state_props_accum += state.size() * (multiplayer.get_peers().size() - 1)
|
||||
|
||||
func push_sent_state(state: Dictionary) -> void:
|
||||
_sent_state_props_accum += state.size()
|
||||
|
||||
func push_sent_state_props(count: int) -> void:
|
||||
_sent_state_props_accum += count
|
||||
|
||||
func push_sent_state_broadcast(state: Dictionary) -> void:
|
||||
_sent_state_props_accum += state.size() * (multiplayer.get_peers().size() - 1)
|
||||
|
||||
|
||||
@@ -115,6 +115,11 @@ var _offset: float = 0.
|
||||
var _rtt: float = 0.
|
||||
var _rtt_jitter: float = 0.
|
||||
|
||||
@onready var _cmd_ping := NetworkCommandServer.register_command(_handle_ping, MultiplayerPeer.TRANSFER_MODE_UNRELIABLE)
|
||||
@onready var _cmd_pong := NetworkCommandServer.register_command(_handle_pong, MultiplayerPeer.TRANSFER_MODE_UNRELIABLE)
|
||||
@onready var _cmd_req_time := NetworkCommandServer.register_command(_handle_request_timestamp, MultiplayerPeer.TRANSFER_MODE_RELIABLE)
|
||||
@onready var _cmd_set_time := NetworkCommandServer.register_command(_handle_set_timestamp, MultiplayerPeer.TRANSFER_MODE_RELIABLE)
|
||||
|
||||
## Emitted after the initial time sync.
|
||||
##
|
||||
## At the start of the game, clients request an initial timestamp to kickstart
|
||||
@@ -145,7 +150,7 @@ func start() -> void:
|
||||
_sample_idx = 0
|
||||
_sample_buffer = _RingBuffer.new(sync_samples)
|
||||
|
||||
_request_timestamp.rpc_id(1)
|
||||
_cmd_req_time.send(PackedByteArray(), 1)
|
||||
|
||||
## Stop the time synchronization loop.
|
||||
func stop() -> void:
|
||||
@@ -169,7 +174,7 @@ func _loop() -> void:
|
||||
_awaiting_samples[_sample_idx] = sample
|
||||
|
||||
sample.ping_sent = _clock.get_time()
|
||||
_send_ping.rpc_id(1, _sample_idx)
|
||||
_cmd_ping.send(var_to_bytes(_sample_idx), 1)
|
||||
|
||||
_sample_idx += 1
|
||||
|
||||
@@ -235,15 +240,19 @@ func _discipline_clock() -> void:
|
||||
|
||||
_offset = offset - nudge
|
||||
|
||||
@rpc("any_peer", "call_remote", "unreliable")
|
||||
func _send_ping(idx: int) -> void:
|
||||
func _handle_ping(sender: int, data: PackedByteArray) -> void:
|
||||
var idx := bytes_to_var(data) as int
|
||||
var ping_received := _clock.get_time()
|
||||
var sender := multiplayer.get_remote_sender_id()
|
||||
|
||||
_send_pong.rpc_id(sender, idx, ping_received, _clock.get_time())
|
||||
_cmd_pong.send(var_to_bytes([idx, ping_received, _clock.get_time()]), sender)
|
||||
|
||||
func _handle_pong(sender: int, data: PackedByteArray) -> void:
|
||||
var args := bytes_to_var(data)
|
||||
|
||||
var idx := args[0] as int
|
||||
var ping_received := args[1] as float
|
||||
var pong_sent := args[2] as float
|
||||
|
||||
@rpc("any_peer", "call_remote", "unreliable")
|
||||
func _send_pong(idx: int, ping_received: float, pong_sent: float) -> void:
|
||||
var pong_received := _clock.get_time()
|
||||
|
||||
if not _awaiting_samples.has(idx):
|
||||
@@ -264,13 +273,13 @@ func _send_pong(idx: int, ping_received: float, pong_sent: float) -> void:
|
||||
# Discipline clock based on new sample
|
||||
_discipline_clock()
|
||||
|
||||
@rpc("any_peer", "call_remote", "reliable")
|
||||
func _request_timestamp() -> void:
|
||||
func _handle_request_timestamp(sender: int, data: PackedByteArray) -> void:
|
||||
_logger.debug("Requested initial timestamp @ %.4fs raw time", [_clock.get_raw_time()])
|
||||
_set_timestamp.rpc_id(multiplayer.get_remote_sender_id(), _clock.get_time())
|
||||
_cmd_set_time.send(var_to_bytes(_clock.get_time()), sender)
|
||||
|
||||
func _handle_set_timestamp(sender: int, data: PackedByteArray) -> void:
|
||||
var timestamp := bytes_to_var(data) as float
|
||||
|
||||
@rpc("any_peer", "call_remote", "reliable")
|
||||
func _set_timestamp(timestamp: float) -> void:
|
||||
_logger.debug("Received initial timestamp @ %.4fs raw time", [_clock.get_raw_time()])
|
||||
_clock.set_time(timestamp)
|
||||
_loop()
|
||||
|
||||
@@ -19,7 +19,7 @@ var tickrate: int:
|
||||
|
||||
## Whether to sync the network ticks to physics updates.
|
||||
##
|
||||
## When set to true, tickrate will be the same as the physics ticks per second,
|
||||
## When set to true, tickrate will be the same as the physics ticks per second,
|
||||
## and the network tick loop will be run inside the physics update process.
|
||||
##
|
||||
## [i]read-only[/i], you can change this in the project settings
|
||||
@@ -33,7 +33,7 @@ var sync_to_physics: bool:
|
||||
##
|
||||
## If the game itself runs slower than the configured tickrate, multiple ticks
|
||||
## will be run in a single go. However, to avoid an endless feedback loop of
|
||||
## running too many ticks in a frame, which makes the game even slower, which
|
||||
## running too many ticks in a frame, which makes the game even slower, which
|
||||
## results in even more ticks and so on, this setting is an upper limit on how
|
||||
## many ticks can be simulated in a single go.
|
||||
##
|
||||
@@ -51,7 +51,7 @@ var max_ticks_per_frame: int:
|
||||
##
|
||||
## Use this value in cases where timestamps need to be shared with the server.
|
||||
##
|
||||
## [i]Note:[/i] Time is continuously synced with the server. If the difference
|
||||
## [i]Note:[/i] Time is continuously synced with the server. If the difference
|
||||
## between local and server time is above a certain threshold, this value will
|
||||
## be adjusted.
|
||||
##
|
||||
@@ -72,7 +72,7 @@ var time: float:
|
||||
##
|
||||
## Use this value in cases where timestamps need to be shared with the server.
|
||||
##
|
||||
## [i]Note:[/i] Time is continuously synced with the server. If the difference
|
||||
## [i]Note:[/i] Time is continuously synced with the server. If the difference
|
||||
## between local and server time is above a certain threshold, this value will
|
||||
## be adjusted.
|
||||
##
|
||||
@@ -88,7 +88,7 @@ var tick: int:
|
||||
|
||||
## Threshold before recalibrating [member tick] and [member time].
|
||||
##
|
||||
## Time is continuously synced to the server. In case the time difference is
|
||||
## Time is continuously synced to the server. In case the time difference is
|
||||
## excessive between local and the server, both [code]tick[/code] and
|
||||
## [code]time[/code] will be reset to the estimated server values.
|
||||
## [br][br]
|
||||
@@ -119,7 +119,7 @@ var stall_threshold: float:
|
||||
|
||||
## Current network time in ticks on the server.
|
||||
##
|
||||
## This is value is only an estimate, and is regularly updated. This means that
|
||||
## This is value is only an estimate, and is regularly updated. This means that
|
||||
## this value can and probably will change depending on network conditions.
|
||||
## [br][br]
|
||||
## [i]read-only[/i]
|
||||
@@ -133,7 +133,7 @@ var remote_tick: int:
|
||||
|
||||
## Current network time in seconds on the server.
|
||||
##
|
||||
## This is value is only an estimate, and is regularly updated. This means that
|
||||
## This is value is only an estimate, and is regularly updated. This means that
|
||||
## this value can and probably will change depending on network conditions.
|
||||
## [br][br]
|
||||
## [i]read-only[/i]
|
||||
@@ -147,8 +147,8 @@ var remote_time: float:
|
||||
|
||||
## Estimated roundtrip time to server.
|
||||
##
|
||||
## This value is updated regularly, during server time sync. Latency can be
|
||||
## estimated as half of the roundtrip time. Returns the same as [member
|
||||
## This value is updated regularly, during server time sync. Latency can be
|
||||
## estimated as half of the roundtrip time. Returns the same as [member
|
||||
## _NetworkTimeSynchronizer.rtt].
|
||||
## [br][br]
|
||||
## Will always be 0 on servers.
|
||||
@@ -163,7 +163,7 @@ var remote_rtt: float:
|
||||
## Current network time in ticks.
|
||||
##
|
||||
## On clients, this value is synced to the server [i]only once[/i] when joining
|
||||
## the game. After that, it will increase monotonically, incrementing every
|
||||
## the game. After that, it will increase monotonically, incrementing every
|
||||
## single tick.
|
||||
## [br][br]
|
||||
## When hosting, this value is simply the number of ticks since game start.
|
||||
@@ -183,7 +183,7 @@ var local_tick: int:
|
||||
## Current network time in seconds.
|
||||
##
|
||||
## On clients, this value is synced to the server [i]only once[/i] when joining
|
||||
## the game. After that, it will increase monotonically, incrementing every
|
||||
## the game. After that, it will increase monotonically, incrementing every
|
||||
## single tick.
|
||||
## [br][br]
|
||||
## When hosting, this value is simply the seconds elapsed since game start.
|
||||
@@ -199,7 +199,7 @@ var local_time: float:
|
||||
return time
|
||||
set(v):
|
||||
push_error("Trying to set read-only variable local_time")
|
||||
|
||||
|
||||
|
||||
## Amount of time a single tick takes, in seconds.
|
||||
##
|
||||
@@ -231,13 +231,13 @@ var tick_factor: float:
|
||||
## Multiplier to get from physics process speeds to tick speeds.
|
||||
##
|
||||
## Some methods, like CharacterBody's move_and_slide take velocity in units/sec
|
||||
## and figure out the time delta on their own. However, they are not aware of
|
||||
## and figure out the time delta on their own. However, they are not aware of
|
||||
## netfox's time, so motion is all wrong in a network tick. For example, the
|
||||
## network ticks run at 30 fps, while the game is running at 60fps, thus
|
||||
## network ticks run at 30 fps, while the game is running at 60fps, thus
|
||||
## move_and_slide will also assume that it's running on 60fps, resulting in
|
||||
## slower than expected movement.
|
||||
##
|
||||
## To circument this, you can multiply any velocities with this variable, and
|
||||
## To circument this, you can multiply any velocities with this variable, and
|
||||
## get the desired speed. Don't forget to then divide by this value if it's a
|
||||
## persistent variable ( e.g. CharacterBody's velocity ).
|
||||
##
|
||||
@@ -256,9 +256,9 @@ var physics_factor: float:
|
||||
|
||||
## The maximum clock stretch factor allowed.
|
||||
##
|
||||
## For more context on clock stretch, see [member clock_stretch_factor]. The
|
||||
## minimum allowed clock stretch factor is derived as 1.0 / clock_stretch_max.
|
||||
## Setting this to larger values will allow for quicker clock adjustment at the
|
||||
## For more context on clock stretch, see [member clock_stretch_factor]. The
|
||||
## minimum allowed clock stretch factor is derived as 1.0 / clock_stretch_max.
|
||||
## Setting this to larger values will allow for quicker clock adjustment at the
|
||||
## cost of bigger deviations in game speed.
|
||||
## [br][br]
|
||||
## Make sure to adjust this value based on the game's needs.
|
||||
@@ -286,7 +286,7 @@ var suppress_offline_peer_warning: bool:
|
||||
## to catch up. When the remote clock is ahead of the simulation clock, the game
|
||||
## will run slightly faster to catch up with the remote clock.
|
||||
## [br][br]
|
||||
## This value indicates the current clock speed multiplier. Values over 1.0
|
||||
## This value indicates the current clock speed multiplier. Values over 1.0
|
||||
## indicate speeding up, under 1.0 indicate slowing down.
|
||||
## [br][br]
|
||||
## See [member clock_stretch_max] for clock stretch bounds.[br]
|
||||
@@ -300,7 +300,7 @@ var clock_stretch_factor: float:
|
||||
|
||||
## The current estimated offset between the reference clock and the simulation
|
||||
## clock.
|
||||
##
|
||||
##
|
||||
## Positive values mean the simulation clock is behind, and needs to run
|
||||
## slightly faster to catch up. Negative values mean the simulation clock is
|
||||
## ahead, and needs to slow down slightly.
|
||||
@@ -316,7 +316,7 @@ var clock_offset: float:
|
||||
## The current estimated offset between the reference clock and the remote
|
||||
## clock.
|
||||
##
|
||||
## Positive values mean the reference clock is behind the remote clock.
|
||||
## Positive values mean the reference clock is behind the remote clock.
|
||||
## Negative values mean the reference clock is ahead of the remote clock.
|
||||
## [br][br]
|
||||
## Returns the same as [member _NetworkTimeSynchronizer.remote_offset].
|
||||
@@ -426,21 +426,21 @@ func start() -> int:
|
||||
# Reset state
|
||||
_tick = 0
|
||||
_initial_sync_done = false
|
||||
|
||||
|
||||
# Host is always synced, as their time is considered ground truth
|
||||
_synced_peers[1] = true
|
||||
|
||||
# Start sync
|
||||
NetworkTimeSynchronizer.start()
|
||||
_state = _STATE_SYNCING
|
||||
|
||||
|
||||
if not multiplayer.is_server():
|
||||
await NetworkTimeSynchronizer.on_initial_sync
|
||||
|
||||
_tick = seconds_to_ticks(NetworkTimeSynchronizer.get_time())
|
||||
_initial_sync_done = true
|
||||
_state = _STATE_ACTIVE
|
||||
|
||||
|
||||
_submit_sync_success.rpc()
|
||||
else:
|
||||
_state = _STATE_ACTIVE
|
||||
@@ -462,13 +462,17 @@ func start() -> int:
|
||||
|
||||
## Stop NetworkTime.
|
||||
##
|
||||
## This will stop the time sync in the background, and no more ticks will be
|
||||
## This will stop the time sync in the background, and no more ticks will be
|
||||
## emitted until the next start.
|
||||
func stop() -> void:
|
||||
NetworkTimeSynchronizer.stop()
|
||||
_tickrate_handshake.stop()
|
||||
|
||||
# Reset state
|
||||
_state = _STATE_INACTIVE
|
||||
_synced_peers.clear()
|
||||
_tick = 0
|
||||
_initial_sync_done = false
|
||||
|
||||
if multiplayer.peer_disconnected.is_connected(_handle_peer_disconnect):
|
||||
multiplayer.peer_disconnected.disconnect(_handle_peer_disconnect)
|
||||
@@ -508,7 +512,7 @@ func _ready() -> void:
|
||||
|
||||
_tickrate_handshake = NetworkTickrateHandshake.new()
|
||||
add_child(_tickrate_handshake)
|
||||
|
||||
|
||||
# Proxy tickrate mismatch event
|
||||
_tickrate_handshake.on_tickrate_mismatch.connect(func(peer, tickrate):
|
||||
on_tickrate_mismatch.emit(peer, tickrate)
|
||||
@@ -524,10 +528,10 @@ func _loop() -> void:
|
||||
# Adjust local clock
|
||||
_clock.step(_clock_stretch_factor)
|
||||
var clock_diff := NetworkTimeSynchronizer.get_time() - _clock.get_time()
|
||||
|
||||
|
||||
# Ignore diffs under 1ms
|
||||
clock_diff = sign(clock_diff) * max(abs(clock_diff) - 0.001, 0.)
|
||||
|
||||
|
||||
var clock_stretch_min := 1. / clock_stretch_max
|
||||
# var clock_stretch_f = (1. + clock_diff / (1. * ticktime)) / 2.
|
||||
var clock_stretch_f := inverse_lerp(-ticktime, +ticktime, clock_diff)
|
||||
@@ -535,7 +539,7 @@ func _loop() -> void:
|
||||
|
||||
var previous_stretch_factor := _clock_stretch_factor
|
||||
_clock_stretch_factor = lerpf(clock_stretch_min, clock_stretch_max, clock_stretch_f)
|
||||
|
||||
|
||||
# Detect editor pause
|
||||
var clock_step := _clock.get_time() - _last_process_time
|
||||
var clock_step_raw := clock_step / previous_stretch_factor
|
||||
@@ -550,7 +554,7 @@ func _loop() -> void:
|
||||
_was_paused = false
|
||||
_next_tick_time += clock_step
|
||||
_tick = seconds_to_ticks(NetworkTimeSynchronizer.get_time())
|
||||
|
||||
|
||||
# Run tick loop if needed
|
||||
var ticks_in_loop := 0
|
||||
_last_process_time = _clock.get_time()
|
||||
@@ -559,19 +563,24 @@ func _loop() -> void:
|
||||
before_tick_loop.emit()
|
||||
|
||||
before_tick.emit(ticktime, tick)
|
||||
|
||||
on_tick.emit(ticktime, tick)
|
||||
|
||||
after_tick.emit(ticktime, tick)
|
||||
|
||||
NetworkHistoryServer._record_sync_state(tick + 1)
|
||||
NetworkSynchronizationServer._synchronize_sync_state(tick + 1)
|
||||
|
||||
_tick += 1
|
||||
ticks_in_loop += 1
|
||||
_next_tick_time += ticktime
|
||||
|
||||
|
||||
if ticks_in_loop > 0:
|
||||
after_tick_loop.emit()
|
||||
NetworkHistoryServer._restore_synchronizer_state(tick)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_process_delta = delta
|
||||
|
||||
|
||||
if _is_active() and not sync_to_physics:
|
||||
_loop()
|
||||
|
||||
@@ -592,9 +601,9 @@ func _handle_peer_disconnect(peer: int) -> void:
|
||||
@rpc("any_peer", "reliable", "call_local")
|
||||
func _submit_sync_success() -> void:
|
||||
var peer_id := multiplayer.get_remote_sender_id()
|
||||
|
||||
|
||||
_logger.trace("Received time sync success from #%s, synced peers: %s", [peer_id, _synced_peers.keys()])
|
||||
|
||||
|
||||
if not _synced_peers.has(peer_id):
|
||||
_synced_peers[peer_id] = true
|
||||
after_client_sync.emit(peer_id)
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
name="netfox"
|
||||
description="Shared internals for netfox addons"
|
||||
author="Tamas Galffy and contributors"
|
||||
version="1.35.3"
|
||||
version="1.40.2"
|
||||
script="netfox.gd"
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
extends _HistoryBuffer
|
||||
class_name _PropertyHistoryBuffer
|
||||
|
||||
func get_snapshot(tick: int) -> _PropertySnapshot:
|
||||
if _buffer.has(tick):
|
||||
return _buffer[tick]
|
||||
else:
|
||||
return _PropertySnapshot.new()
|
||||
|
||||
func set_snapshot(tick: int, data) -> void:
|
||||
if data is Dictionary:
|
||||
var snapshot := _PropertySnapshot.from_dictionary(data)
|
||||
super(tick, snapshot)
|
||||
elif data is _PropertySnapshot:
|
||||
super(tick, data)
|
||||
else:
|
||||
push_error("Data not a PropertSnapshot! %s" % [data])
|
||||
|
||||
func get_history(tick: int) -> _PropertySnapshot:
|
||||
var snapshot = super(tick)
|
||||
|
||||
return snapshot if snapshot else _PropertySnapshot.new()
|
||||
|
||||
func trim(earliest_tick_to_keep: int = NetworkRollback.history_start) -> void:
|
||||
super(earliest_tick_to_keep)
|
||||
|
||||
func merge(data: _PropertySnapshot, tick:int) -> void:
|
||||
set_snapshot(tick, get_snapshot(tick).merge(data))
|
||||
@@ -1,111 +0,0 @@
|
||||
extends RefCounted
|
||||
class_name _RollbackHistoryRecorder
|
||||
|
||||
# Provided externally by RBS
|
||||
var _state_history: _PropertyHistoryBuffer
|
||||
var _input_history: _PropertyHistoryBuffer
|
||||
|
||||
var _state_property_config: _PropertyConfig
|
||||
var _input_property_config: _PropertyConfig
|
||||
|
||||
var _property_cache: PropertyCache
|
||||
|
||||
var _latest_state_tick: int
|
||||
var _skipset: _Set
|
||||
|
||||
func configure(
|
||||
p_state_history: _PropertyHistoryBuffer, p_input_history: _PropertyHistoryBuffer,
|
||||
p_state_property_config: _PropertyConfig, p_input_property_config: _PropertyConfig,
|
||||
p_property_cache: PropertyCache,
|
||||
p_skipset: _Set
|
||||
) -> void:
|
||||
_state_history = p_state_history
|
||||
_input_history = p_input_history
|
||||
_state_property_config = p_state_property_config
|
||||
_input_property_config = p_input_property_config
|
||||
_property_cache = p_property_cache
|
||||
_skipset = p_skipset
|
||||
|
||||
func set_latest_state_tick(p_latest_state_tick: int) -> void:
|
||||
_latest_state_tick = p_latest_state_tick
|
||||
|
||||
func apply_state(tick: int) -> void:
|
||||
# Apply state for tick
|
||||
var state = _state_history.get_history(tick)
|
||||
state.apply(_property_cache)
|
||||
|
||||
func apply_display_state() -> void:
|
||||
apply_state(NetworkRollback.display_tick)
|
||||
|
||||
func apply_tick(tick: int) -> void:
|
||||
var state := _state_history.get_history(tick)
|
||||
var input := _input_history.get_history(tick)
|
||||
|
||||
state.apply(_property_cache)
|
||||
input.apply(_property_cache)
|
||||
|
||||
func trim_history() -> void:
|
||||
# Trim history
|
||||
_state_history.trim()
|
||||
_input_history.trim()
|
||||
|
||||
func record_input(tick: int) -> void:
|
||||
# Record input
|
||||
if not _get_recorded_input_props().is_empty():
|
||||
var input = _PropertySnapshot.extract(_get_recorded_input_props())
|
||||
var input_tick: int = tick + NetworkRollback.input_delay
|
||||
_input_history.set_snapshot(input_tick, input)
|
||||
|
||||
func record_state(tick: int) -> void:
|
||||
# Record state for specified tick ( current + 1 )
|
||||
# Check if any of the managed nodes were mutated
|
||||
var is_mutated := _get_recorded_state_props().any(func(pe):
|
||||
return NetworkRollback.is_mutated(pe.node, tick - 1))
|
||||
|
||||
var record_state := _PropertySnapshot.extract(_get_state_props_to_record(tick))
|
||||
if record_state.size():
|
||||
var merge_state := _state_history.get_history(tick - 1)
|
||||
_state_history.set_snapshot(tick, merge_state.merge(record_state))
|
||||
|
||||
func _should_record_tick(tick: int) -> bool:
|
||||
if _get_recorded_state_props().is_empty():
|
||||
# Don't record tick if there's no props to record
|
||||
return false
|
||||
|
||||
if _get_recorded_state_props().any(func(pe):
|
||||
return NetworkRollback.is_mutated(pe.node, tick - 1)):
|
||||
# If there's any node that was mutated, there's something to record
|
||||
return true
|
||||
|
||||
# Otherwise, record only if we don't have authoritative state for the tick
|
||||
return tick > _latest_state_tick
|
||||
|
||||
func _get_state_props_to_record(tick: int) -> Array[PropertyEntry]:
|
||||
if not _should_record_tick(tick):
|
||||
return []
|
||||
if _skipset.is_empty():
|
||||
return _get_recorded_state_props()
|
||||
|
||||
return _get_recorded_state_props().filter(func(pe): return _should_record_property(pe, tick))
|
||||
|
||||
func _should_record_property(property_entry: PropertyEntry, tick: int) -> bool:
|
||||
if NetworkRollback.is_mutated(property_entry.node, tick - 1):
|
||||
return true
|
||||
if _skipset.has(property_entry.node):
|
||||
return false
|
||||
return true
|
||||
|
||||
# =============================================================================
|
||||
# Shared utils, extract later
|
||||
|
||||
func _get_recorded_state_props() -> Array[PropertyEntry]:
|
||||
return _state_property_config.get_properties()
|
||||
|
||||
func _get_owned_state_props() -> Array[PropertyEntry]:
|
||||
return _state_property_config.get_owned_properties()
|
||||
|
||||
func _get_recorded_input_props() -> Array[PropertyEntry]:
|
||||
return _input_property_config.get_owned_properties()
|
||||
|
||||
func _get_owned_input_props() -> Array[PropertyEntry]:
|
||||
return _input_property_config.get_owned_properties()
|
||||
@@ -1 +0,0 @@
|
||||
uid://bn6fsqxbfhihk
|
||||
@@ -1,282 +0,0 @@
|
||||
extends Node
|
||||
class_name _RollbackHistoryTransmitter
|
||||
|
||||
var root: Node
|
||||
var enable_input_broadcast: bool = true
|
||||
var full_state_interval: int
|
||||
var diff_ack_interval: int
|
||||
|
||||
# Provided externally by RBS
|
||||
var _state_history: _PropertyHistoryBuffer
|
||||
var _input_history: _PropertyHistoryBuffer
|
||||
var _visibility_filter: PeerVisibilityFilter
|
||||
|
||||
var _state_property_config: _PropertyConfig
|
||||
var _input_property_config: _PropertyConfig
|
||||
|
||||
var _property_cache: PropertyCache
|
||||
var _skipset: _Set
|
||||
|
||||
# Collaborators
|
||||
var _input_encoder: _RedundantHistoryEncoder
|
||||
var _full_state_encoder: _SnapshotHistoryEncoder
|
||||
var _diff_state_encoder: _DiffHistoryEncoder
|
||||
|
||||
# State
|
||||
var _ackd_state: Dictionary = {}
|
||||
var _next_full_state_tick: int
|
||||
var _next_diff_ack_tick: int
|
||||
|
||||
var _earliest_input_tick: int
|
||||
var _latest_state_tick: int
|
||||
|
||||
var _is_predicted_tick: bool
|
||||
var _is_initialized: bool
|
||||
|
||||
# Signals
|
||||
signal _on_transmit_state(state: Dictionary, tick: int)
|
||||
|
||||
static var _logger: NetfoxLogger = NetfoxLogger._for_netfox("RollbackHistoryTransmitter")
|
||||
|
||||
func get_earliest_input_tick() -> int:
|
||||
return _earliest_input_tick
|
||||
|
||||
func get_latest_state_tick() -> int:
|
||||
return _latest_state_tick
|
||||
|
||||
func set_predicted_tick(p_is_predicted_tick) -> void:
|
||||
_is_predicted_tick = p_is_predicted_tick
|
||||
|
||||
func sync_settings(p_root: Node, p_enable_input_broadcast: bool, p_full_state_interval: int, p_diff_ack_interval: int) -> void:
|
||||
root = p_root
|
||||
enable_input_broadcast = p_enable_input_broadcast
|
||||
full_state_interval = p_full_state_interval
|
||||
diff_ack_interval = p_diff_ack_interval
|
||||
|
||||
func configure(
|
||||
p_state_history: _PropertyHistoryBuffer, p_input_history: _PropertyHistoryBuffer,
|
||||
p_state_property_config: _PropertyConfig, p_input_property_config: _PropertyConfig,
|
||||
p_visibility_filter: PeerVisibilityFilter,
|
||||
p_property_cache: PropertyCache,
|
||||
p_skipset: _Set
|
||||
) -> void:
|
||||
_state_history = p_state_history
|
||||
_input_history = p_input_history
|
||||
_state_property_config = p_state_property_config
|
||||
_input_property_config = p_input_property_config
|
||||
_visibility_filter = p_visibility_filter
|
||||
_property_cache = p_property_cache
|
||||
_skipset = p_skipset
|
||||
|
||||
_input_encoder = _RedundantHistoryEncoder.new(_input_history, _property_cache)
|
||||
_full_state_encoder = _SnapshotHistoryEncoder.new(_state_history, _property_cache)
|
||||
_diff_state_encoder = _DiffHistoryEncoder.new(_state_history, _property_cache)
|
||||
|
||||
_is_initialized = true
|
||||
|
||||
reset()
|
||||
|
||||
func reset() -> void:
|
||||
_ackd_state.clear()
|
||||
_latest_state_tick = NetworkTime.tick - 1
|
||||
_earliest_input_tick = NetworkTime.tick
|
||||
_next_full_state_tick = NetworkTime.tick
|
||||
_next_diff_ack_tick = NetworkTime.tick
|
||||
|
||||
# Scatter full state sends, so not all nodes send at the same tick
|
||||
if is_inside_tree():
|
||||
_next_full_state_tick += hash(root.get_path()) % maxi(1, full_state_interval)
|
||||
_next_diff_ack_tick += hash(root.get_path()) % maxi(1, diff_ack_interval)
|
||||
else:
|
||||
_next_full_state_tick += hash(root.name) % maxi(1, full_state_interval)
|
||||
_next_diff_ack_tick += hash(root.name) % maxi(1, diff_ack_interval)
|
||||
|
||||
_diff_state_encoder.add_properties(_state_property_config.get_properties())
|
||||
_full_state_encoder.set_properties(_get_owned_state_props())
|
||||
_input_encoder.set_properties(_get_owned_input_props())
|
||||
|
||||
func conclude_tick_loop() -> void:
|
||||
_earliest_input_tick = NetworkTime.tick
|
||||
|
||||
func transmit_input(tick: int) -> void:
|
||||
if not _get_owned_input_props().is_empty():
|
||||
var input_tick: int = tick + NetworkRollback.input_delay
|
||||
var input_data := _input_encoder.encode(input_tick, _get_owned_input_props())
|
||||
var state_owning_peer := root.get_multiplayer_authority()
|
||||
NetworkRollback.register_input_submission(root, tick)
|
||||
|
||||
if enable_input_broadcast:
|
||||
for peer in _visibility_filter.get_rpc_target_peers():
|
||||
_submit_input.rpc_id(peer, input_tick, input_data)
|
||||
elif state_owning_peer != multiplayer.get_unique_id():
|
||||
_submit_input.rpc_id(state_owning_peer, input_tick, input_data)
|
||||
|
||||
func transmit_state(tick: int) -> void:
|
||||
if _get_owned_state_props().is_empty():
|
||||
# We don't own state, don't transmit anything
|
||||
return
|
||||
|
||||
if _is_predicted_tick and not _input_property_config.get_properties().is_empty():
|
||||
# Don't transmit anything if we're predicting
|
||||
# EXCEPT when we're running inputless
|
||||
return
|
||||
|
||||
# Include properties we own
|
||||
var full_state := _PropertySnapshot.new()
|
||||
|
||||
for property in _get_owned_state_props():
|
||||
if _should_broadcast(property, tick):
|
||||
full_state.set_value(property.to_string(), property.get_value())
|
||||
|
||||
_on_transmit_state.emit(full_state, tick)
|
||||
|
||||
# No properties to send?
|
||||
if full_state.is_empty():
|
||||
return
|
||||
|
||||
_latest_state_tick = max(_latest_state_tick, tick)
|
||||
|
||||
var is_sending_diffs := NetworkRollback.enable_diff_states
|
||||
var is_full_state_tick := not is_sending_diffs or (full_state_interval > 0 and tick > _next_full_state_tick)
|
||||
|
||||
if is_full_state_tick:
|
||||
# Broadcast new full state
|
||||
for peer in _visibility_filter.get_rpc_target_peers():
|
||||
_send_full_state(tick, peer)
|
||||
|
||||
# Adjust next full state if sending diffs
|
||||
if is_sending_diffs:
|
||||
_next_full_state_tick = tick + full_state_interval
|
||||
else:
|
||||
# Send diffs to each peer
|
||||
for peer in _visibility_filter.get_visible_peers():
|
||||
var reference_tick := _ackd_state.get(peer, -1) as int
|
||||
if reference_tick < 0 or not _state_history.has(reference_tick):
|
||||
# Peer hasn't ack'd any tick, or we don't have the ack'd tick
|
||||
# Send full state
|
||||
_send_full_state(tick, peer)
|
||||
continue
|
||||
|
||||
# Prepare diff
|
||||
var diff_state_data := _diff_state_encoder.encode(tick, reference_tick, _get_owned_state_props())
|
||||
|
||||
if _diff_state_encoder.get_full_snapshot().size() == _diff_state_encoder.get_encoded_snapshot().size():
|
||||
# State is completely different, send full state
|
||||
_send_full_state(tick, peer)
|
||||
else:
|
||||
# Send only diff
|
||||
_submit_diff_state.rpc_id(peer, diff_state_data, tick, reference_tick)
|
||||
|
||||
# Push metrics
|
||||
NetworkPerformance.push_full_state(_diff_state_encoder.get_full_snapshot())
|
||||
NetworkPerformance.push_sent_state(_diff_state_encoder.get_encoded_snapshot())
|
||||
|
||||
func _should_broadcast(property: PropertyEntry, tick: int) -> bool:
|
||||
# Only broadcast if we've simulated the node
|
||||
# NOTE: _can_simulate checks mutations, but to override _skipset
|
||||
# we check first
|
||||
if NetworkRollback.is_mutated(property.node, tick - 1):
|
||||
return true
|
||||
if _skipset.has(property.node):
|
||||
return false
|
||||
if NetworkRollback.is_rollback_aware(property.node):
|
||||
return NetworkRollback.is_simulated(property.node)
|
||||
|
||||
# Node is not rollback-aware, broadcast updates only if we own it
|
||||
return property.node.is_multiplayer_authority()
|
||||
|
||||
func _send_full_state(tick: int, peer: int = 0) -> void:
|
||||
var full_state_snapshot := _state_history.get_snapshot(tick).as_dictionary()
|
||||
var full_state_data := _full_state_encoder.encode(tick, _get_owned_state_props())
|
||||
|
||||
_submit_full_state.rpc_id(peer, full_state_data, tick)
|
||||
|
||||
if peer <= 0:
|
||||
NetworkPerformance.push_full_state_broadcast(full_state_snapshot)
|
||||
NetworkPerformance.push_sent_state_broadcast(full_state_snapshot)
|
||||
else:
|
||||
NetworkPerformance.push_full_state(full_state_snapshot)
|
||||
NetworkPerformance.push_sent_state(full_state_snapshot)
|
||||
|
||||
func _notification(what):
|
||||
if what == NOTIFICATION_PREDELETE:
|
||||
NetworkRollback.free_input_submission_data_for(root)
|
||||
|
||||
@rpc("any_peer", "unreliable", "call_remote")
|
||||
func _submit_input(tick: int, data: Array) -> void:
|
||||
if not _is_initialized:
|
||||
# Settings not processed yet
|
||||
return
|
||||
|
||||
var sender := multiplayer.get_remote_sender_id()
|
||||
var snapshots := _input_encoder.decode(data, _input_property_config.get_properties_owned_by(sender))
|
||||
var earliest_received_input = _input_encoder.apply(tick, snapshots, sender)
|
||||
if earliest_received_input >= 0:
|
||||
_earliest_input_tick = mini(_earliest_input_tick, earliest_received_input)
|
||||
NetworkRollback.register_input_submission(root, tick)
|
||||
|
||||
# `serialized_state` is a serialized _PropertySnapshot
|
||||
@rpc("any_peer", "unreliable_ordered", "call_remote")
|
||||
func _submit_full_state(data: Array, tick: int) -> void:
|
||||
if not _is_initialized:
|
||||
# Settings not processed yet
|
||||
return
|
||||
|
||||
var sender := multiplayer.get_remote_sender_id()
|
||||
var snapshot := _full_state_encoder.decode(data, _state_property_config.get_properties_owned_by(sender))
|
||||
if not _full_state_encoder.apply(tick, snapshot, sender):
|
||||
# Invalid data
|
||||
return
|
||||
|
||||
_latest_state_tick = tick
|
||||
if NetworkRollback.enable_diff_states:
|
||||
_ack_full_state.rpc_id(sender, tick)
|
||||
|
||||
# State is a serialized _PropertySnapshot (Dictionary[String, Variant])
|
||||
@rpc("any_peer", "unreliable_ordered", "call_remote")
|
||||
func _submit_diff_state(data: PackedByteArray, tick: int, reference_tick: int) -> void:
|
||||
if not _is_initialized:
|
||||
# Settings not processed yet
|
||||
return
|
||||
|
||||
var sender = multiplayer.get_remote_sender_id()
|
||||
var diff_snapshot := _diff_state_encoder.decode(data, _state_property_config.get_properties_owned_by(sender))
|
||||
if not _diff_state_encoder.apply(tick, diff_snapshot, reference_tick, sender):
|
||||
# Invalid data
|
||||
return
|
||||
|
||||
_latest_state_tick = tick
|
||||
|
||||
if NetworkRollback.enable_diff_states:
|
||||
if diff_ack_interval > 0 and tick > _next_diff_ack_tick:
|
||||
_ack_diff_state.rpc_id(sender, tick)
|
||||
_next_diff_ack_tick = tick + diff_ack_interval
|
||||
|
||||
@rpc("any_peer", "reliable", "call_remote")
|
||||
func _ack_full_state(tick: int) -> void:
|
||||
var sender_id := multiplayer.get_remote_sender_id()
|
||||
_ackd_state[sender_id] = tick
|
||||
|
||||
_logger.trace("Peer %d ack'd full state for tick %d", [sender_id, tick])
|
||||
|
||||
@rpc("any_peer", "unreliable_ordered", "call_remote")
|
||||
func _ack_diff_state(tick: int) -> void:
|
||||
var sender_id := multiplayer.get_remote_sender_id()
|
||||
_ackd_state[sender_id] = tick
|
||||
|
||||
_logger.trace("Peer %d ack'd diff state for tick %d", [sender_id, tick])
|
||||
|
||||
# =============================================================================
|
||||
# Shared utils, extract later
|
||||
|
||||
func _get_recorded_state_props() -> Array[PropertyEntry]:
|
||||
return _state_property_config.get_properties()
|
||||
|
||||
func _get_owned_state_props() -> Array[PropertyEntry]:
|
||||
return _state_property_config.get_owned_properties()
|
||||
|
||||
func _get_recorded_input_props() -> Array[PropertyEntry]:
|
||||
return _input_property_config.get_owned_properties()
|
||||
|
||||
func _get_owned_input_props() -> Array[PropertyEntry]:
|
||||
return _input_property_config.get_owned_properties()
|
||||
@@ -1 +0,0 @@
|
||||
uid://ofadsxmvd3e3
|
||||
@@ -0,0 +1 @@
|
||||
uid://bc61m7m28c1k7
|
||||
@@ -85,7 +85,6 @@ var display_tick: int:
|
||||
## with input latency higher than network latency.
|
||||
## [br][br]
|
||||
## [i]read-only[/i], you can change this in the project settings
|
||||
|
||||
var input_delay: int:
|
||||
get:
|
||||
return _input_delay
|
||||
@@ -100,7 +99,6 @@ var input_delay: int:
|
||||
## in transmission, the next (n-1) packets will contain the data for it.
|
||||
## [br][br]
|
||||
## [i]read-only[/i], you can change this in the project settings
|
||||
|
||||
var input_redundancy: int:
|
||||
get:
|
||||
return max(1, _input_redundancy)
|
||||
@@ -168,7 +166,9 @@ var _rollback_stage: String = ""
|
||||
var _is_rollback: bool = false
|
||||
var _simulated_nodes: _Set = _Set.new()
|
||||
var _mutated_nodes: Dictionary = {}
|
||||
var _input_submissions: Dictionary = {}
|
||||
|
||||
var _earliest_input := -1
|
||||
var _earliest_state := -1
|
||||
|
||||
const _STAGE_BEFORE := "B"
|
||||
const _STAGE_PREPARE := "P"
|
||||
@@ -268,33 +268,44 @@ func is_just_mutated(target: Object, p_tick: int = tick) -> bool:
|
||||
return false
|
||||
|
||||
## Register that a node has submitted its input for a specific tick
|
||||
func register_input_submission(root_node: Node, tick: int) -> void:
|
||||
if not _input_submissions.has(root_node):
|
||||
_input_submissions[root_node] = tick
|
||||
else:
|
||||
_input_submissions[root_node] = maxi(_input_submissions[root_node], tick)
|
||||
## @deprecated
|
||||
func register_rollback_input_submission(_node: Node, _tick: int) -> void:
|
||||
pass
|
||||
|
||||
## Get the latest input tick submitted by a specific root node
|
||||
## Get the latest input tick submitted for a specific node
|
||||
## [br][br]
|
||||
## Returns [code]-1[/code] if no input was submitted for the node, ever.
|
||||
func get_latest_input_tick(root_node: Node) -> int:
|
||||
if _input_submissions.has(root_node):
|
||||
return _input_submissions[root_node]
|
||||
return -1
|
||||
func get_latest_input_tick(node: Node) -> int:
|
||||
var input_nodes := RollbackSimulationServer._get_inputs_of(node)
|
||||
var reference_tick := NetworkTime.tick
|
||||
|
||||
return NetworkHistoryServer.get_latest_input_for(input_nodes, reference_tick)
|
||||
|
||||
## Check if a node has submitted input for a specific tick (or later)
|
||||
func has_input_for_tick(root_node: Node, tick: int) -> bool:
|
||||
return _input_submissions.has(root_node) and _input_submissions[root_node] >= tick
|
||||
func has_input_for_tick(node: Node, tick: int) -> bool:
|
||||
var latest_input := get_latest_input_tick(node)
|
||||
return latest_input != -1 and latest_input >= tick
|
||||
|
||||
## Free all input submission data for a node
|
||||
## [br][br]
|
||||
## Use this once the node is freed.
|
||||
func free_input_submission_data_for(root_node: Node) -> void:
|
||||
_input_submissions.erase(root_node)
|
||||
## @deprecated
|
||||
func free_input_submission_data_for(_node: Node) -> void:
|
||||
pass
|
||||
|
||||
func _get_rollback_method(object: Object) -> Callable:
|
||||
return object._rollback_tick
|
||||
|
||||
func _ready():
|
||||
NetfoxLogger.register_tag(_get_rollback_tag)
|
||||
NetworkTime.after_tick_loop.connect(_rollback)
|
||||
NetworkTime.after_tick.connect(func(_dt, tick):
|
||||
NetworkHistoryServer._record_rollback_input(tick + input_delay)
|
||||
NetworkSynchronizationServer._synchronize_input(tick + input_delay)
|
||||
)
|
||||
|
||||
NetworkSynchronizationServer._on_input.connect(_handle_input)
|
||||
NetworkSynchronizationServer._on_state.connect(_handle_state)
|
||||
|
||||
func _exit_tree():
|
||||
NetfoxLogger.free_tag(_get_rollback_tag)
|
||||
@@ -313,6 +324,17 @@ func _rollback() -> void:
|
||||
_resim_from = NetworkTime.tick
|
||||
before_loop.emit()
|
||||
|
||||
# Figure out where to start rollback from
|
||||
var range_source = "notif"
|
||||
if _earliest_input >= 0 and _earliest_input <= _resim_from:
|
||||
range_source = "earliest input"
|
||||
_resim_from = _earliest_input
|
||||
if _earliest_state >= 0 and _earliest_state <= _resim_from:
|
||||
range_source = "latest state"
|
||||
_resim_from = _earliest_state
|
||||
_resim_from = mini(_resim_from, NetworkTime.tick - 1)
|
||||
_logger.trace("Simulating range @%d>@%d using %s", [_resim_from, NetworkTime.tick, range_source])
|
||||
|
||||
# Only set _is_rollback *after* emitting before_loop
|
||||
_is_rollback = true
|
||||
_rollback_stage = _STAGE_BEFORE
|
||||
@@ -331,6 +353,9 @@ func _rollback() -> void:
|
||||
)
|
||||
from = NetworkTime.tick - history_limit
|
||||
|
||||
_earliest_input = -1
|
||||
_earliest_state = -1
|
||||
|
||||
# for tick in from .. to:
|
||||
_rollback_from = from
|
||||
_rollback_to = to
|
||||
@@ -343,6 +368,8 @@ func _rollback() -> void:
|
||||
# Restore input and state for tick
|
||||
_rollback_stage = _STAGE_PREPARE
|
||||
on_prepare_tick.emit(tick)
|
||||
NetworkHistoryServer._restore_rollback_input(tick)
|
||||
NetworkHistoryServer._restore_rollback_state(tick)
|
||||
after_prepare_tick.emit(tick)
|
||||
|
||||
# Simulate rollback tick
|
||||
@@ -353,20 +380,43 @@ func _rollback() -> void:
|
||||
# If not: Latest input >= tick >= Earliest input
|
||||
_rollback_stage = _STAGE_SIMULATE
|
||||
on_process_tick.emit(tick)
|
||||
RollbackSimulationServer.simulate(NetworkTime.ticktime, tick)
|
||||
after_process_tick.emit(tick)
|
||||
|
||||
# Record state for tick + 1
|
||||
_rollback_stage = _STAGE_RECORD
|
||||
on_record_tick.emit(tick + 1)
|
||||
NetworkHistoryServer._record_rollback_state(tick + 1)
|
||||
NetworkSynchronizationServer._synchronize_state(tick + 1)
|
||||
|
||||
# Restore display state
|
||||
_rollback_stage = _STAGE_AFTER
|
||||
after_loop.emit()
|
||||
NetworkHistoryServer._restore_rollback_state(display_tick)
|
||||
RollbackSimulationServer._trim_ticks_simulated(history_start)
|
||||
|
||||
# Cleanup
|
||||
_mutated_nodes.clear()
|
||||
_is_rollback = false
|
||||
|
||||
func _handle_input(snapshot: _Snapshot):
|
||||
if snapshot.is_empty():
|
||||
return
|
||||
if _earliest_input < 0 or snapshot.tick < _earliest_input:
|
||||
_logger.trace("Ingested input @%d, earliest @%d->@%d", [snapshot.tick, _earliest_input, snapshot.tick])
|
||||
_earliest_input = snapshot.tick
|
||||
else:
|
||||
_logger.trace("Ingested input @%d, earliest @%d->@%d", [snapshot.tick, _earliest_input, _earliest_input])
|
||||
|
||||
func _handle_state(snapshot: _Snapshot):
|
||||
if snapshot.is_empty():
|
||||
return
|
||||
if _earliest_state < 0 or snapshot.tick < _earliest_state:
|
||||
_logger.trace("Ingested state @%d, latest @%d->@%d", [snapshot.tick, _earliest_state, snapshot.tick])
|
||||
_earliest_state = snapshot.tick
|
||||
else:
|
||||
_logger.trace("Ingested state @%d, latest @%d->@%d", [snapshot.tick, _earliest_state, _earliest_state])
|
||||
|
||||
# Insight 1:
|
||||
# state(x) = simulate(state(x - 1), input(x - 1))
|
||||
# state(x + 1) = simulate(state(x), input(x))
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
@tool
|
||||
extends Node
|
||||
class_name PredictiveSynchronizer
|
||||
|
||||
## Similar to [RollbackSynchronizer], this class manages local variables in a
|
||||
## rollback context for predictive simulation without networking.
|
||||
##
|
||||
## This is a simplified version that focuses on local state management.
|
||||
## [br][br]
|
||||
## Like [RollbackSynchronizer], it automatically discovers nodes
|
||||
## with a [code]_rollback_tick(delta: float, tick: int)[/code]
|
||||
## method and calls them during the prediction phase.
|
||||
|
||||
## The root node for resolving node paths in properties. Defaults to the parent
|
||||
## node.
|
||||
@export var root: Node = get_parent()
|
||||
|
||||
@export_group("State")
|
||||
## Properties that define the game state.
|
||||
## [br][br]
|
||||
## State properties are recorded for each tick and restored during rollback.
|
||||
## State is restored before every rollback tick, and recorded after simulating
|
||||
## the tick.
|
||||
@export var state_properties: Array[String]
|
||||
|
||||
var _state_properties := _PropertyPool.new()
|
||||
var _sim_nodes: Array[Node] = []
|
||||
|
||||
var _properties_dirty: bool = false
|
||||
|
||||
## Process settings.
|
||||
##
|
||||
## Call this after any change to configuration.
|
||||
func process_settings() -> void:
|
||||
# Deregister all nodes we've registered previously
|
||||
for subject in _state_properties.get_subjects():
|
||||
NetworkHistoryServer.deregister(subject)
|
||||
|
||||
for node in _sim_nodes:
|
||||
RollbackSimulationServer.deregister_node(node)
|
||||
|
||||
# Gather all prediction-aware nodes to call during prediction ticks
|
||||
_sim_nodes = root.find_children("*")
|
||||
_sim_nodes.push_front(root)
|
||||
_sim_nodes = _sim_nodes.filter(func(it): return NetworkRollback.is_rollback_aware(it))
|
||||
_sim_nodes.erase(self)
|
||||
|
||||
# Keep history of state properties
|
||||
_state_properties.set_from_paths(root, state_properties)
|
||||
for subject in _state_properties.get_subjects():
|
||||
for property in _state_properties.get_properties_of(subject):
|
||||
NetworkHistoryServer.register_rollback_state(subject, property)
|
||||
|
||||
# Simulated notes to participate in rollback
|
||||
for node in _sim_nodes:
|
||||
RollbackSimulationServer.register(NetworkRollback._get_rollback_method(node))
|
||||
|
||||
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
|
||||
|
||||
process_settings.call_deferred()
|
||||
|
||||
func _enter_tree() -> void:
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
|
||||
if not NetworkTime.is_initial_sync_done():
|
||||
# Wait for time sync to complete
|
||||
await NetworkTime.after_sync
|
||||
process_settings.call_deferred()
|
||||
|
||||
func _reprocess_settings() -> void:
|
||||
if not _properties_dirty or Engine.is_editor_hint():
|
||||
return
|
||||
|
||||
_properties_dirty = false
|
||||
process_settings()
|
||||
|
||||
## Add a state 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_state(node: Variant, property: String):
|
||||
var property_path := PropertyEntry.make_path(root, node, property)
|
||||
if not property_path or state_properties.has(property_path):
|
||||
return
|
||||
|
||||
state_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()
|
||||
if what == NOTIFICATION_PREDELETE:
|
||||
for node in _sim_nodes:
|
||||
RollbackSimulationServer.deregister_node(node)
|
||||
for subject in _state_properties.get_subjects():
|
||||
NetworkHistoryServer.deregister(subject)
|
||||
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
if not root:
|
||||
root = get_parent()
|
||||
|
||||
# Explore state properties
|
||||
if not root:
|
||||
return ["No valid root node found!"]
|
||||
|
||||
var result := PackedStringArray()
|
||||
result.append_array(_NetfoxEditorUtils.gather_properties(root, "_get_rollback_state_properties",
|
||||
func(node, prop):
|
||||
add_state(node, prop)
|
||||
))
|
||||
|
||||
return result
|
||||
@@ -0,0 +1 @@
|
||||
uid://ct51w7rsjcxvw
|
||||
@@ -1,34 +0,0 @@
|
||||
extends RefCounted
|
||||
class_name RollbackFreshnessStore
|
||||
|
||||
## This class tracks nodes and whether they have processed any given tick during
|
||||
## a rollback.
|
||||
|
||||
# TODO: _Set
|
||||
var _data: Dictionary = {}
|
||||
|
||||
func is_fresh(node: Node, tick: int) -> bool:
|
||||
if not _data.has(tick):
|
||||
return true
|
||||
|
||||
if not _data[tick].has(node):
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
func notify_processed(node: Node, tick: int) -> void:
|
||||
if not _data.has(tick):
|
||||
_data[tick] = {}
|
||||
|
||||
_data[tick][node] = true
|
||||
|
||||
func trim() -> void:
|
||||
while not _data.is_empty():
|
||||
var earliest_tick := _data.keys().min()
|
||||
if earliest_tick < NetworkRollback.history_start:
|
||||
_data.erase(earliest_tick)
|
||||
else:
|
||||
break
|
||||
|
||||
func clear():
|
||||
_data.clear()
|
||||
@@ -32,23 +32,11 @@ class_name RollbackSynchronizer
|
||||
## for every tick.
|
||||
## [br][br]
|
||||
## Only considered if [member _NetworkRollback.enable_diff_states] is true.
|
||||
## @deprecated: This can now be configured in the project settings.
|
||||
@export_range(0, 128, 1, "or_greater")
|
||||
var full_state_interval: int = 24
|
||||
|
||||
## Ticks to wait between unreliably acknowledging diff states.
|
||||
## [br][br]
|
||||
## This can reduce the amount of properties sent in diff states, due to clients
|
||||
## more often acknowledging received states. To avoid introducing hickups, these
|
||||
## are sent unreliably.
|
||||
## [br][br]
|
||||
## If set to 0, diff states will never be acknowledged. If set to 1, all diff
|
||||
## states will be acknowledged. If set higher, ack's will be sent regularly, but
|
||||
## not for every diff state.
|
||||
## [br][br]
|
||||
## If enabled, it's worth to tune this setting until network traffic is actually
|
||||
## reduced.
|
||||
## [br][br]
|
||||
## Only considered if [member _NetworkRollback.enable_diff_states] is true.
|
||||
## @deprecated: This is no longer used.
|
||||
@export_range(0, 128, 1, "or_greater")
|
||||
var diff_ack_interval: int = 0
|
||||
|
||||
@@ -62,74 +50,95 @@ var diff_ack_interval: int = 0
|
||||
## This will broadcast input to all peers, turning this off will limit to
|
||||
## sending it to the server only. Turning this off is recommended to save
|
||||
## bandwidth and reduce cheating risks.
|
||||
## @deprecated: This can now be configured in the project settings.
|
||||
@export var enable_input_broadcast: bool = true
|
||||
|
||||
# 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 _state_property_config: _PropertyConfig = _PropertyConfig.new()
|
||||
var _input_property_config: _PropertyConfig = _PropertyConfig.new()
|
||||
var _nodes: Array[Node] = []
|
||||
|
||||
var _simset: _Set = _Set.new()
|
||||
var _skipset: _Set = _Set.new()
|
||||
var _state_properties := _PropertyPool.new()
|
||||
var _input_properties := _PropertyPool.new()
|
||||
var _sim_nodes := [] as Array[Node]
|
||||
var _schema_nodes := _Set.new()
|
||||
|
||||
var _properties_dirty: bool = false
|
||||
|
||||
var _property_cache := PropertyCache.new(root)
|
||||
var _freshness_store := RollbackFreshnessStore.new()
|
||||
|
||||
var _states := _PropertyHistoryBuffer.new()
|
||||
var _inputs := _PropertyHistoryBuffer.new()
|
||||
var _last_simulated_tick: int
|
||||
|
||||
var _has_input: bool
|
||||
var _input_tick: int
|
||||
var _is_predicted_tick: bool
|
||||
|
||||
static var _logger: NetfoxLogger = NetfoxLogger._for_netfox("RollbackSynchronizer")
|
||||
|
||||
# Composition
|
||||
var _history_transmitter: _RollbackHistoryTransmitter
|
||||
var _history_recorder: _RollbackHistoryRecorder
|
||||
|
||||
## Process settings.
|
||||
##
|
||||
## [br][br]
|
||||
## Call this after any change to configuration. Updates based on authority too
|
||||
## ( calls process_authority ).
|
||||
func process_settings() -> void:
|
||||
# Deregister simulated, state and input nodes
|
||||
for node in _sim_nodes + _state_properties.get_subjects() + _input_properties.get_subjects():
|
||||
RollbackSimulationServer.deregister_node(node)
|
||||
_sim_nodes.clear()
|
||||
|
||||
# Clear
|
||||
_property_cache.root = root
|
||||
_property_cache.clear()
|
||||
_freshness_store.clear()
|
||||
|
||||
_nodes.clear()
|
||||
|
||||
_states.clear()
|
||||
_inputs.clear()
|
||||
process_authority()
|
||||
|
||||
# Gather all rollback-aware nodes to simulate during rollbacks
|
||||
_nodes = root.find_children("*")
|
||||
_nodes.push_front(root)
|
||||
_nodes = _nodes.filter(func(it): return NetworkRollback.is_rollback_aware(it))
|
||||
_nodes.erase(self)
|
||||
var nodes := root.find_children("*") as Array[Node]
|
||||
nodes.push_front(root)
|
||||
nodes = nodes.filter(func(it): return NetworkRollback.is_rollback_aware(it))
|
||||
nodes.erase(self)
|
||||
|
||||
_history_transmitter.sync_settings(root, enable_input_broadcast, full_state_interval, diff_ack_interval)
|
||||
_history_transmitter.configure(_states, _inputs, _state_property_config, _input_property_config, visibility_filter, _property_cache, _skipset)
|
||||
_history_recorder.configure(_states, _inputs, _state_property_config, _input_property_config, _property_cache, _skipset)
|
||||
# Register simulation callbacks
|
||||
for node in nodes:
|
||||
RollbackSimulationServer.register(NetworkRollback._get_rollback_method(node))
|
||||
_sim_nodes.append(node)
|
||||
|
||||
# Both simulated and state nodes depend on all inputs
|
||||
# TODO(#564): Write tests for setups where a node is synchronized but not simulated
|
||||
for node in nodes + _state_properties.get_subjects():
|
||||
for input_node in _input_properties.get_subjects():
|
||||
RollbackSimulationServer.register_rollback_input_for(node, input_node)
|
||||
|
||||
# Register identifiers
|
||||
for node in _state_properties.get_subjects() + _input_properties.get_subjects():
|
||||
NetworkIdentityServer.register_node(node)
|
||||
|
||||
# Register visibility filter
|
||||
for node in _state_properties.get_subjects():
|
||||
NetworkSynchronizationServer.register_visibility_filter(node, visibility_filter)
|
||||
|
||||
## Process settings based on authority.
|
||||
##
|
||||
## [br][br]
|
||||
## Call this whenever the authority of any of the nodes managed by
|
||||
## RollbackSynchronizer changes. Make sure to do this at the same time on all
|
||||
## peers.
|
||||
func process_authority():
|
||||
_state_property_config.local_peer_id = multiplayer.get_unique_id()
|
||||
_input_property_config.local_peer_id = multiplayer.get_unique_id()
|
||||
# Deregister all recorded properties
|
||||
for node in _state_properties.get_subjects():
|
||||
for property in _state_properties.get_properties_of(node):
|
||||
NetworkHistoryServer.deregister_rollback_state(node, property)
|
||||
NetworkSynchronizationServer.deregister_rollback_state(node, property)
|
||||
|
||||
_state_property_config.set_properties_from_paths(state_properties, _property_cache)
|
||||
_input_property_config.set_properties_from_paths(input_properties, _property_cache)
|
||||
for node in _input_properties.get_subjects():
|
||||
for property in _input_properties.get_properties_of(node):
|
||||
NetworkHistoryServer.deregister_rollback_input(node, property)
|
||||
NetworkSynchronizationServer.deregister_rollback_input(node, property)
|
||||
|
||||
# Process authority
|
||||
_state_properties.set_from_paths(root, state_properties)
|
||||
_input_properties.set_from_paths(root, input_properties)
|
||||
|
||||
# Register new recorded properties
|
||||
for node in _state_properties.get_subjects():
|
||||
for property in _state_properties.get_properties_of(node):
|
||||
NetworkHistoryServer.register_rollback_state(node, property)
|
||||
NetworkSynchronizationServer.register_rollback_state(node, property)
|
||||
|
||||
for node in _input_properties.get_subjects():
|
||||
for property in _input_properties.get_properties_of(node):
|
||||
NetworkHistoryServer.register_rollback_input(node, property)
|
||||
NetworkSynchronizationServer.register_rollback_input(node, property)
|
||||
|
||||
## Add a state property.
|
||||
## [br][br]
|
||||
@@ -159,42 +168,77 @@ func add_input(node: Variant, property: String) -> void:
|
||||
_properties_dirty = true
|
||||
_reprocess_settings.call_deferred()
|
||||
|
||||
## Set the schema for transmitting properties over the network.
|
||||
## [br][br]
|
||||
## The [param schema] must be a dictionary, with the keys being property path
|
||||
## strings, and the values are the associated [NetworkSchemaSerializer] objects.
|
||||
## Properties are interpreted relative to the [member root] node. The schema can
|
||||
## contain both state and input properties. Properties not specified in the
|
||||
## schema will use a generic fallback serializer. By using the right serializer
|
||||
## for the right property, bandwidth usage can be lowered.
|
||||
## [br][br]
|
||||
## See [NetworkSchemas] for many common serializers.
|
||||
## [br][br]
|
||||
## Example:
|
||||
## [codeblock]
|
||||
## rollback_synchronizer.set_schema({
|
||||
## ":transform": NetworkSchemas.transform3f32(),
|
||||
## ":velocity": NetworkSchemas.vec3f32(),
|
||||
## "Input:movement": NetworkSchemas.vec3f32()
|
||||
## })
|
||||
## [/codeblock]
|
||||
func set_schema(schema: Dictionary) -> void:
|
||||
# Remove previous schema
|
||||
for node in _schema_nodes:
|
||||
NetworkSynchronizationServer.deregister_schema_for(node)
|
||||
_schema_nodes.clear()
|
||||
|
||||
# Register new schema
|
||||
for prop in schema:
|
||||
var prop_entry := PropertyEntry.parse(root, prop)
|
||||
var serializer := schema[prop] as NetworkSchemaSerializer
|
||||
NetworkSynchronizationServer.register_schema(prop_entry.node, prop_entry.property, serializer)
|
||||
_schema_nodes.add(prop_entry.node)
|
||||
|
||||
## Check if input is available for the current tick.
|
||||
##
|
||||
## [br][br]
|
||||
## This input is not always current, it may be from multiple ticks ago.
|
||||
## [br][br]
|
||||
## Returns true if input is available.
|
||||
func has_input() -> bool:
|
||||
return _has_input
|
||||
return get_input_age() >= 0
|
||||
|
||||
## Get the age of currently available input in ticks.
|
||||
##
|
||||
## [br][br]
|
||||
## The available input may be from the current tick, or from multiple ticks ago.
|
||||
## This number of tick is the input's age.
|
||||
## [br][br]
|
||||
## Calling this when [member has_input] is false will yield an error.
|
||||
func get_input_age() -> int:
|
||||
if has_input():
|
||||
return NetworkRollback.tick - _input_tick
|
||||
else:
|
||||
_logger.error("Trying to check input age without having input!")
|
||||
return -1
|
||||
return NetworkHistoryServer.get_input_age_for(_input_properties.get_subjects(), NetworkRollback.tick)
|
||||
|
||||
## Check if the current tick is predicted.
|
||||
##
|
||||
## [br][br]
|
||||
## A tick becomes predicted if there's no up-to-date input available. It will be
|
||||
## simulated and recorded, but will not be broadcast, nor considered
|
||||
## authoritative.
|
||||
func is_predicting() -> bool:
|
||||
return _is_predicted_tick
|
||||
if RollbackSimulationServer.get_simulated_object() != null:
|
||||
# An object is being simulated, check if it's predicted
|
||||
return RollbackSimulationServer.is_predicting_current()
|
||||
else:
|
||||
# We're outside of simulation, predicting if we don't have current input
|
||||
return get_input_age() != 0
|
||||
|
||||
## Ignore a node's prediction for the current rollback tick.
|
||||
##
|
||||
## [br][br]
|
||||
## Call this when the input is too old to base predictions on. This call is
|
||||
## ignored if [member enable_prediction] is false.
|
||||
func ignore_prediction(node: Node) -> void:
|
||||
if enable_prediction:
|
||||
_skipset.add(node)
|
||||
# Not needed, netfox records properties as non-auth if predicting
|
||||
# Once the data is received from the owner, it won't be overwritten by
|
||||
# predictions.
|
||||
#
|
||||
# This method may see some use again, otherwise it will be deprecated.
|
||||
pass
|
||||
|
||||
## Get the tick of the last known input.
|
||||
## [br][br]
|
||||
@@ -208,11 +252,7 @@ func ignore_prediction(node: Node) -> void:
|
||||
## [br][br]
|
||||
## Returns -1 if there's no known input.
|
||||
func get_last_known_input() -> int:
|
||||
# If we own input, it is updated regularly, this will be the current tick
|
||||
# If we don't own input, _inputs is only updated when input data is received
|
||||
if not _inputs.is_empty():
|
||||
return _inputs.keys().max()
|
||||
return -1
|
||||
return NetworkHistoryServer.get_latest_input_for(_input_properties.get_subjects(), NetworkTime.tick)
|
||||
|
||||
## Get the tick of the last known state.
|
||||
## [br][br]
|
||||
@@ -221,10 +261,7 @@ func get_last_known_input() -> int:
|
||||
## data may change as new input arrives. For peers that don't own state, this
|
||||
## will be the tick of the latest state received from the state owner.
|
||||
func get_last_known_state() -> int:
|
||||
# If we own state, this will be updated when recording and broadcasting
|
||||
# state, this will be the current tick
|
||||
# If we don't own state, this will be updated when state data is received
|
||||
return _history_transmitter.get_latest_state_tick()
|
||||
return NetworkHistoryServer.get_state_age_for(_state_properties.get_subjects(), NetworkTime.tick)
|
||||
|
||||
func _ready() -> void:
|
||||
if Engine.is_editor_hint():
|
||||
@@ -235,60 +272,17 @@ func _ready() -> void:
|
||||
await NetworkTime.after_sync
|
||||
|
||||
process_settings.call_deferred()
|
||||
|
||||
func _connect_signals() -> void:
|
||||
NetworkTime.before_tick.connect(_before_tick)
|
||||
NetworkTime.after_tick.connect(_after_tick)
|
||||
|
||||
NetworkRollback.on_prepare_tick.connect(_on_prepare_tick)
|
||||
NetworkRollback.on_process_tick.connect(_process_tick)
|
||||
NetworkRollback.on_record_tick.connect(_on_record_tick)
|
||||
|
||||
NetworkRollback.before_loop.connect(_before_rollback_loop)
|
||||
NetworkRollback.after_loop.connect(_after_rollback_loop)
|
||||
|
||||
func _disconnect_signals() -> void:
|
||||
NetworkTime.before_tick.disconnect(_before_tick)
|
||||
NetworkTime.after_tick.disconnect(_after_tick)
|
||||
|
||||
NetworkRollback.on_prepare_tick.disconnect(_on_prepare_tick)
|
||||
NetworkRollback.on_process_tick.disconnect(_process_tick)
|
||||
NetworkRollback.on_record_tick.disconnect(_on_record_tick)
|
||||
|
||||
NetworkRollback.before_loop.disconnect(_before_rollback_loop)
|
||||
NetworkRollback.after_loop.disconnect(_after_rollback_loop)
|
||||
|
||||
func _before_tick(_dt: float, tick: int) -> void:
|
||||
_history_recorder.apply_state(tick)
|
||||
|
||||
func _after_tick(_dt: float, tick: int) -> void:
|
||||
_history_recorder.record_input(tick)
|
||||
_history_transmitter.transmit_input(tick)
|
||||
_history_recorder.trim_history()
|
||||
_freshness_store.trim()
|
||||
|
||||
func _before_rollback_loop() -> void:
|
||||
_notify_resim()
|
||||
|
||||
func _on_prepare_tick(tick: int) -> void:
|
||||
_history_recorder.apply_tick(tick)
|
||||
_prepare_tick_process(tick)
|
||||
|
||||
func _process_tick(tick: int) -> void:
|
||||
_run_rollback_tick(tick)
|
||||
_push_simset_metrics()
|
||||
|
||||
func _on_record_tick(tick: int) -> void:
|
||||
_history_recorder.record_state(tick)
|
||||
_history_transmitter.transmit_state(tick)
|
||||
|
||||
func _after_rollback_loop() -> void:
|
||||
_history_recorder.apply_display_state()
|
||||
_history_transmitter.conclude_tick_loop()
|
||||
multiplayer.connected_to_server.connect(process_settings)
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
if what == NOTIFICATION_EDITOR_PRE_SAVE:
|
||||
update_configuration_warnings()
|
||||
elif what == NOTIFICATION_PREDELETE:
|
||||
for node in _sim_nodes + _state_properties.get_subjects() + _input_properties.get_subjects():
|
||||
RollbackSimulationServer.deregister_node(node)
|
||||
NetworkSynchronizationServer.deregister(node)
|
||||
NetworkIdentityServer.deregister_node(node)
|
||||
NetworkHistoryServer.deregister(node)
|
||||
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
if not root:
|
||||
@@ -321,131 +315,14 @@ func _enter_tree() -> void:
|
||||
if not visibility_filter.get_parent():
|
||||
add_child(visibility_filter)
|
||||
|
||||
if _history_transmitter == null:
|
||||
_history_transmitter = _RollbackHistoryTransmitter.new()
|
||||
add_child(_history_transmitter, true)
|
||||
_history_transmitter.set_multiplayer_authority(get_multiplayer_authority())
|
||||
|
||||
if _history_recorder == null:
|
||||
_history_recorder = _RollbackHistoryRecorder.new()
|
||||
|
||||
if not NetworkTime.is_initial_sync_done():
|
||||
# Wait for time sync to complete
|
||||
await NetworkTime.after_sync
|
||||
_connect_signals.call_deferred()
|
||||
process_settings.call_deferred()
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
|
||||
_disconnect_signals()
|
||||
|
||||
func _notify_resim() -> void:
|
||||
if _get_owned_input_props().is_empty():
|
||||
# We don't have any inputs we own, simulate from earliest we've received
|
||||
NetworkRollback.notify_resimulation_start(_history_transmitter.get_earliest_input_tick())
|
||||
else:
|
||||
# We own inputs, simulate from latest authorative state
|
||||
NetworkRollback.notify_resimulation_start(_history_transmitter.get_latest_state_tick())
|
||||
|
||||
func _prepare_tick_process(tick: int) -> void:
|
||||
_history_recorder.set_latest_state_tick(_history_transmitter._latest_state_tick)
|
||||
|
||||
# Save data for input prediction
|
||||
var retrieved_tick := _inputs.get_closest_tick(tick)
|
||||
|
||||
# These are used as input for input age ( i.e. do we even have input, and if so, how old? )
|
||||
_has_input = retrieved_tick != -1
|
||||
_input_tick = retrieved_tick
|
||||
|
||||
# Used to explicitly determine if this is a predicted tick
|
||||
# ( even if we could grab *some* input )
|
||||
_is_predicted_tick = _is_predicted_tick_for(null, tick)
|
||||
_history_transmitter.set_predicted_tick(_is_predicted_tick)
|
||||
|
||||
# Reset the set of simulated and ignored nodes
|
||||
_simset.clear()
|
||||
_skipset.clear()
|
||||
|
||||
# Gather nodes that can be simulated
|
||||
for node in _nodes:
|
||||
if _can_simulate(node, tick):
|
||||
NetworkRollback.notify_simulated(node)
|
||||
|
||||
func _can_simulate(node: Node, tick: int) -> bool:
|
||||
if not enable_prediction and _is_predicted_tick_for(node, tick):
|
||||
# Don't simulate if prediction is not allowed and tick is predicted
|
||||
return false
|
||||
if NetworkRollback.is_mutated(node, tick):
|
||||
# Mutated nodes are always resimulated
|
||||
return true
|
||||
if input_properties.is_empty():
|
||||
# If we're running inputless and own the node, simulate it if we haven't
|
||||
if node.is_multiplayer_authority():
|
||||
return tick > _last_simulated_tick
|
||||
# If we're running inputless and don't own the node, only run as prediction
|
||||
return enable_prediction
|
||||
if node.is_multiplayer_authority():
|
||||
# Simulate from earliest input
|
||||
# Don't simulate frames we don't have input for
|
||||
return tick >= _history_transmitter.get_earliest_input_tick()
|
||||
else:
|
||||
# Simulate ONLY if we have state from server
|
||||
# Simulate from latest authorative state - anything the server confirmed we don't rerun
|
||||
# Don't simulate frames we don't have input for
|
||||
return tick >= _history_transmitter.get_latest_state_tick()
|
||||
|
||||
# `node` can be set to null, in case we're not simulating a specific node
|
||||
func _is_predicted_tick_for(node: Node, tick: int) -> bool:
|
||||
if input_properties.is_empty() and node != null:
|
||||
# We're running without inputs
|
||||
# It's only predicted if we don't own the node
|
||||
return not node.is_multiplayer_authority()
|
||||
else:
|
||||
# We have input properties, it's only predicted if we don't have the input for the tick
|
||||
return not _inputs.has(tick)
|
||||
|
||||
func _run_rollback_tick(tick: int) -> void:
|
||||
# Simulate rollback tick
|
||||
# Method call on rewindables
|
||||
# Rollback synchronizers go through each node they manage
|
||||
# If current tick is in node's range, tick
|
||||
# If authority: Latest input >= tick >= Latest state
|
||||
# If not: Latest input >= tick >= Earliest input
|
||||
for node in _nodes:
|
||||
if not NetworkRollback.is_simulated(node):
|
||||
continue
|
||||
|
||||
var is_fresh := _freshness_store.is_fresh(node, tick)
|
||||
_is_predicted_tick = _is_predicted_tick_for(node, tick)
|
||||
NetworkRollback.process_rollback(node, NetworkTime.ticktime, tick, is_fresh)
|
||||
|
||||
if _skipset.has(node):
|
||||
continue
|
||||
|
||||
_freshness_store.notify_processed(node, tick)
|
||||
_simset.add(node)
|
||||
|
||||
func _push_simset_metrics():
|
||||
# Push metrics
|
||||
NetworkPerformance.push_rollback_nodes_simulated(_simset.size())
|
||||
|
||||
func _reprocess_settings() -> void:
|
||||
if not _properties_dirty or Engine.is_editor_hint():
|
||||
return
|
||||
|
||||
_properties_dirty = false
|
||||
process_settings()
|
||||
|
||||
func _get_recorded_state_props() -> Array[PropertyEntry]:
|
||||
return _state_property_config.get_properties()
|
||||
|
||||
func _get_owned_state_props() -> Array[PropertyEntry]:
|
||||
return _state_property_config.get_owned_properties()
|
||||
|
||||
func _get_recorded_input_props() -> Array[PropertyEntry]:
|
||||
return _input_property_config.get_owned_properties()
|
||||
|
||||
func _get_owned_input_props() -> Array[PropertyEntry]:
|
||||
return _input_property_config.get_owned_properties()
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
extends RefCounted
|
||||
class_name NetworkSchemaSerializer
|
||||
|
||||
## Base class for serializers, to use with [NetworkSchemas]
|
||||
##
|
||||
## Each serializer must be able to encode and decode values passed to it.
|
||||
## Data is stored in [StreamPeerBuffer] objects.
|
||||
## [br][br]
|
||||
## To implement a custom serializer, extend this class and pass an instance of
|
||||
## it in place of a NetworkSchemaSerializer, for example to [method
|
||||
## RollbackSynchronizer.set_schema].
|
||||
##
|
||||
## @tutorial(Network schemas): https://foxssake.github.io/netfox/latest/netfox/guides/network-schemas/
|
||||
|
||||
## Encode [param value] into [param buffer]
|
||||
func encode(value: Variant, buffer: StreamPeerBuffer) -> void:
|
||||
pass
|
||||
|
||||
## Decode a value from [param buffer] and return it
|
||||
func decode(buffer: StreamPeerBuffer) -> Variant:
|
||||
return null
|
||||
@@ -0,0 +1 @@
|
||||
uid://btmpightc21cy
|
||||
@@ -0,0 +1,36 @@
|
||||
extends RefCounted
|
||||
class_name _NetworkSchema
|
||||
|
||||
var _serializers := {} # subject to (property to NetworkSchemaSerializer)
|
||||
var _fallback: NetworkSchemaSerializer
|
||||
|
||||
func _init(fallback: NetworkSchemaSerializer = NetworkSchemas.variant()) -> void:
|
||||
_fallback = fallback
|
||||
|
||||
func add(subject: Object, property: NodePath, serializer: NetworkSchemaSerializer) -> void:
|
||||
if not _serializers.has(subject):
|
||||
_serializers[subject] = { property: serializer }
|
||||
else:
|
||||
_serializers[subject][property] = serializer
|
||||
|
||||
func erase(subject: Object, property: NodePath) -> void:
|
||||
if not _serializers.has(subject):
|
||||
return
|
||||
|
||||
var subject_schema := _serializers[subject] as Dictionary
|
||||
subject_schema.erase(property)
|
||||
|
||||
if subject_schema.is_empty():
|
||||
_serializers.erase(subject)
|
||||
|
||||
func erase_subject(subject: Object) -> void:
|
||||
_serializers.erase(subject)
|
||||
|
||||
func encode(subject: Object, property: NodePath, value: Variant, buffer: StreamPeerBuffer) -> void:
|
||||
_get_serializer(subject, property).encode(value, buffer)
|
||||
|
||||
func decode(subject: Object, property: NodePath, buffer: StreamPeerBuffer) -> Variant:
|
||||
return _get_serializer(subject, property).decode(buffer)
|
||||
|
||||
func _get_serializer(subject: Object, property: NodePath) -> NetworkSchemaSerializer:
|
||||
return _serializers.get(subject, {}).get(property, _fallback)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cmqrteshb5sop
|
||||
@@ -0,0 +1,882 @@
|
||||
extends Object
|
||||
class_name NetworkSchemas
|
||||
|
||||
## Provides various schema serializers
|
||||
##
|
||||
## While some method names are abbreviated, they use a few naming schemes. For
|
||||
## example: [br][br]
|
||||
## [method uint16] - unsigned integer, 16 bits[br]
|
||||
## [method vec2t] - [Vector2], component of specified [i]type[/i][br]
|
||||
## [method vec3f32] - [Vector3], each component as a [method float32][br]
|
||||
## [br]
|
||||
## To handle collections, see [method array_of] and [method dictionary].
|
||||
##
|
||||
## @tutorial(Network schemas): https://foxssake.github.io/netfox/latest/netfox/guides/network-schemas/
|
||||
|
||||
## Serialize any data type supported by [method @GlobalScope.var_to_bytes].
|
||||
## [br][br]
|
||||
## Final size depends on the value.
|
||||
static func variant() -> NetworkSchemaSerializer:
|
||||
return _VariantSerializer.new()
|
||||
|
||||
## Serialize strings in UTF-8 encoding.
|
||||
## [br][br]
|
||||
## Final size depends on the string, the string itself is zero-terminated.
|
||||
static func string() -> NetworkSchemaSerializer:
|
||||
return _StringSerializer.new()
|
||||
|
||||
## Serialize booleans as 8 bits.
|
||||
## [br][br]
|
||||
## Final size is 1 byte.
|
||||
static func bool8() -> NetworkSchemaSerializer:
|
||||
return _BoolSerializer.new()
|
||||
|
||||
## Serialize unsigned integers as 8 bits.
|
||||
## [br][br]
|
||||
## Final size is 1 byte.
|
||||
static func uint8() -> NetworkSchemaSerializer:
|
||||
return _Uint8Serializer.new()
|
||||
|
||||
## Serialize unsigned integers as 16 bits.
|
||||
## [br][br]
|
||||
## Final size is 2 bytes.
|
||||
static func uint16() -> NetworkSchemaSerializer:
|
||||
return _Uint16Serializer.new()
|
||||
|
||||
## Serialize unsigned integers as 32 bits.
|
||||
## [br][br]
|
||||
## Final size is 4 bytes.
|
||||
static func uint32() -> NetworkSchemaSerializer:
|
||||
return _Uint32Serializer.new()
|
||||
|
||||
## Serialize unsigned integers as 64 bits.
|
||||
## [br][br]
|
||||
## Final size is 8 bytes.
|
||||
static func uint64() -> NetworkSchemaSerializer:
|
||||
return _Uint64Serializer.new()
|
||||
|
||||
## Serialize an unsigned integer as a variable amount of bytes.
|
||||
## [br][br]
|
||||
## Each byte contains 7 bits of data. The 8th bit indicates whether there are
|
||||
## more bytes left. Thus, small numbers fitting into 7 bits will be encoded as
|
||||
## a single byte, while larger numbers take more space as they increase.
|
||||
## [br][br]
|
||||
## Final size is 1 byte for every 7 bits of numeric data.
|
||||
static func varuint() -> NetworkSchemaSerializer:
|
||||
return _VaruintSerializer.instance
|
||||
|
||||
static func _varbits() -> NetworkSchemaSerializer:
|
||||
return _VariableBitsetSerializer.instance
|
||||
|
||||
## Serialize signed integers as 8 bits.
|
||||
## [br][br]
|
||||
## Final size is 1 byte.
|
||||
static func int8() -> NetworkSchemaSerializer:
|
||||
return _Int8Serializer.new()
|
||||
|
||||
## Serialize signed integers as 16 bits.
|
||||
## [br][br]
|
||||
## Final size is 2 bytes.
|
||||
static func int16() -> NetworkSchemaSerializer:
|
||||
return _Int16Serializer.new()
|
||||
|
||||
## Serialize signed integers as 32 bits.
|
||||
## [br][br]
|
||||
## Final size is 4 bytes.
|
||||
static func int32() -> NetworkSchemaSerializer:
|
||||
return _Int32Serializer.new()
|
||||
|
||||
## Serialize signed integers as 64 bits.
|
||||
## [br][br]
|
||||
## Final size is 8 bytes.
|
||||
static func int64() -> NetworkSchemaSerializer:
|
||||
return _Int64Serializer.new()
|
||||
|
||||
## Serialize floats in half-precision, as 16 bits.
|
||||
## [br][br]
|
||||
## This is only supported in Godot 4.4 and up, earlier versions fall back to
|
||||
## [method float32].
|
||||
## [br][br]
|
||||
## Final size is 2 bytes, 4 if using fallback.
|
||||
static func float16() -> NetworkSchemaSerializer:
|
||||
return _Float16Serializer.new()
|
||||
|
||||
## Serialize floats in single-precision, as 32 bits.
|
||||
## [br][br]
|
||||
## Final size is 4 bytes.
|
||||
static func float32() -> NetworkSchemaSerializer:
|
||||
return _Float32Serializer.new()
|
||||
|
||||
## Serialize floats in double-precision, as 64 bits.
|
||||
## [br][br]
|
||||
## Final size is 8 bytes.
|
||||
static func float64() -> NetworkSchemaSerializer:
|
||||
return _Float64Serializer.new()
|
||||
|
||||
## Serialize signed fractions in the [code][-1.0, +1.0][/code] range as 8 bits.
|
||||
## [br][br]
|
||||
## Final size is 1 byte.
|
||||
static func sfrac8() -> NetworkSchemaSerializer:
|
||||
return _QuantizingSerializer.new(uint8(), -1., 1., 0, 0xFF)
|
||||
|
||||
## Serialize signed fractions in the [code][-1.0, +1.0][/code] range as 16 bits.
|
||||
## [br][br]
|
||||
## Final size is 2 bytes.
|
||||
static func sfrac16() -> NetworkSchemaSerializer:
|
||||
return _QuantizingSerializer.new(uint16(), -1., 1., 0, 0xFFFF)
|
||||
|
||||
## Serialize signed fractions in the [code][-1.0, +1.0][/code] range as 32 bits.
|
||||
## [br][br]
|
||||
## Final size is 4 bytes.
|
||||
static func sfrac32() -> NetworkSchemaSerializer:
|
||||
return _QuantizingSerializer.new(uint32(), -1., 1., 0, 0xFFFFFFFF)
|
||||
|
||||
## Serialize signed fractions in the [code][0.0, 1.0][/code] range as 8 bits.
|
||||
## [br][br]
|
||||
## Final size is 1 byte.
|
||||
static func ufrac8() -> NetworkSchemaSerializer:
|
||||
return _QuantizingSerializer.new(uint8(), 0., 1., 0, 0xFF)
|
||||
|
||||
## Serialize signed fractions in the [code][0.0, 1.0][/code] range as 16 bits.
|
||||
## [br][br]
|
||||
## Final size is 2 bytes.
|
||||
static func ufrac16() -> NetworkSchemaSerializer:
|
||||
return _QuantizingSerializer.new(uint16(), 0., 1., 0, 0xFFFF)
|
||||
|
||||
## Serialize signed fractions in the [code][0.0, 1.0][/code] range as 32 bits.
|
||||
## [br][br]
|
||||
## Final size is 4 bytes.
|
||||
static func ufrac32() -> NetworkSchemaSerializer:
|
||||
return _QuantizingSerializer.new(uint32(), 0., 1., 0, 0xFFFFFFFF)
|
||||
|
||||
## Serialize degrees as 8 bits. The value will always decode to the
|
||||
## [code][0.0, 360.0)[/code] range.
|
||||
## [br][br]
|
||||
## Final size is 1 byte.
|
||||
static func degrees8() -> NetworkSchemaSerializer:
|
||||
return _ModuloSerializer.new(uint8(), 360., 0xFF)
|
||||
|
||||
## Serialize degrees as 16 bits. The value will always decode to the
|
||||
## [code][0.0, 360.0)[/code] range.
|
||||
## [br][br]
|
||||
## Final size is 2 bytes.
|
||||
static func degrees16() -> NetworkSchemaSerializer:
|
||||
return _ModuloSerializer.new(uint16(), 360., 0xFFFF)
|
||||
|
||||
## Serialize degrees as 32 bits. The value will always decode to the
|
||||
## [code][0.0, 360.0)[/code] range.
|
||||
## [br][br]
|
||||
## Final size is 4 bytes.
|
||||
static func degrees32() -> NetworkSchemaSerializer:
|
||||
return _ModuloSerializer.new(uint32(), 360., 0xFFFFFFFF)
|
||||
|
||||
## Serialize radians as 8 bits. The value will always decode to the
|
||||
## [code][0.0, TAU)[/code] range.
|
||||
## [br][br]
|
||||
## Final size is 1 byte.
|
||||
static func radians8() -> NetworkSchemaSerializer:
|
||||
return _ModuloSerializer.new(uint8(), TAU, 0xFF)
|
||||
|
||||
## Serialize radians as 16 bits. The value will always decode to the
|
||||
## [code][0.0, TAU)[/code] range.
|
||||
## [br][br]
|
||||
## Final size is 2 bytes.
|
||||
static func radians16() -> NetworkSchemaSerializer:
|
||||
return _ModuloSerializer.new(uint16(), TAU, 0xFFFF)
|
||||
|
||||
## Serialize radians as 32 bits. The value will always decode to the
|
||||
## [code][0.0, TAU)[/code] range.
|
||||
## [br][br]
|
||||
## Final size is 4 bytes.
|
||||
static func radians32() -> NetworkSchemaSerializer:
|
||||
return _ModuloSerializer.new(uint32(), TAU, 0xFFFFFFFF)
|
||||
|
||||
## Serialize [Vector2] objects, using [param component_serializer] to
|
||||
## serialize each component of the vector.
|
||||
## [br][br]
|
||||
## Serializes 2 components, size depends on the [param component_serializer].
|
||||
static func vec2t(component_serializer: NetworkSchemaSerializer) -> NetworkSchemaSerializer:
|
||||
return _GenericVec2Serializer.new(component_serializer)
|
||||
|
||||
## Serialize [Vector2] objects, with each component being a half-precision
|
||||
## float.
|
||||
## [br][br]
|
||||
## This is only supported in Godot 4.4 and up. Earlier versions fall back to
|
||||
## [method vec2f32].
|
||||
## [br][br]
|
||||
## Final size is 4 bytes, 8 if using fallback.
|
||||
static func vec2f16() -> NetworkSchemaSerializer:
|
||||
return vec2t(float16())
|
||||
|
||||
## Serialize [Vector2] objects, with each component being a single-precision
|
||||
## float.
|
||||
## [br][br]
|
||||
## Final size is 8 bytes.
|
||||
static func vec2f32() -> NetworkSchemaSerializer:
|
||||
return vec2t(float32())
|
||||
|
||||
## Serialize [Vector2] objects, with each component being a double-precision
|
||||
## float.
|
||||
## [br][br]
|
||||
## Final size is 16 bytes.
|
||||
static func vec2f64() -> NetworkSchemaSerializer:
|
||||
return vec2t(float64())
|
||||
|
||||
## Serialize [Vector3] objects, using [param component_serializer] to
|
||||
## serialize each component of the vector.
|
||||
## [br][br]
|
||||
## Serializes 3 components, size depends on the [param component_serializer].
|
||||
static func vec3t(component_serializer: NetworkSchemaSerializer) -> NetworkSchemaSerializer:
|
||||
return _GenericVec3Serializer.new(component_serializer)
|
||||
|
||||
## Serialize [Vector3] objects, with each component being a half-precision
|
||||
## float.
|
||||
## [br][br]
|
||||
## This is only supported in Godot 4.4 and up. Earlier versions fall back to
|
||||
## [method vec3f32].
|
||||
## [br][br]
|
||||
## Final size is 6 bytes, 12 if using fallback.
|
||||
static func vec3f16() -> NetworkSchemaSerializer:
|
||||
return vec3t(float16())
|
||||
|
||||
## Serialize [Vector3] objects, with each component being a double-precision
|
||||
## float.
|
||||
## [br][br]
|
||||
## Final size is 12 bytes.
|
||||
static func vec3f32() -> NetworkSchemaSerializer:
|
||||
return vec3t(float32())
|
||||
|
||||
## Serialize [Vector3] objects, with each component being a double-precision
|
||||
## float.
|
||||
## [br][br]
|
||||
## Final size is 24 bytes.
|
||||
static func vec3f64() -> NetworkSchemaSerializer:
|
||||
return vec3t(float64())
|
||||
|
||||
## Serialize [Vector4] objects, using [param component_serializer] to
|
||||
## serialize each component of the vector.
|
||||
## [br][br]
|
||||
## Serializes 4 components, size depends on the [param component_serializer].
|
||||
static func vec4t(component_serializer: NetworkSchemaSerializer) -> NetworkSchemaSerializer:
|
||||
return _GenericVec4Serializer.new(component_serializer)
|
||||
|
||||
## Serialize [Vector4] objects, with each component being a half-precision
|
||||
## float.
|
||||
## [br][br]
|
||||
## This is only supported in Godot 4.4 and up. Earlier versions fall back to
|
||||
## [method vec4f32].
|
||||
## [br][br]
|
||||
## Final size is 8 bytes, 16 if using fallback.
|
||||
static func vec4f16() -> NetworkSchemaSerializer:
|
||||
return vec4t(float16())
|
||||
|
||||
## Serialize [Vector4] objects, with each component being a double-precision
|
||||
## float.
|
||||
## [br][br]
|
||||
## Final size is 16 bytes.
|
||||
static func vec4f32() -> NetworkSchemaSerializer:
|
||||
return vec4t(float32())
|
||||
|
||||
## Serialize [Vector4] objects, with each component being a double-precision
|
||||
## float.
|
||||
## [br][br]
|
||||
## Final size is 32 bytes.
|
||||
static func vec4f64() -> NetworkSchemaSerializer:
|
||||
return vec4t(float64())
|
||||
|
||||
# Normals
|
||||
## Serialize normalized [Vector2] objects, using [param component_serializer] to
|
||||
## serialize each component of the vector.
|
||||
## [br][br]
|
||||
## Serializes 1 component, size depends on the [param component_serializer].
|
||||
static func normal2t(component_serializer: NetworkSchemaSerializer) -> NetworkSchemaSerializer:
|
||||
return _Normal2Serializer.new(component_serializer)
|
||||
|
||||
## Serialize normalized [Vector2] objects, with each component being a
|
||||
## half-precision float.
|
||||
## [br][br]
|
||||
## This is only supported in Godot 4.4 and up. Earlier versions fall back to
|
||||
## [method normal2f32].
|
||||
## [br][br]
|
||||
## Final size is 2 bytes, 4 if using fallback.
|
||||
static func normal2f16() -> NetworkSchemaSerializer:
|
||||
return normal2t(float16())
|
||||
|
||||
## Serialize normalized [Vector2] objects, with each component being a
|
||||
## single-precision float.
|
||||
## [br][br]
|
||||
## Final size is 4 bytes.
|
||||
static func normal2f32() -> NetworkSchemaSerializer:
|
||||
return normal2t(float32())
|
||||
|
||||
## Serialize normalized [Vector2] objects, with each component being a
|
||||
## double-precision float.
|
||||
## [br][br]
|
||||
## Final size is 8 bytes.
|
||||
static func normal2f64() -> NetworkSchemaSerializer:
|
||||
return normal2t(float64())
|
||||
|
||||
## Serialize normalized [Vector3] objects, using [param component_serializer] to
|
||||
## serialize each component of the vector.
|
||||
## [br][br]
|
||||
## Serializes 2 components, size depends on the [param component_serializer].
|
||||
static func normal3t(component_serializer: NetworkSchemaSerializer) -> NetworkSchemaSerializer:
|
||||
return _Normal3Serializer.new(component_serializer)
|
||||
|
||||
## Serialize normalized [Vector3] objects, with each component being a
|
||||
## half-precision float.
|
||||
## [br][br]
|
||||
## This is only supported in Godot 4.4 and up. Earlier versions fall back to
|
||||
## [method normal3f32].
|
||||
## [br][br]
|
||||
## Final size is 4 bytes, 8 if using fallback.
|
||||
static func normal3f16() -> NetworkSchemaSerializer:
|
||||
return normal3t(float16())
|
||||
|
||||
## Serialize normalized [Vector3] objects, with each component being a
|
||||
## single-precision float.
|
||||
## [br][br]
|
||||
## Final size is 8 bytes.
|
||||
static func normal3f32() -> NetworkSchemaSerializer:
|
||||
return normal3t(float32())
|
||||
|
||||
## Serialize normalized [Vector3] objects, with each component being a
|
||||
## double-precision float.
|
||||
## [br][br]
|
||||
## Final size is 16 bytes.
|
||||
static func normal3f64() -> NetworkSchemaSerializer:
|
||||
return normal3t(float64())
|
||||
|
||||
# Quaternion
|
||||
## Serialize [Quaternion] objects, using [param component_serializer] to
|
||||
## serialize each component of the quaternion.
|
||||
## [br][br]
|
||||
## Serializes 4 components, size depends on the [param component_serializer].
|
||||
static func quatt(component_serializer: NetworkSchemaSerializer) -> NetworkSchemaSerializer:
|
||||
return _GenericQuaternionSerializer.new(component_serializer)
|
||||
|
||||
## Serialize [Quaternion] objects, with each component being a half-precision
|
||||
## float.
|
||||
## [br][br]
|
||||
## This is only supported in Godot 4.4 and up. Earlier versions fall back to
|
||||
## [method quat32f].
|
||||
## [br][br]
|
||||
## Final size is 8 bytes, 16 if using fallback.
|
||||
static func quatf16() -> NetworkSchemaSerializer:
|
||||
return quatt(float16())
|
||||
|
||||
## Serialize [Quaternion] objects, with each component being a single-precision
|
||||
## float.
|
||||
## [br][br]
|
||||
## Final size is 16 bytes.
|
||||
static func quatf32() -> NetworkSchemaSerializer:
|
||||
return quatt(float32())
|
||||
|
||||
## Serialize [Quaternion] objects, with each component being a double-precision
|
||||
## float.
|
||||
## [br][br]
|
||||
## Final size is 32 bytes.
|
||||
static func quatf64() -> NetworkSchemaSerializer:
|
||||
return quatt(float64())
|
||||
|
||||
# Transforms
|
||||
## Serialize [Transform2D] objects, using [param component_serializer] to
|
||||
## serialize each component of the transform.
|
||||
## [br][br]
|
||||
## Serializes a 2x3 matrix in 6 components, final size depends on [param
|
||||
## component_serializer].
|
||||
static func transform2t(component_serializer: NetworkSchemaSerializer) -> NetworkSchemaSerializer:
|
||||
return _GenericTransform2DSerializer.new(component_serializer)
|
||||
|
||||
## Serialize [Transform2D] objects, with each component being a half-precision
|
||||
## float.
|
||||
## [br][br]
|
||||
## This is only supported in Godot 4.4 and up. Earlier versions fall back to
|
||||
## [method transform2f32].
|
||||
## [br][br]
|
||||
## Final size is 12 bytes, 24 if using fallback.
|
||||
static func transform2f16() -> NetworkSchemaSerializer:
|
||||
return transform2t(float16())
|
||||
|
||||
## Serialize [Transform2D] objects, with each component being a single-precision
|
||||
## float.
|
||||
## [br][br]
|
||||
## Final size is 24 bytes.
|
||||
static func transform2f32() -> NetworkSchemaSerializer:
|
||||
return transform2t(float32())
|
||||
|
||||
## Serialize [Transform2D] objects, with each component being a double-precision
|
||||
## float.
|
||||
## [br][br]
|
||||
## Final size is 48 bytes.
|
||||
static func transform2f64() -> NetworkSchemaSerializer:
|
||||
return transform2t(float64())
|
||||
|
||||
## Serialize [Transform3D] objects, using [param component_serializer] to
|
||||
## serialize each component of the transform.
|
||||
## [br][br]
|
||||
## Serializes a 3x4 matrix in 12 components, final size depends on [param
|
||||
## component_serializer].
|
||||
static func transform3t(component_serializer: NetworkSchemaSerializer) -> NetworkSchemaSerializer:
|
||||
return _GenericTransform3DSerializer.new(component_serializer)
|
||||
|
||||
## Serialize [Transform3D] objects, with each component being a half-precision
|
||||
## float.
|
||||
## [br][br]
|
||||
## This is only supported in Godot 4.4 and up. Earlier versions fall back to
|
||||
## [method transform3f32].
|
||||
## [br][br]
|
||||
## Final size is 24 bytes, 48 if using fallback.
|
||||
static func transform3f16() -> NetworkSchemaSerializer:
|
||||
return transform3t(float16())
|
||||
|
||||
## Serialize [Transform3D] objects, with each component being a single-precision
|
||||
## float.
|
||||
## [br][br]
|
||||
## Final size is 48 bytes.
|
||||
static func transform3f32() -> NetworkSchemaSerializer:
|
||||
return transform3t(float32())
|
||||
|
||||
## Serialize [Transform2D] objects, with each component being a double-precision
|
||||
## float.
|
||||
## [br][br]
|
||||
## Final size is 96 bytes.
|
||||
static func transform3f64() -> NetworkSchemaSerializer:
|
||||
return transform3t(float64())
|
||||
|
||||
# Collections
|
||||
|
||||
## Serialize homogenoeous arrays, using [param item_serializer] to
|
||||
## serialize each item, and [param size_serializer] to serialize the array's
|
||||
## size.
|
||||
## [br][br]
|
||||
## To serialize heterogenoeous arrays, use [method variant] as the item
|
||||
## serializer.
|
||||
## [br][br]
|
||||
## Final size is [code]sizeof(size_serializer) + array.size() * sizeof(item_serializer)[/code]
|
||||
static func array_of(item_serializer: NetworkSchemaSerializer = variant(), size_serializer: NetworkSchemaSerializer = uint16()) -> NetworkSchemaSerializer:
|
||||
return _ArraySerializer.new(item_serializer, size_serializer)
|
||||
|
||||
## Serialize homogenoeous dictionaries, using [param key_serialize] and
|
||||
## [param value_serializer] to serialize key-value pairs, and
|
||||
## [param size_serializer] to serialize the number of entries.
|
||||
## [br][br]
|
||||
## If either the keys or values are not homogenoeous, use [method variant].
|
||||
## [br][br]
|
||||
## Final size is [code]sizeof(size_serializer) + dictionary.size() * (sizeof(key_serializer) + sizeof(value_serializer))[/code]
|
||||
static func dictionary(key_serializer: NetworkSchemaSerializer = variant(),
|
||||
value_serializer: NetworkSchemaSerializer = variant(),
|
||||
size_serializer: NetworkSchemaSerializer = uint16()) -> NetworkSchemaSerializer:
|
||||
return _DictionarySerializer.new(key_serializer, value_serializer, size_serializer)
|
||||
|
||||
# Serializes [_NetworkIdentityReference] objects
|
||||
static func _netref() -> NetworkSchemaSerializer:
|
||||
return _NetworkIdentityReferenceSerializer.new()
|
||||
|
||||
# Serializer classes
|
||||
|
||||
class _VariantSerializer extends NetworkSchemaSerializer:
|
||||
func encode(v: Variant, b: StreamPeerBuffer) -> void:
|
||||
b.put_var(v, false)
|
||||
|
||||
func decode(b: StreamPeerBuffer) -> Variant:
|
||||
return b.get_var(false)
|
||||
|
||||
class _StringSerializer extends NetworkSchemaSerializer:
|
||||
func encode(v: Variant, b: StreamPeerBuffer) -> void:
|
||||
b.put_utf8_string(str(v))
|
||||
|
||||
func decode(b: StreamPeerBuffer) -> Variant:
|
||||
return b.get_utf8_string()
|
||||
|
||||
class _BoolSerializer extends NetworkSchemaSerializer:
|
||||
func encode(v: Variant, b: StreamPeerBuffer) -> void:
|
||||
b.put_u8(1 if v else 0)
|
||||
|
||||
func decode(b: StreamPeerBuffer) -> Variant:
|
||||
return b.get_u8() > 0
|
||||
|
||||
class _Uint8Serializer extends NetworkSchemaSerializer:
|
||||
func encode(v: Variant, b: StreamPeerBuffer) -> void: b.put_u8(v)
|
||||
func decode(b: StreamPeerBuffer) -> Variant: return b.get_u8()
|
||||
|
||||
class _Uint16Serializer extends NetworkSchemaSerializer:
|
||||
func encode(v: Variant, b: StreamPeerBuffer) -> void: b.put_u16(v)
|
||||
func decode(b: StreamPeerBuffer) -> Variant: return b.get_u16()
|
||||
|
||||
class _Uint32Serializer extends NetworkSchemaSerializer:
|
||||
func encode(v: Variant, b: StreamPeerBuffer) -> void: b.put_u32(v)
|
||||
func decode(b: StreamPeerBuffer) -> Variant: return b.get_u32()
|
||||
|
||||
class _Uint64Serializer extends NetworkSchemaSerializer:
|
||||
func encode(v: Variant, b: StreamPeerBuffer) -> void: b.put_u64(v)
|
||||
func decode(b: StreamPeerBuffer) -> Variant: return b.get_u64()
|
||||
|
||||
class _Int8Serializer extends NetworkSchemaSerializer:
|
||||
func encode(v: Variant, b: StreamPeerBuffer) -> void: b.put_8(v)
|
||||
func decode(b: StreamPeerBuffer) -> Variant: return b.get_8()
|
||||
|
||||
class _Int16Serializer extends NetworkSchemaSerializer:
|
||||
func encode(v: Variant, b: StreamPeerBuffer) -> void: b.put_16(v)
|
||||
func decode(b: StreamPeerBuffer) -> Variant: return b.get_16()
|
||||
|
||||
class _Int32Serializer extends NetworkSchemaSerializer:
|
||||
func encode(v: Variant, b: StreamPeerBuffer) -> void: b.put_32(v)
|
||||
func decode(b: StreamPeerBuffer) -> Variant: return b.get_32()
|
||||
|
||||
class _Int64Serializer extends NetworkSchemaSerializer:
|
||||
func encode(v: Variant, b: StreamPeerBuffer) -> void: b.put_64(v)
|
||||
func decode(b: StreamPeerBuffer) -> Variant: return b.get_64()
|
||||
|
||||
class _VaruintSerializer extends NetworkSchemaSerializer:
|
||||
static var instance := _VaruintSerializer.new()
|
||||
|
||||
func encode(v: Variant, b: StreamPeerBuffer) -> void:
|
||||
var value := v as int
|
||||
for __ in 8: # Bounded while loop
|
||||
var nominator := value & 0b0111_1111 # Grab the lowest 7 bits
|
||||
var continuator := value > 0b0111_1111 # Continue if more bits
|
||||
var cont_bit := 0b1000_0000 if continuator else 0
|
||||
var byte := nominator | cont_bit # Combine into 1 byte
|
||||
value = value >> 7 # Discard lower 7 bits
|
||||
b.put_u8(byte) # Save byte
|
||||
|
||||
# Stop if no more bytes to write
|
||||
if not continuator:
|
||||
break
|
||||
|
||||
func decode(b: StreamPeerBuffer) -> Variant:
|
||||
var value := 0
|
||||
for i in 8:
|
||||
var byte := b.get_u8()
|
||||
var nominator := byte & 0b0111_1111
|
||||
var continuator := (byte & 0b1000_0000) != 0
|
||||
value += nominator << (i * 7)
|
||||
|
||||
if not continuator:
|
||||
break
|
||||
return value
|
||||
|
||||
class _VariableBitsetSerializer extends NetworkSchemaSerializer:
|
||||
static var instance := _VariableBitsetSerializer.new()
|
||||
|
||||
func encode(v: Variant, b: StreamPeerBuffer) -> void:
|
||||
var bitset := v as _Bitset
|
||||
if bitset.is_empty():
|
||||
b.put_u8(0)
|
||||
return
|
||||
|
||||
for i in range(0, bitset.bit_count(), 7):
|
||||
var byte := 0
|
||||
|
||||
# Set bits
|
||||
if bitset.bit_count() > i + 0 and bitset.get_bit(i + 0): byte |= 0x01
|
||||
if bitset.bit_count() > i + 1 and bitset.get_bit(i + 1): byte |= 0x02
|
||||
if bitset.bit_count() > i + 2 and bitset.get_bit(i + 2): byte |= 0x04
|
||||
if bitset.bit_count() > i + 3 and bitset.get_bit(i + 3): byte |= 0x08
|
||||
if bitset.bit_count() > i + 4 and bitset.get_bit(i + 4): byte |= 0x10
|
||||
if bitset.bit_count() > i + 5 and bitset.get_bit(i + 5): byte |= 0x20
|
||||
if bitset.bit_count() > i + 6 and bitset.get_bit(i + 6): byte |= 0x40
|
||||
|
||||
# Set highest bit if there's more bytes to read
|
||||
if bitset.bit_count() > i + 7: byte |= 80
|
||||
|
||||
b.put_u8(byte)
|
||||
|
||||
func decode(b: StreamPeerBuffer) -> Variant:
|
||||
var bools := []
|
||||
|
||||
while true:
|
||||
var byte := b.get_u8()
|
||||
|
||||
bools.append(byte & 0x01)
|
||||
bools.append(byte & 0x02)
|
||||
bools.append(byte & 0x04)
|
||||
bools.append(byte & 0x08)
|
||||
bools.append(byte & 0x10)
|
||||
bools.append(byte & 0x20)
|
||||
bools.append(byte & 0x40)
|
||||
|
||||
# Stop if no more data to read
|
||||
if byte & 0x80 == 0:
|
||||
break
|
||||
|
||||
return _Bitset.of_bools(bools)
|
||||
|
||||
class _Float16Serializer extends NetworkSchemaSerializer:
|
||||
func encode(v: Variant, b: StreamPeerBuffer) -> void:
|
||||
if Engine.get_version_info().hex >= 0x040400:
|
||||
b.put_half(v)
|
||||
else:
|
||||
b.put_float(v)
|
||||
|
||||
func decode(b: StreamPeerBuffer) -> Variant:
|
||||
if Engine.get_version_info().hex >= 0x040400:
|
||||
return b.get_half()
|
||||
else:
|
||||
return b.get_float()
|
||||
|
||||
class _Float32Serializer extends NetworkSchemaSerializer:
|
||||
func encode(v: Variant, b: StreamPeerBuffer) -> void: b.put_float(v)
|
||||
func decode(b: StreamPeerBuffer) -> Variant: return b.get_float()
|
||||
|
||||
class _Float64Serializer extends NetworkSchemaSerializer:
|
||||
func encode(v: Variant, b: StreamPeerBuffer) -> void: b.put_double(v)
|
||||
func decode(b: StreamPeerBuffer) -> Variant: return b.get_double()
|
||||
|
||||
class _GenericVec2Serializer extends NetworkSchemaSerializer:
|
||||
var component: NetworkSchemaSerializer
|
||||
|
||||
func _init(p_component: NetworkSchemaSerializer):
|
||||
component = p_component
|
||||
|
||||
func encode(v: Variant, b: StreamPeerBuffer) -> void:
|
||||
component.encode(v.x, b)
|
||||
component.encode(v.y, b)
|
||||
|
||||
func decode(b: StreamPeerBuffer) -> Variant:
|
||||
return Vector2(component.decode(b), component.decode(b))
|
||||
|
||||
class _GenericVec3Serializer extends NetworkSchemaSerializer:
|
||||
var component: NetworkSchemaSerializer
|
||||
|
||||
func _init(p_component: NetworkSchemaSerializer):
|
||||
component = p_component
|
||||
|
||||
func encode(v: Variant, b: StreamPeerBuffer) -> void:
|
||||
component.encode(v.x, b)
|
||||
component.encode(v.y, b)
|
||||
component.encode(v.z, b)
|
||||
|
||||
func decode(b: StreamPeerBuffer) -> Variant:
|
||||
return Vector3(
|
||||
component.decode(b), component.decode(b), component.decode(b)
|
||||
)
|
||||
|
||||
class _Normal2Serializer extends NetworkSchemaSerializer:
|
||||
var component: NetworkSchemaSerializer
|
||||
|
||||
func _init(p_component: NetworkSchemaSerializer):
|
||||
component = p_component
|
||||
|
||||
func encode(v: Variant, b: StreamPeerBuffer) -> void:
|
||||
component.encode((v as Vector2).angle(), b)
|
||||
|
||||
func decode(b: StreamPeerBuffer) -> Variant:
|
||||
return Vector2.RIGHT.rotated(component.decode(b))
|
||||
|
||||
class _Normal3Serializer extends NetworkSchemaSerializer:
|
||||
var component: NetworkSchemaSerializer
|
||||
|
||||
func _init(p_component: NetworkSchemaSerializer):
|
||||
component = p_component
|
||||
|
||||
func encode(v: Variant, b: StreamPeerBuffer) -> void:
|
||||
var uv := (v as Vector3).octahedron_encode()
|
||||
component.encode(uv.x, b)
|
||||
component.encode(uv.y, b)
|
||||
|
||||
func decode(b: StreamPeerBuffer) -> Variant:
|
||||
return Vector3.octahedron_decode(
|
||||
Vector2(component.decode(b), component.decode(b))
|
||||
)
|
||||
|
||||
class _GenericVec4Serializer extends NetworkSchemaSerializer:
|
||||
var component: NetworkSchemaSerializer
|
||||
|
||||
func _init(p_component: NetworkSchemaSerializer):
|
||||
component = p_component
|
||||
|
||||
func encode(v: Variant, b: StreamPeerBuffer) -> void:
|
||||
component.encode(v.x, b)
|
||||
component.encode(v.y, b)
|
||||
component.encode(v.z, b)
|
||||
component.encode(v.w, b)
|
||||
|
||||
func decode(b: StreamPeerBuffer) -> Variant:
|
||||
return Vector4(
|
||||
component.decode(b), component.decode(b), component.decode(b), component.decode(b)
|
||||
)
|
||||
|
||||
class _GenericQuaternionSerializer extends NetworkSchemaSerializer:
|
||||
var component: NetworkSchemaSerializer
|
||||
|
||||
func _init(p_component: NetworkSchemaSerializer):
|
||||
component = p_component
|
||||
|
||||
func encode(v: Variant, b: StreamPeerBuffer) -> void:
|
||||
component.encode(v.x, b)
|
||||
component.encode(v.y, b)
|
||||
component.encode(v.z, b)
|
||||
component.encode(v.w, b)
|
||||
|
||||
func decode(b: StreamPeerBuffer) -> Variant:
|
||||
return Quaternion(
|
||||
component.decode(b), component.decode(b), component.decode(b), component.decode(b)
|
||||
)
|
||||
|
||||
class _GenericTransform2DSerializer extends NetworkSchemaSerializer:
|
||||
var component: NetworkSchemaSerializer
|
||||
|
||||
func _init(p_component: NetworkSchemaSerializer):
|
||||
component = p_component
|
||||
|
||||
func encode(v: Variant, b: StreamPeerBuffer) -> void:
|
||||
var t := v as Transform2D
|
||||
|
||||
component.encode(t.x.x, b); component.encode(t.x.y, b)
|
||||
component.encode(t.y.x, b); component.encode(t.y.y, b)
|
||||
component.encode(t.origin.x, b); component.encode(t.origin.y, b)
|
||||
|
||||
func decode(b: StreamPeerBuffer) -> Variant:
|
||||
return Transform2D(
|
||||
Vector2(component.decode(b), component.decode(b)),
|
||||
Vector2(component.decode(b), component.decode(b)),
|
||||
Vector2(component.decode(b), component.decode(b)),
|
||||
)
|
||||
|
||||
class _GenericTransform3DSerializer extends NetworkSchemaSerializer:
|
||||
var component: NetworkSchemaSerializer
|
||||
|
||||
func _init(p_component: NetworkSchemaSerializer):
|
||||
component = p_component
|
||||
|
||||
func encode(v: Variant, b: StreamPeerBuffer) -> void:
|
||||
var t := v as Transform3D
|
||||
|
||||
component.encode(t.basis.x.x, b); component.encode(t.basis.x.y, b); component.encode(t.basis.x.z, b)
|
||||
component.encode(t.basis.y.x, b); component.encode(t.basis.y.y, b); component.encode(t.basis.y.z, b)
|
||||
component.encode(t.basis.z.x, b); component.encode(t.basis.z.y, b); component.encode(t.basis.z.z, b)
|
||||
component.encode(t.origin.x, b); component.encode(t.origin.y, b); component.encode(t.origin.z, b)
|
||||
|
||||
func decode(b: StreamPeerBuffer) -> Variant:
|
||||
return Transform3D(
|
||||
Basis(
|
||||
Vector3(component.decode(b), component.decode(b), component.decode(b)),
|
||||
Vector3(component.decode(b), component.decode(b), component.decode(b)),
|
||||
Vector3(component.decode(b), component.decode(b), component.decode(b)),
|
||||
),
|
||||
Vector3(component.decode(b), component.decode(b), component.decode(b))
|
||||
)
|
||||
|
||||
class _QuantizingSerializer extends NetworkSchemaSerializer:
|
||||
var component: NetworkSchemaSerializer
|
||||
var from_min: Variant
|
||||
var from_max: Variant
|
||||
var to_min: Variant
|
||||
var to_max: Variant
|
||||
|
||||
func _init(
|
||||
p_component: NetworkSchemaSerializer, p_from_min: Variant,
|
||||
p_from_max: Variant, p_to_min: Variant, p_to_max: Variant
|
||||
):
|
||||
component = p_component
|
||||
from_min = p_from_min
|
||||
from_max = p_from_max
|
||||
to_min = p_to_min
|
||||
to_max = p_to_max
|
||||
|
||||
func encode(v: Variant, b: StreamPeerBuffer) -> void:
|
||||
var f := inverse_lerp(from_min, from_max, v)
|
||||
var s := lerp(to_min, to_max, f)
|
||||
component.encode(s, b)
|
||||
|
||||
func decode(b: StreamPeerBuffer) -> Variant:
|
||||
var s := component.decode(b)
|
||||
var f := inverse_lerp(to_min, to_max, s)
|
||||
return lerp(from_min, from_max, f)
|
||||
|
||||
class _ModuloSerializer extends NetworkSchemaSerializer:
|
||||
var component: NetworkSchemaSerializer
|
||||
var value_max: Variant
|
||||
var component_max: Variant
|
||||
|
||||
func _init(p_component: NetworkSchemaSerializer, p_value_max: Variant, p_component_max: Variant):
|
||||
component = p_component
|
||||
value_max = p_value_max
|
||||
component_max = p_component_max
|
||||
|
||||
func encode(v: Variant, b: StreamPeerBuffer) -> void:
|
||||
var f = fposmod(float(v), value_max) / value_max
|
||||
var s = f * component_max
|
||||
component.encode(s, b)
|
||||
|
||||
func decode(b: StreamPeerBuffer) -> Variant:
|
||||
var s = float(component.decode(b))
|
||||
return (s / component_max) * value_max
|
||||
|
||||
class _ArraySerializer extends NetworkSchemaSerializer:
|
||||
var component: NetworkSchemaSerializer
|
||||
var size: NetworkSchemaSerializer
|
||||
|
||||
func _init(p_component: NetworkSchemaSerializer, p_size: NetworkSchemaSerializer):
|
||||
component = p_component
|
||||
size = p_size
|
||||
|
||||
func encode(v: Variant, b: StreamPeerBuffer) -> void:
|
||||
var array := v as Array
|
||||
|
||||
size.encode(array.size(), b)
|
||||
for item in array:
|
||||
component.encode(item, b)
|
||||
|
||||
func decode(b: StreamPeerBuffer) -> Variant:
|
||||
var array := []
|
||||
|
||||
var item_count = size.decode(b)
|
||||
array.resize(item_count)
|
||||
for i in item_count:
|
||||
array[i] = component.decode(b)
|
||||
|
||||
return array
|
||||
|
||||
class _DictionarySerializer extends NetworkSchemaSerializer:
|
||||
var key_serializer: NetworkSchemaSerializer
|
||||
var value_serializer: NetworkSchemaSerializer
|
||||
var size_serializer: NetworkSchemaSerializer
|
||||
|
||||
func _init(p_key_serializer: NetworkSchemaSerializer, p_value_serializer: NetworkSchemaSerializer, p_size_serializer: NetworkSchemaSerializer):
|
||||
key_serializer = p_key_serializer
|
||||
value_serializer = p_value_serializer
|
||||
size_serializer = p_size_serializer
|
||||
|
||||
func encode(v: Variant, b: StreamPeerBuffer) -> void:
|
||||
var dictionary := v as Dictionary
|
||||
|
||||
size_serializer.encode(dictionary.size(), b)
|
||||
for key in dictionary:
|
||||
var value = dictionary[key]
|
||||
key_serializer.encode(key, b)
|
||||
value_serializer.encode(value, b)
|
||||
|
||||
func decode(b: StreamPeerBuffer) -> Variant:
|
||||
var dictionary := {}
|
||||
|
||||
var size = size_serializer.decode(b)
|
||||
for i in size:
|
||||
var key = key_serializer.decode(b)
|
||||
var value = value_serializer.decode(b)
|
||||
dictionary[key] = value
|
||||
|
||||
return dictionary
|
||||
|
||||
class _NetworkIdentityReferenceSerializer extends NetworkSchemaSerializer:
|
||||
static var varuint := _VaruintSerializer.new()
|
||||
|
||||
func encode(v: Variant, b: StreamPeerBuffer) -> void:
|
||||
var ref := v as _NetworkIdentityReference
|
||||
if ref.has_id():
|
||||
varuint.encode(ref.get_id(), b)
|
||||
else:
|
||||
b.put_u8(0)
|
||||
# TODO(#562): Get rid of Godot's prepended 32 bits of string length
|
||||
# TODO(#562): Write is easy, prefer not manually iterating till \0 on read
|
||||
b.put_utf8_string(ref.get_full_name())
|
||||
|
||||
func decode(b: StreamPeerBuffer) -> Variant:
|
||||
var id := varuint.decode(b) as int
|
||||
if id == 0:
|
||||
var full_name := b.get_utf8_string()
|
||||
return _NetworkIdentityReference.of_full_name(full_name)
|
||||
else:
|
||||
return _NetworkIdentityReference.of_id(id)
|
||||
@@ -0,0 +1 @@
|
||||
uid://byaxeh31f2i6m
|
||||
@@ -0,0 +1,37 @@
|
||||
extends RefCounted
|
||||
class_name _BaseSnapshotSerializer
|
||||
|
||||
var _identity_server: _NetworkIdentityServer
|
||||
var _schemas: _NetworkSchema
|
||||
|
||||
static var _default_filter := func(subject: Object): return true
|
||||
static var _logger := NetfoxLogger._for_netfox("DenseSnapshotSerializer")
|
||||
|
||||
func _init(p_schemas: _NetworkSchema, p_identity_server: _NetworkIdentityServer = null):
|
||||
assert(p_schemas != null, "Missing schemas!")
|
||||
# Intentionally storing reference, so it can be modified from the outside
|
||||
# e.g. RollbackSynchronizerServer adds a property
|
||||
_schemas = p_schemas
|
||||
_identity_server = p_identity_server
|
||||
|
||||
func _write_property(node: Node, property: NodePath, value: Variant, buffer: StreamPeerBuffer) -> void:
|
||||
_schemas.encode(node, property, value, buffer)
|
||||
|
||||
func _read_property(node: Node, property: NodePath, buffer: StreamPeerBuffer) -> Variant:
|
||||
return _schemas.decode(node, property, buffer)
|
||||
|
||||
func _write_identifier(subject: Object, peer: int, buffer: StreamPeerBuffer) -> Error:
|
||||
var netref := NetworkSchemas._netref()
|
||||
var identifier := _get_identity_server()._get_identifier_of(subject)
|
||||
if not identifier:
|
||||
_logger.error("Can't synchronize %s, identifier missing!", [subject])
|
||||
return ERR_DOES_NOT_EXIST
|
||||
|
||||
var idref := identifier.reference_for(peer)
|
||||
netref.encode(idref, buffer)
|
||||
return OK
|
||||
|
||||
func _get_identity_server() -> _NetworkIdentityServer:
|
||||
if not _identity_server:
|
||||
_identity_server = NetworkIdentityServer
|
||||
return _identity_server
|
||||
@@ -0,0 +1 @@
|
||||
uid://b8mn7i6dnt0bu
|
||||
@@ -0,0 +1,87 @@
|
||||
extends _BaseSnapshotSerializer
|
||||
class_name _DenseSnapshotSerializer
|
||||
|
||||
static func _static_init():
|
||||
_logger = NetfoxLogger._for_netfox("DenseSnapshotSerializer")
|
||||
|
||||
func write_for(peer: int, snapshot: _Snapshot, properties: _PropertyPool, filter: Callable = _default_filter, buffer: StreamPeerBuffer = null) -> PackedByteArray:
|
||||
if buffer == null:
|
||||
buffer = StreamPeerBuffer.new()
|
||||
|
||||
var netref := NetworkSchemas._netref()
|
||||
var varuint := NetworkSchemas.varuint()
|
||||
|
||||
var node_buffer := StreamPeerBuffer.new()
|
||||
|
||||
var has_data := false
|
||||
|
||||
# Write tick
|
||||
buffer.put_u32(snapshot.tick)
|
||||
|
||||
# For each node
|
||||
for subject in properties.get_subjects():
|
||||
if not filter.call(subject): continue
|
||||
if not snapshot.is_auth(subject): continue
|
||||
|
||||
var node := subject as Node
|
||||
assert(node.is_multiplayer_authority(), "Trying to serialize state for non-owned node!")
|
||||
assert(snapshot.is_auth(node), "Trying to serialize non-auth state node!")
|
||||
|
||||
# Write identifier
|
||||
if _write_identifier(node, peer, buffer) != OK:
|
||||
continue
|
||||
|
||||
# Write properties as-is
|
||||
# First into a buffer, so we can start with the state size
|
||||
node_buffer.clear()
|
||||
for property in properties.get_properties_of(node):
|
||||
assert(snapshot.has_property(node, property), "Trying to serialize missing property %s on subject %s!" % [property, node])
|
||||
|
||||
var value := snapshot.get_property(node, property)
|
||||
_write_property(node, property, value, node_buffer)
|
||||
|
||||
# Indicate state size for the node
|
||||
varuint.encode(node_buffer.data_array.size(), buffer)
|
||||
|
||||
# Write node state
|
||||
buffer.put_data(node_buffer.data_array)
|
||||
|
||||
has_data = true
|
||||
|
||||
if has_data:
|
||||
return buffer.data_array
|
||||
else:
|
||||
return PackedByteArray()
|
||||
|
||||
func read_from(peer: int, properties: _PropertyPool, buffer: StreamPeerBuffer, is_auth: bool = true) -> _Snapshot:
|
||||
var netref := NetworkSchemas._netref()
|
||||
var varuint := NetworkSchemas.varuint()
|
||||
var node_buffer := StreamPeerBuffer.new()
|
||||
|
||||
# Read tick
|
||||
var tick := buffer.get_u32()
|
||||
var snapshot := _Snapshot.new(tick)
|
||||
|
||||
while buffer.get_available_bytes() > 0:
|
||||
# Read identity reference, data size, and data
|
||||
var idref := netref.decode(buffer) as _NetworkIdentityReference
|
||||
var node_data_size := varuint.decode(buffer) as int
|
||||
node_buffer.data_array = buffer.get_partial_data(node_data_size)[1]
|
||||
|
||||
# Resolve to identifier
|
||||
var identifier := _get_identity_server()._resolve_reference(peer, idref)
|
||||
if not identifier:
|
||||
# TODO(#563): Handle unknown IDs gracefully
|
||||
_logger.warning("Received unknown identity reference %s, skipping data", [idref])
|
||||
continue
|
||||
var node := identifier.get_subject() as Node
|
||||
|
||||
# Read properties
|
||||
for property in properties.get_properties_of(node):
|
||||
if node_buffer.get_available_bytes() == 0: break
|
||||
|
||||
var value := _read_property(node, property, node_buffer)
|
||||
snapshot.set_property(node, property, value)
|
||||
snapshot.set_auth(node, is_auth)
|
||||
|
||||
return snapshot
|
||||
@@ -0,0 +1 @@
|
||||
uid://k37sxuptebt3
|
||||
@@ -0,0 +1 @@
|
||||
uid://bf67e0hnmp70h
|
||||
@@ -0,0 +1,40 @@
|
||||
extends _BaseSnapshotSerializer
|
||||
class_name _RedundantSnapshotSerializer
|
||||
|
||||
var _dense_serializer: _DenseSnapshotSerializer
|
||||
|
||||
static func _static_init():
|
||||
_logger = NetfoxLogger._for_netfox("RedundantSnapshotSerializer")
|
||||
|
||||
func _init(p_schemas: _NetworkSchema, p_identity_server: _NetworkIdentityServer = null):
|
||||
super(p_schemas)
|
||||
_dense_serializer = _DenseSnapshotSerializer.new(_schemas, p_identity_server)
|
||||
|
||||
func write_for(peer: int, snapshots: Array[_Snapshot], properties: _PropertyPool, buffer: StreamPeerBuffer = null) -> PackedByteArray:
|
||||
var varuint := NetworkSchemas.varuint()
|
||||
|
||||
if buffer == null:
|
||||
buffer = StreamPeerBuffer.new()
|
||||
|
||||
# TODO(#560): How about encoding the first snapshot as-is, and then the rest as diffs
|
||||
for snapshot in snapshots:
|
||||
var serialized := _dense_serializer.write_for(peer, snapshot, properties)
|
||||
|
||||
# Write size and snapshot
|
||||
varuint.encode(serialized.size(), buffer)
|
||||
buffer.put_data(serialized)
|
||||
|
||||
return buffer.data_array
|
||||
|
||||
func read_from(peer: int, properties: _PropertyPool, buffer: StreamPeerBuffer, is_auth: bool = true) -> Array[_Snapshot]:
|
||||
var varuint := NetworkSchemas.varuint()
|
||||
|
||||
var snapshots := [] as Array[_Snapshot]
|
||||
while buffer.get_available_bytes() > 0:
|
||||
var snapshot_size := varuint.decode(buffer)
|
||||
var snapshot_buffer := StreamPeerBuffer.new()
|
||||
snapshot_buffer.data_array = buffer.get_partial_data(snapshot_size)[1]
|
||||
|
||||
var snapshot := _dense_serializer.read_from(peer, properties, snapshot_buffer, is_auth)
|
||||
snapshots.append(snapshot)
|
||||
return snapshots
|
||||
@@ -0,0 +1 @@
|
||||
uid://ch0p4f4nuy1c
|
||||
@@ -0,0 +1,92 @@
|
||||
extends _BaseSnapshotSerializer
|
||||
class_name _SparseSnapshotSerializer
|
||||
|
||||
static func _static_init():
|
||||
_logger = NetfoxLogger._for_netfox("SparseSnapshotSerializer")
|
||||
|
||||
func write_for(peer: int, snapshot: _Snapshot, properties: _PropertyPool, filter: Callable = _default_filter, buffer: StreamPeerBuffer = null) -> PackedByteArray:
|
||||
if buffer == null:
|
||||
buffer = StreamPeerBuffer.new()
|
||||
|
||||
var netref := NetworkSchemas._netref()
|
||||
var varuint := NetworkSchemas.varuint()
|
||||
var varbits := NetworkSchemas._varbits()
|
||||
|
||||
var node_buffer := StreamPeerBuffer.new()
|
||||
|
||||
var has_data := false
|
||||
|
||||
# Write ticks
|
||||
buffer.put_u32(snapshot.tick)
|
||||
|
||||
# For each node
|
||||
for subject in properties.get_subjects():
|
||||
if not filter.call(subject): continue
|
||||
if not snapshot.is_auth(subject): continue
|
||||
|
||||
var node := subject as Node
|
||||
assert(node.is_multiplayer_authority(), "Trying to serialize state for non-owned node!")
|
||||
assert(snapshot.is_auth(node), "Trying to serialize non-auth state node!")
|
||||
|
||||
# Write identifier
|
||||
if _write_identifier(node, peer, buffer) != OK:
|
||||
continue
|
||||
|
||||
node_buffer.clear()
|
||||
|
||||
var node_props := properties.get_properties_of(node)
|
||||
var changed_bits := _Bitset.new(node_props.size())
|
||||
|
||||
for i in node_props.size():
|
||||
var property := node_props[i]
|
||||
if not snapshot.has_property(node, property):
|
||||
continue
|
||||
|
||||
changed_bits.set_bit(i)
|
||||
var value := snapshot.get_property(node, property)
|
||||
_write_property(node, property, value, node_buffer)
|
||||
|
||||
varuint.encode(node_buffer.data_array.size(), buffer) # Node props len
|
||||
varbits.encode(changed_bits, buffer) # Changed prop bits
|
||||
buffer.put_data(node_buffer.data_array) # Changed props
|
||||
has_data = true
|
||||
|
||||
if has_data:
|
||||
return buffer.data_array
|
||||
else:
|
||||
# Return an empty buffer if we ended up not serializing anything
|
||||
return PackedByteArray()
|
||||
|
||||
func read_from(peer: int, properties: _PropertyPool, buffer: StreamPeerBuffer, is_auth: bool = true) -> _Snapshot:
|
||||
var netref := NetworkSchemas._netref()
|
||||
var varuint := NetworkSchemas.varuint()
|
||||
var varbits := NetworkSchemas._varbits()
|
||||
var node_buffer := StreamPeerBuffer.new()
|
||||
|
||||
# Grab ticks
|
||||
var tick := buffer.get_u32()
|
||||
var snapshot := _Snapshot.new(tick)
|
||||
|
||||
while buffer.get_available_bytes() > 0:
|
||||
# Read header, including identity reference
|
||||
var idref := netref.decode(buffer) as _NetworkIdentityReference
|
||||
var node_data_size := varuint.decode(buffer) as int
|
||||
var changed_bits := varbits.decode(buffer) as _Bitset
|
||||
node_buffer.data_array = buffer.get_partial_data(node_data_size)[1]
|
||||
|
||||
# Resolve to identifier
|
||||
var identifier := _get_identity_server()._resolve_reference(peer, idref)
|
||||
if not identifier:
|
||||
# TODO(#563): Handle unknown IDs gracefully
|
||||
_logger.warning("Received unknown identity reference %s, skipping data", [idref])
|
||||
break
|
||||
var node := identifier.get_subject() as Node
|
||||
|
||||
# Read changed properties
|
||||
var node_props := properties.get_properties_of(node)
|
||||
for idx in changed_bits.get_set_indices():
|
||||
var property := node_props[idx]
|
||||
var value := _read_property(node, property, node_buffer)
|
||||
snapshot.set_property(node, property, value)
|
||||
snapshot.set_auth(node, is_auth)
|
||||
return snapshot
|
||||
@@ -0,0 +1 @@
|
||||
uid://b01d03njlfhh1
|
||||
@@ -0,0 +1,49 @@
|
||||
class_name _NetworkIdentifier
|
||||
extends RefCounted
|
||||
|
||||
var _subject: Object
|
||||
var _full_name: String
|
||||
var _ids: Dictionary = {} # peer to id
|
||||
var _local_id: int
|
||||
|
||||
signal on_id(peer: int, id: int)
|
||||
|
||||
func _init(subject: Object, full_name: String, local_id: int, local_peer: int):
|
||||
_subject = subject
|
||||
_full_name = full_name
|
||||
_local_id = local_id
|
||||
_ids[local_peer] = local_id
|
||||
|
||||
func has_id_for(peer: int) -> bool:
|
||||
return _ids.has(peer)
|
||||
|
||||
func get_id_for(peer: int) -> int:
|
||||
return _ids.get(peer, -1)
|
||||
|
||||
func set_id_for(peer: int, id: int) -> void:
|
||||
assert(not _ids.has(peer), "ID for peer #%d is already set!" % [peer])
|
||||
_ids[peer] = id
|
||||
on_id.emit(peer, id)
|
||||
|
||||
func get_local_id() -> int:
|
||||
return _local_id
|
||||
|
||||
func get_full_name() -> String:
|
||||
return _full_name
|
||||
|
||||
func get_subject() -> Object:
|
||||
return _subject
|
||||
|
||||
func get_known_peers() -> Array[int]:
|
||||
var result := [] as Array[int]
|
||||
result.assign(_ids.keys())
|
||||
return result
|
||||
|
||||
func reference_for(peer: int) -> _NetworkIdentityReference:
|
||||
if has_id_for(peer):
|
||||
return _NetworkIdentityReference.of_id(get_id_for(peer))
|
||||
else:
|
||||
return _NetworkIdentityReference.of_full_name(get_full_name())
|
||||
|
||||
func _to_string() -> String:
|
||||
return "NetworkIdentifier(%s)" % [_full_name]
|
||||
@@ -0,0 +1 @@
|
||||
uid://gnyygflpr78h
|
||||
@@ -0,0 +1,35 @@
|
||||
class_name _NetworkIdentityReference
|
||||
extends RefCounted
|
||||
|
||||
var _full_name: String = ""
|
||||
var _id: int = -1
|
||||
|
||||
static func of_full_name(full_name: String) -> _NetworkIdentityReference:
|
||||
var reference := _NetworkIdentityReference.new()
|
||||
reference._full_name = full_name
|
||||
return reference
|
||||
|
||||
static func of_id(id: int) -> _NetworkIdentityReference:
|
||||
var reference := _NetworkIdentityReference.new()
|
||||
reference._id = id
|
||||
return reference
|
||||
|
||||
func has_id() -> bool:
|
||||
return _id >= 0
|
||||
|
||||
func get_id() -> int:
|
||||
return _id
|
||||
|
||||
func get_full_name() -> String:
|
||||
return _full_name
|
||||
|
||||
func equals(other: Variant) -> bool:
|
||||
if other is _NetworkIdentityReference:
|
||||
return _full_name == other._full_name and _id == other._id
|
||||
return false
|
||||
|
||||
func _to_string() -> String:
|
||||
if has_id():
|
||||
return "NetworkIdentityReference#%d" % [_id]
|
||||
else:
|
||||
return "NetworkIdentityReference(%s)" % [_full_name]
|
||||
@@ -0,0 +1 @@
|
||||
uid://c6eyaau5r8g51
|
||||
@@ -0,0 +1,47 @@
|
||||
extends RefCounted
|
||||
class_name _ObjectSnapshot
|
||||
|
||||
# Represents snapshot data for a single object, by storing the object's values
|
||||
# for specified properties
|
||||
|
||||
var _object: Object
|
||||
var _is_auth: bool = false
|
||||
var _data: Dictionary = {}
|
||||
|
||||
func _init(p_object: Object) -> void:
|
||||
_object = p_object
|
||||
|
||||
func duplicate() -> _ObjectSnapshot:
|
||||
var result := _ObjectSnapshot.new(_object)
|
||||
result._is_auth = _is_auth
|
||||
result._data = _data.duplicate()
|
||||
return result
|
||||
|
||||
func get_value(property: NodePath, default: Variant = null) -> Variant:
|
||||
return _data.get(property, default)
|
||||
|
||||
func set_value(property: NodePath, value: Variant) -> void:
|
||||
_data[property] = value
|
||||
|
||||
func has_value(property: NodePath) -> bool:
|
||||
return _data.has(property)
|
||||
|
||||
func record_property(property: NodePath) -> void:
|
||||
set_value(property, _object.get_indexed(property))
|
||||
|
||||
func apply() -> void:
|
||||
for property in properties():
|
||||
var value := get_value(property)
|
||||
_object.set_indexed(property, value)
|
||||
|
||||
func is_auth() -> bool:
|
||||
return _is_auth
|
||||
|
||||
func set_auth(p_auth: bool) -> void:
|
||||
_is_auth = p_auth
|
||||
|
||||
func properties() -> Array:
|
||||
return _data.keys()
|
||||
|
||||
func _to_string() -> String:
|
||||
return "ObjectSnapshot(%s(%d), %s, %s)" % [_object, (_object as Node).get_multiplayer_authority() if _object is Node else -1, _is_auth, _data]
|
||||
@@ -0,0 +1 @@
|
||||
uid://bx4b8w54gleym
|
||||
@@ -0,0 +1,103 @@
|
||||
extends RefCounted
|
||||
class_name _PerObjectHistory
|
||||
|
||||
# Stores a per-object history of ObjectSnapshots
|
||||
# Each managed object has its own timeline of snapshots
|
||||
|
||||
var _history_size: int
|
||||
var _data := {} # Object to _HistoryBuffer
|
||||
|
||||
func _init(p_history_size: int):
|
||||
_history_size = p_history_size
|
||||
|
||||
func subjects() -> Array[Object]:
|
||||
var result := [] as Array[Object]
|
||||
result.assign(_data.keys())
|
||||
return result
|
||||
|
||||
func is_auth(tick: int, subject: Object) -> bool:
|
||||
if not _data.has(subject):
|
||||
return false
|
||||
|
||||
var history := _data[subject] as _HistoryBuffer
|
||||
if not history.has_at(tick):
|
||||
return false
|
||||
|
||||
var snapshot := history.get_at(tick) as _ObjectSnapshot
|
||||
return snapshot.is_auth()
|
||||
|
||||
func erase_subject(subject: Object) -> void:
|
||||
_data.erase(subject)
|
||||
|
||||
func ensure_snapshot(tick: int, subject: Object, carry_forward: bool) -> _ObjectSnapshot:
|
||||
var has_subject := _data.has(subject)
|
||||
if not _data.has(subject):
|
||||
_data[subject] = _HistoryBuffer.new(_history_size)
|
||||
|
||||
var history := _data[subject] as _HistoryBuffer
|
||||
var has_tick := history.has_at(tick)
|
||||
var has_latest := history.has_latest_at(tick)
|
||||
|
||||
if not history.has_at(tick):
|
||||
if not history.has_latest_at(tick):
|
||||
history.set_at(tick, _ObjectSnapshot.new(subject))
|
||||
elif carry_forward:
|
||||
history.set_at(tick, history.get_latest_at(tick).duplicate())
|
||||
else:
|
||||
history.set_at(tick, _ObjectSnapshot.new(subject))
|
||||
|
||||
assert(history.get_at(tick) != null, "Failed to ensure snapshot!")
|
||||
return history.get_at(tick) as _ObjectSnapshot
|
||||
|
||||
func get_latest_snapshot(tick: int, subject: Object) -> _ObjectSnapshot:
|
||||
if not _data.has(subject):
|
||||
return null
|
||||
|
||||
var history := _data[subject] as _HistoryBuffer
|
||||
if not history.has_latest_at(tick):
|
||||
return null
|
||||
|
||||
return history.get_latest_at(tick) as _ObjectSnapshot
|
||||
|
||||
func get_latest_tick(tick: int, subject: Object) -> int:
|
||||
if not _data.has(subject):
|
||||
return -1
|
||||
|
||||
var history := _data[subject] as _HistoryBuffer
|
||||
if not history.has_latest_at(tick):
|
||||
return -1
|
||||
|
||||
return history.get_latest_index_at(tick)
|
||||
|
||||
func set_property(tick: int, subject: Object, property: NodePath, value: Variant) -> void:
|
||||
if not _data.has(subject):
|
||||
_data[subject] = _HistoryBuffer.new(_history_size)
|
||||
|
||||
var history := _data[subject] as _HistoryBuffer
|
||||
if not history.has_at(tick):
|
||||
history.set_at(tick, _ObjectSnapshot.new(subject))
|
||||
|
||||
var snapshot := history.get_at(tick) as _ObjectSnapshot
|
||||
snapshot.set_value(property, value)
|
||||
|
||||
func has_property(tick: int, subject: Object, property: NodePath) -> bool:
|
||||
if not _data.has(subject):
|
||||
return false
|
||||
|
||||
var history := _data[subject] as _HistoryBuffer
|
||||
if not history.has_at(tick):
|
||||
return false
|
||||
|
||||
var snapshot := history.get_at(tick) as _ObjectSnapshot
|
||||
return snapshot.has_value(property)
|
||||
|
||||
func get_property(tick: int, subject: Object, property: NodePath, default: Variant = null) -> Variant:
|
||||
if not _data.has(subject):
|
||||
return default
|
||||
|
||||
var history := _data[subject] as _HistoryBuffer
|
||||
if not history.has_at(tick):
|
||||
return default
|
||||
|
||||
var snapshot := history.get_at(tick) as _ObjectSnapshot
|
||||
return snapshot.get_value(property, default)
|
||||
@@ -0,0 +1 @@
|
||||
uid://70ow466d8yqh
|
||||
@@ -0,0 +1,64 @@
|
||||
extends RefCounted
|
||||
class_name _PropertyPool
|
||||
|
||||
# Stores a set of properties, with each property belonging to a subject
|
||||
|
||||
var _properties_by_subject := {} # object to property array
|
||||
|
||||
static func of(entries: Array[Array]) -> _PropertyPool:
|
||||
var pool := _PropertyPool.new()
|
||||
|
||||
for entry in entries:
|
||||
var subject := entry[0] as Object
|
||||
var property := entry[1] as NodePath
|
||||
pool.add(subject, property)
|
||||
|
||||
return pool
|
||||
|
||||
func add(subject: Object, property: NodePath) -> void:
|
||||
if has(subject, property):
|
||||
return
|
||||
|
||||
if not _properties_by_subject.has(subject):
|
||||
_properties_by_subject[subject] = [property]
|
||||
else:
|
||||
_properties_by_subject[subject].append(property)
|
||||
|
||||
func has(subject: Object, property: NodePath) -> bool:
|
||||
return (_properties_by_subject.get(subject, []) as Array).has(property)
|
||||
|
||||
func erase(subject: Object, property: NodePath) -> void:
|
||||
if not _properties_by_subject.has(subject):
|
||||
return
|
||||
|
||||
var props := _properties_by_subject[subject] as Array
|
||||
props.erase(property)
|
||||
|
||||
if props.is_empty():
|
||||
_properties_by_subject.erase(subject)
|
||||
|
||||
func erase_subject(subject: Object) -> void:
|
||||
_properties_by_subject.erase(subject)
|
||||
|
||||
func clear() -> void:
|
||||
_properties_by_subject.clear()
|
||||
|
||||
func get_properties_of(subject: Object) -> Array[NodePath]:
|
||||
var properties := [] as Array[NodePath]
|
||||
properties.assign(_properties_by_subject.get(subject, []))
|
||||
return properties
|
||||
|
||||
func get_subjects() -> Array[Object]:
|
||||
var subjects := [] as Array[Object]
|
||||
subjects.assign(_properties_by_subject.keys())
|
||||
return subjects
|
||||
|
||||
func is_empty() -> bool:
|
||||
return _properties_by_subject.is_empty()
|
||||
|
||||
func set_from_paths(root: Node, paths: Array[String]) -> void:
|
||||
clear()
|
||||
|
||||
for path in paths:
|
||||
var prop := PropertyEntry.parse(root, path)
|
||||
add(prop.node, prop.property)
|
||||
@@ -0,0 +1 @@
|
||||
uid://j4luqnbeo8jv
|
||||
@@ -0,0 +1,171 @@
|
||||
extends RefCounted
|
||||
class_name _Snapshot
|
||||
|
||||
# Stores property values of multiple subjects, recorded for a specific tick
|
||||
|
||||
var tick: int
|
||||
var _data := {} # object to (property to variant)
|
||||
var _auth_subjects := _Set.new()
|
||||
|
||||
static func make_patch(from: _Snapshot, to: _Snapshot, tick: int = to.tick) -> _Snapshot:
|
||||
var patch := _Snapshot.new(tick)
|
||||
|
||||
for subject in from._data:
|
||||
# Target has no knowledge of subject, don't patch
|
||||
if not to._data.has(subject):
|
||||
continue
|
||||
# Only patch to auth subjects
|
||||
if not to.is_auth(subject):
|
||||
continue
|
||||
|
||||
for property in to._data[subject]:
|
||||
# Target snapshot has different value, patch it
|
||||
if from.get_property(subject, property) != to.get_property(subject, property):
|
||||
patch.set_property(subject, property, to.get_property(subject, property))
|
||||
patch.set_auth(subject, to.is_auth(subject))
|
||||
|
||||
return patch
|
||||
|
||||
# Each entry should be [subject, property, value]
|
||||
static func of(tick: int, entries: Array[Array], auth_subjects: Array[Object]) -> _Snapshot:
|
||||
var snapshot := _Snapshot.new(tick)
|
||||
for entry in entries:
|
||||
var subject := entry[0] as Object
|
||||
var property := entry[1] as NodePath
|
||||
var value := entry[2] as Variant
|
||||
|
||||
snapshot.set_property(subject, property, value)
|
||||
|
||||
for subject in auth_subjects:
|
||||
snapshot.set_auth(subject, true)
|
||||
|
||||
return snapshot
|
||||
|
||||
func _init(p_tick: int):
|
||||
tick = p_tick
|
||||
|
||||
func duplicate() -> _Snapshot:
|
||||
var result := _Snapshot.new(tick)
|
||||
result._data = _data.duplicate(true)
|
||||
result._auth_subjects = _auth_subjects.duplicate()
|
||||
return result
|
||||
|
||||
func set_auth(subject: Object, is_auth: bool) -> void:
|
||||
if is_auth:
|
||||
_auth_subjects.add(subject)
|
||||
else:
|
||||
_auth_subjects.erase(subject)
|
||||
|
||||
func set_property(subject: Object, property: NodePath, value: Variant) -> void:
|
||||
if not _data.has(subject):
|
||||
_data[subject] = { property: value }
|
||||
else:
|
||||
_data[subject][property] = value
|
||||
|
||||
func record_property(subject: Object, property: NodePath) -> void:
|
||||
var value := subject.get_indexed(property)
|
||||
set_property(subject, property, value)
|
||||
|
||||
func get_property(subject: Object, property: NodePath) -> Variant:
|
||||
return _data.get(subject, {}).get(property)
|
||||
|
||||
func has_property(subject: Object, property: NodePath) -> bool:
|
||||
if not _data.has(subject):
|
||||
return false
|
||||
if not _data[subject].has(property):
|
||||
return false
|
||||
return true
|
||||
|
||||
func merge(snapshot: _Snapshot) -> bool:
|
||||
var has_changed := false
|
||||
|
||||
for subject in snapshot._data:
|
||||
if not _data.has(subject):
|
||||
# We have no data of the subject, copy all
|
||||
_data[subject] = snapshot._data[subject].duplicate()
|
||||
set_auth(subject, snapshot.is_auth(subject))
|
||||
has_changed = true
|
||||
continue
|
||||
|
||||
if snapshot.is_auth(subject) or not is_auth(subject):
|
||||
var own_props := _data[subject] as Dictionary
|
||||
var their_props := snapshot._data[subject] as Dictionary
|
||||
|
||||
has_changed = has_changed or own_props != their_props
|
||||
|
||||
own_props.merge(their_props, true)
|
||||
set_auth(subject, snapshot.is_auth(subject))
|
||||
|
||||
return has_changed
|
||||
|
||||
func apply() -> void:
|
||||
for subject in _data:
|
||||
for property in _data[subject]:
|
||||
var value = _data[subject][property]
|
||||
(subject as Object).set_indexed(property, value)
|
||||
|
||||
func sanitize(sender: int) -> void:
|
||||
var invalid_subjects := []
|
||||
for subject in _data:
|
||||
if subject is Node:
|
||||
if subject.get_multiplayer_authority() != sender:
|
||||
invalid_subjects.append(subject)
|
||||
|
||||
for subject in invalid_subjects:
|
||||
_data.erase(invalid_subjects)
|
||||
|
||||
func has_subject(subject: Object, require_auth: bool = false) -> bool:
|
||||
if not _data.has(subject):
|
||||
return false
|
||||
if require_auth and not is_auth(subject):
|
||||
return false
|
||||
return true
|
||||
|
||||
func has_subjects(subjects: Array, require_auth: bool = false) -> bool:
|
||||
for subject in subjects:
|
||||
if not has_subject(subject, require_auth):
|
||||
return false
|
||||
return true
|
||||
|
||||
func get_subjects() -> Array:
|
||||
return _data.keys()
|
||||
|
||||
func get_auth_subjects() -> Array:
|
||||
return _auth_subjects.values()
|
||||
|
||||
func get_subject_properties(subject: Object) -> Array:
|
||||
return _data.get(subject, {}).keys()
|
||||
|
||||
func is_empty() -> bool:
|
||||
return _data.is_empty()
|
||||
|
||||
func size() -> int:
|
||||
var result := 0
|
||||
for subject in _data:
|
||||
result += (_data[subject] as Dictionary).size()
|
||||
return result
|
||||
|
||||
func is_auth(subject: Object) -> bool:
|
||||
return _auth_subjects.has(subject)
|
||||
|
||||
func equals(other) -> bool:
|
||||
if other is _Snapshot:
|
||||
return tick == other.tick and _data == other._data and _auth_subjects.equals(other._auth_subjects)
|
||||
else:
|
||||
return false
|
||||
|
||||
func _to_string() -> String:
|
||||
var result := "_Snapshot(#%d" % [tick]
|
||||
for subject in _data:
|
||||
for property in _data[subject]:
|
||||
var value = _data[subject][property]
|
||||
result += ", %s:%s(%s): %s" % [subject, property, is_auth(subject), value]
|
||||
result += ")"
|
||||
return result
|
||||
|
||||
func _to_vest():
|
||||
return {
|
||||
"tick": tick,
|
||||
"data": _data,
|
||||
"auth_subjects": _auth_subjects
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://b76plodlwp2u6
|
||||
@@ -0,0 +1 @@
|
||||
uid://ch0j0cclkpey6
|
||||
@@ -0,0 +1,179 @@
|
||||
extends Node
|
||||
class_name _NetworkCommandServer
|
||||
|
||||
## Transmits commands over the network
|
||||
##
|
||||
## Commands are a simpler, lightweight alternative to RPCs. Commands consist of
|
||||
## a single byte for ID, and the raw binary data. The ID lets the receiving peer
|
||||
## decide what to execute, with the binary data serving as the input.
|
||||
## [br][br]
|
||||
## Being a simpler construct makes commands a good fit for regular, fundamental
|
||||
## operations.
|
||||
## [br][br]
|
||||
## Commands are, by default, transmitted over regular RPCs. To use less data,
|
||||
## commands can also be transmitted as raw packets, using
|
||||
## [method SceneMultiplayer.send_bytes]. This is an opt-in feature - if the game
|
||||
## is already using [method SceneMultiplayer.send_bytes], it needs to be aware
|
||||
## of commands, and must check each packet whether it's a command or one of its
|
||||
## own packets. To check if a packet is a command, use [method
|
||||
## is_command_packet].
|
||||
|
||||
var _packet_prefix := PackedByteArray([0, 78, 70]) # "\0nf"
|
||||
var _next_idx := 0
|
||||
var _rpc_transport := _RPCTransport.new()
|
||||
var _packet_transport := _PacketTransport.new(_packet_prefix)
|
||||
var _commands := {} # id to `Command`
|
||||
|
||||
var _use_raw := ProjectSettings.get_setting("netfox/general/use_raw_commands", false) as bool
|
||||
|
||||
static var _logger := NetfoxLogger._for_netfox("NetworkCommandServer")
|
||||
|
||||
func _ready():
|
||||
add_child(_rpc_transport, true)
|
||||
add_child(_packet_transport, true)
|
||||
|
||||
_rpc_transport.on_receive.connect(_handle_command)
|
||||
_packet_transport.on_receive.connect(_handle_command)
|
||||
|
||||
## Register a command at the next available ID
|
||||
func register_command(handler: Callable, mode: MultiplayerPeer.TransferMode = MultiplayerPeer.TRANSFER_MODE_RELIABLE, channel: int = 0) -> Command:
|
||||
var idx := _next_idx
|
||||
_next_idx += 1
|
||||
return register_command_at(idx, handler)
|
||||
|
||||
## Register a command at a specific index
|
||||
## [br][br]
|
||||
## A specific ID should only be registered once. Doing otherwise will trigger an
|
||||
## assert in the editor, but will overwrite the previous command in release.
|
||||
func register_command_at(idx: int, handler: Callable, mode: MultiplayerPeer.TransferMode = MultiplayerPeer.TRANSFER_MODE_RELIABLE, channel: int = 0) -> Command:
|
||||
assert(not _commands.has(idx), "Command #%d is already taken!" % idx)
|
||||
var command := Command.new(self, idx, handler, mode, channel)
|
||||
_commands[idx] = command
|
||||
|
||||
_next_idx = maxi(_next_idx, idx + 1)
|
||||
|
||||
return command
|
||||
|
||||
## Send a command with index and data
|
||||
func send_command(idx: int, data: PackedByteArray, target_peer: int = 0, mode: MultiplayerPeer.TransferMode = MultiplayerPeer.TRANSFER_MODE_RELIABLE, channel: int = 0) -> void:
|
||||
if _use_raw:
|
||||
_packet_transport.send(idx, data, target_peer, mode, channel)
|
||||
else:
|
||||
_rpc_transport.send(idx, data, target_peer, mode, channel)
|
||||
|
||||
## Return true if [param packet] is a command packet
|
||||
## [br][br]
|
||||
## Always returns [code]true[/code] if RPCs are used for transmitting commands.
|
||||
func is_command_packet(packet: PackedByteArray) -> bool:
|
||||
if not _use_raw:
|
||||
return true
|
||||
return _packet_transport.is_command_packet(packet)
|
||||
|
||||
## Return the prefix bytes for command packets
|
||||
## [br][br]
|
||||
## Can be used to avoid conflicts between command packets and game packets.
|
||||
func get_command_packet_prefix() -> PackedByteArray:
|
||||
return _packet_prefix
|
||||
|
||||
func _handle_command(sender: int, idx: int, data: PackedByteArray) -> void:
|
||||
var command := _commands.get(idx) as Command
|
||||
if not command:
|
||||
_logger.error("Received unknown command #%d!", [idx])
|
||||
return
|
||||
command._handle(sender, data)
|
||||
|
||||
## Networked command
|
||||
##
|
||||
## Provides a convenient interface for sending and managing networked commands.
|
||||
## [br][br]
|
||||
## Should not be instantiated manually.
|
||||
class Command:
|
||||
var _command_server: _NetworkCommandServer
|
||||
|
||||
var _idx: int
|
||||
var _handler: Callable
|
||||
var _mode: MultiplayerPeer.TransferMode
|
||||
var _channel: int
|
||||
|
||||
func _init(p_command_server: _NetworkCommandServer, p_idx: int, p_handler: Callable, p_mode: MultiplayerPeer.TransferMode, p_channel: int):
|
||||
_command_server = p_command_server
|
||||
_idx = p_idx
|
||||
_handler = p_handler
|
||||
_mode = p_mode
|
||||
_channel = p_channel
|
||||
|
||||
## Send command
|
||||
func send(data: PackedByteArray, target_peer: int = 0) -> void:
|
||||
_command_server.send_command(_idx, data, target_peer, _mode, _channel)
|
||||
|
||||
func _handle(sender: int, data: PackedByteArray) -> void:
|
||||
_handler.call(sender, data)
|
||||
|
||||
class _Transport extends Node:
|
||||
signal on_receive(idx: int, data: PackedByteArray)
|
||||
|
||||
func send(idx: int, data: PackedByteArray, target_peer: int, mode: MultiplayerPeer.TransferMode, channel: int) -> void:
|
||||
pass
|
||||
|
||||
class _PacketTransport extends _Transport:
|
||||
var _packet_prefix: PackedByteArray
|
||||
|
||||
func _init(p_packet_prefix: PackedByteArray):
|
||||
_packet_prefix = p_packet_prefix
|
||||
|
||||
func _ready():
|
||||
(multiplayer as SceneMultiplayer).peer_packet.connect(_handle_packet)
|
||||
|
||||
func send(idx: int, data: PackedByteArray, target_peer: int, mode: MultiplayerPeer.TransferMode, channel: int) -> void:
|
||||
var buffer := StreamPeerBuffer.new()
|
||||
buffer.put_data(_packet_prefix)
|
||||
buffer.put_u8(idx)
|
||||
buffer.put_data(data)
|
||||
|
||||
(multiplayer as SceneMultiplayer).send_bytes(buffer.data_array, target_peer, mode, channel)
|
||||
|
||||
func is_command_packet(packet: PackedByteArray) -> bool:
|
||||
if packet.size() < _packet_prefix.size():
|
||||
return false
|
||||
|
||||
for i in _packet_prefix.size():
|
||||
if packet[i] != _packet_prefix[i]:
|
||||
return false
|
||||
return true
|
||||
|
||||
func _handle_packet(peer: int, packet: PackedByteArray) -> void:
|
||||
var buffer := StreamPeerBuffer.new()
|
||||
buffer.data_array = packet
|
||||
|
||||
# Check header
|
||||
if not is_command_packet(packet):
|
||||
return
|
||||
|
||||
# Grab data
|
||||
buffer.seek(_packet_prefix.size())
|
||||
var idx := buffer.get_u8()
|
||||
var data := buffer.get_partial_data(buffer.get_available_bytes())[1] as PackedByteArray
|
||||
|
||||
on_receive.emit(peer, idx, data)
|
||||
|
||||
class _RPCTransport extends _Transport:
|
||||
func send(idx: int, data: PackedByteArray, target_peer: int, mode: MultiplayerPeer.TransferMode, _channel: int) -> void:
|
||||
match mode:
|
||||
MultiplayerPeer.TRANSFER_MODE_UNRELIABLE: _submit_unreliable.rpc_id(target_peer, idx, data)
|
||||
MultiplayerPeer.TRANSFER_MODE_UNRELIABLE_ORDERED: _submit_unreliable_ordered.rpc_id(target_peer, idx, data)
|
||||
MultiplayerPeer.TRANSFER_MODE_RELIABLE: _submit_reliable.rpc_id(target_peer, idx, data)
|
||||
|
||||
@rpc("any_peer", "call_remote", "unreliable")
|
||||
func _submit_unreliable(idx: int, data: PackedByteArray):
|
||||
var sender := multiplayer.get_remote_sender_id()
|
||||
on_receive.emit(sender, idx, data)
|
||||
|
||||
@rpc("any_peer", "call_remote", "unreliable_ordered")
|
||||
func _submit_unreliable_ordered(idx: int, data: PackedByteArray):
|
||||
var sender := multiplayer.get_remote_sender_id()
|
||||
on_receive.emit(sender, idx, data)
|
||||
|
||||
@rpc("any_peer", "call_remote", "reliable")
|
||||
func _submit_reliable(idx: int, data: PackedByteArray):
|
||||
var sender := multiplayer.get_remote_sender_id()
|
||||
on_receive.emit(sender, idx, data)
|
||||
@@ -0,0 +1 @@
|
||||
uid://mkgjb8gdl0fp
|
||||
@@ -0,0 +1,273 @@
|
||||
extends Node
|
||||
class_name _NetworkHistoryServer
|
||||
|
||||
## Tracks the history of objects' properties
|
||||
##
|
||||
## Specifically, history is stored for rollback state properties, rollback input
|
||||
## properties, and synchronized state properties.
|
||||
## [br][br]
|
||||
## Keeping history lets rollback restore earlier game states for resimulation,
|
||||
## and enables [_NetworkSynchronizationServer] to send diff states by comparing
|
||||
## against historical data.
|
||||
|
||||
var _rb_input_properties := _PropertyPool.new()
|
||||
var _rb_state_properties := _PropertyPool.new()
|
||||
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
|
||||
|
||||
# Source of truth for history
|
||||
var _rb_input_history := _PerObjectHistory.new(_rb_history_size)
|
||||
var _rb_state_history := _PerObjectHistory.new(_rb_history_size)
|
||||
var _sync_history := _PerObjectHistory.new(_sync_history_size)
|
||||
|
||||
# Cached snapshots for syncing
|
||||
var _rb_input_snapshots := _HistoryBuffer.new(_rb_history_size)
|
||||
var _rb_state_snapshots := _HistoryBuffer.new(_rb_history_size)
|
||||
var _sync_state_snapshots := _HistoryBuffer.new(_sync_history_size)
|
||||
|
||||
static var _logger := NetfoxLogger._for_netfox("NetworkHistoryServer")
|
||||
|
||||
## Register a rollback state property
|
||||
func register_rollback_state(node: Node, property: NodePath) -> void:
|
||||
_rb_state_properties.add(node, property)
|
||||
|
||||
## Deregister a rollback state property
|
||||
func deregister_rollback_state(node: Node, property: NodePath) -> void:
|
||||
_rb_state_properties.erase(node, property)
|
||||
|
||||
## Register a rollback input property
|
||||
func register_rollback_input(node: Node, property: NodePath) -> void:
|
||||
_rb_input_properties.add(node, property)
|
||||
|
||||
## Deregister a rollback input property
|
||||
func deregister_rollback_input(node: Node, property: NodePath) -> void:
|
||||
_rb_input_properties.erase(node, property)
|
||||
|
||||
## Register a synchronized state property
|
||||
func register_sync_state(node: Node, property: NodePath) -> void:
|
||||
_sync_state_properties.add(node, property)
|
||||
|
||||
## Deregister a synchronized state property
|
||||
func deregister_sync_state(node: Node, property: NodePath) -> void:
|
||||
_sync_state_properties.erase(node, property)
|
||||
|
||||
## Deregister a node, no longer tracking any property it had registered using
|
||||
## any of the [code]register_*()[/code] methods
|
||||
func deregister(node: Node) -> void:
|
||||
_rb_state_properties.erase_subject(node)
|
||||
_rb_input_properties.erase_subject(node)
|
||||
_sync_state_properties.erase_subject(node)
|
||||
|
||||
_rb_state_history.erase_subject(node)
|
||||
_rb_input_history.erase_subject(node)
|
||||
_sync_history.erase_subject(node)
|
||||
|
||||
## Return the latest tick where any of the [param]subjects[/param] had rollback
|
||||
## state data available
|
||||
func get_latest_state_tick_for(subjects: Array, tick: int) -> int:
|
||||
return _get_latest_for(subjects, tick, _rb_state_history)
|
||||
|
||||
## Return how old is the latest rollback state data for any of the [param]
|
||||
## subjects[/param], in ticks
|
||||
func get_state_age_for(subjects: Array, tick: int) -> int:
|
||||
var latest_state := get_latest_state_tick_for(subjects, tick)
|
||||
if latest_state < 0:
|
||||
return -1
|
||||
else:
|
||||
return tick - latest_state
|
||||
|
||||
## Return the latest tick where any of the [param]subjects[/param] had rollback
|
||||
## input data available
|
||||
func get_latest_input_for(subjects: Array, tick: int) -> int:
|
||||
return _get_latest_for(subjects, tick, _rb_input_history)
|
||||
|
||||
## Return how old is the latest rollback input data for any of the [param]
|
||||
## subjects[/param], in ticks
|
||||
func get_input_age_for(subjects: Array, tick: int) -> int:
|
||||
var latest_input := get_latest_input_for(subjects, tick)
|
||||
if latest_input < 0:
|
||||
return -1
|
||||
else:
|
||||
return tick - latest_input
|
||||
|
||||
func _record_rollback_input(tick: int) -> void:
|
||||
_record(tick, _rb_input_history, _rb_input_snapshots, _rb_input_properties, true, func(subject: Node):
|
||||
return subject.is_multiplayer_authority()
|
||||
)
|
||||
|
||||
func _record_rollback_state(tick: int) -> void:
|
||||
var input_snapshot := _get_rollback_input_snapshot(tick - 1)
|
||||
|
||||
_record(tick, _rb_state_history, _rb_state_snapshots, _rb_state_properties, false, func(subject: Node):
|
||||
if not subject.is_multiplayer_authority():
|
||||
return false
|
||||
if RollbackSimulationServer._is_predicting(input_snapshot, subject):
|
||||
return false
|
||||
return true
|
||||
)
|
||||
|
||||
func _record_sync_state(tick: int) -> void:
|
||||
_record(tick, _sync_history, _sync_state_snapshots, _sync_state_properties, true, func(subject: Node):
|
||||
return subject.is_multiplayer_authority()
|
||||
)
|
||||
|
||||
func _restore_rollback_input(tick: int) -> bool:
|
||||
return _restore_latest(tick, _rb_input_history)
|
||||
|
||||
func _restore_rollback_state(tick: int) -> bool:
|
||||
return _restore_latest(tick, _rb_state_history)
|
||||
|
||||
func _restore_synchronizer_state(tick: int) -> bool:
|
||||
return _restore_latest(tick, _sync_history)
|
||||
|
||||
func _get_rollback_input_snapshot(tick: int) -> _Snapshot:
|
||||
return _rb_input_snapshots.get_at(tick)
|
||||
|
||||
func _get_rollback_state_snapshot(tick: int) -> _Snapshot:
|
||||
return _rb_state_snapshots.get_at(tick)
|
||||
|
||||
func _get_synchronizer_state_snapshot(tick: int) -> _Snapshot:
|
||||
return _sync_state_snapshots.get_at(tick)
|
||||
|
||||
func _merge_rollback_input(snapshot: _Snapshot) -> bool:
|
||||
_merge_snapshot(snapshot, _rb_input_snapshots, true)
|
||||
return _merge_history(snapshot, _rb_input_history, true)
|
||||
|
||||
func _merge_rollback_state(snapshot: _Snapshot) -> bool:
|
||||
_merge_snapshot(snapshot, _rb_state_snapshots, true)
|
||||
return _merge_history(snapshot, _rb_state_history)
|
||||
|
||||
func _merge_synchronizer_state(snapshot: _Snapshot) -> bool:
|
||||
_merge_snapshot(snapshot, _sync_state_snapshots, true)
|
||||
return _merge_history(snapshot, _sync_history)
|
||||
|
||||
func _record(tick: int, history: _PerObjectHistory, snapshots: _HistoryBuffer, property_pool: _PropertyPool, only_auth: bool, auth_filter: Callable) -> void:
|
||||
var snapshot := snapshots.get_at(tick, _Snapshot.new(tick)) as _Snapshot
|
||||
if not snapshots.has_at(tick):
|
||||
snapshots.set_at(tick, snapshot)
|
||||
|
||||
for subject in property_pool.get_subjects():
|
||||
assert(subject is Node, "Only nodes supported for now!")
|
||||
|
||||
var is_auth := auth_filter.call(subject)
|
||||
|
||||
if only_auth and not is_auth:
|
||||
continue
|
||||
if not is_auth and history.is_auth(tick, subject):
|
||||
continue
|
||||
|
||||
var subject_snapshot := history.ensure_snapshot(tick, subject, false) #!!
|
||||
assert(not property_pool.get_properties_of(subject).is_empty(), "Subject present in property pool without properties! Please report a bug!")
|
||||
for property in property_pool.get_properties_of(subject):
|
||||
subject_snapshot.record_property(property)
|
||||
snapshot.record_property(subject, property)
|
||||
snapshot.set_auth(subject, is_auth)
|
||||
subject_snapshot.set_auth(is_auth)
|
||||
|
||||
match history:
|
||||
_rb_input_history:
|
||||
_logger.trace("Recorded input @%d: %s", [tick, snapshot])
|
||||
_rb_state_history:
|
||||
_logger.trace("Recorded state @%d: %s", [tick, snapshot])
|
||||
|
||||
func _restore_latest(tick: int, history: _PerObjectHistory) -> bool:
|
||||
var any_applied := false
|
||||
|
||||
for subject in history.subjects():
|
||||
# Grab latest snapshot up to tick
|
||||
var snapshot := history.get_latest_snapshot(tick, subject)
|
||||
|
||||
# Apply if any
|
||||
if snapshot:
|
||||
snapshot.apply()
|
||||
any_applied = true
|
||||
|
||||
match history:
|
||||
_rb_input_history: _logger.trace("Restored input @%d: %s", [tick, snapshot])
|
||||
_rb_state_history: _logger.trace("Restored state @%d: %s", [tick, snapshot])
|
||||
|
||||
return any_applied
|
||||
|
||||
func _merge_snapshot(snapshot: _Snapshot, snapshots: _HistoryBuffer, reverse: bool = false) -> bool:
|
||||
var tick := snapshot.tick
|
||||
|
||||
if not snapshots.has_at(snapshot.tick):
|
||||
snapshots.set_at(tick, snapshot)
|
||||
return true
|
||||
|
||||
var original_snapshot := snapshots.get_at(tick) as _Snapshot
|
||||
if reverse:
|
||||
var original_subjects := original_snapshot.get_auth_subjects()
|
||||
var incoming_subjects := snapshot.get_auth_subjects()
|
||||
|
||||
# Merge the original snapshot on top of the incoming
|
||||
# This prevents players from changing history, e.g. rewrite their past
|
||||
# inputs
|
||||
snapshots.set_at(tick, snapshot)
|
||||
snapshot.merge(original_snapshot)
|
||||
|
||||
# Only return true if we've received inputs for a new node
|
||||
return incoming_subjects.any(func(it): return not original_subjects.has(it))
|
||||
else:
|
||||
return original_snapshot.merge(snapshot)
|
||||
|
||||
func _merge_history(snapshot: _Snapshot, history: _PerObjectHistory, reverse: bool = false) -> bool:
|
||||
var tick := snapshot.tick
|
||||
var has_updated := false
|
||||
|
||||
if tick < NetworkRollback.history_start:
|
||||
_logger.warning("Snapshot being merged is too old! (@%d)", [tick])
|
||||
return false
|
||||
|
||||
for subject in snapshot.get_subjects():
|
||||
var object_snapshot := history.ensure_snapshot(tick, subject, not reverse)
|
||||
|
||||
# Never overwrite auth data
|
||||
if object_snapshot.is_auth() and not snapshot.is_auth(subject):
|
||||
continue
|
||||
|
||||
for property in snapshot.get_subject_properties(subject):
|
||||
# If merging in reverse, don't update anything that we already have
|
||||
# a value for - only accept previously unknown property values
|
||||
if reverse and object_snapshot.has_value(property):
|
||||
continue
|
||||
|
||||
var original_value := object_snapshot.get_value(property)
|
||||
var new_value := snapshot.get_property(subject, property)
|
||||
|
||||
object_snapshot.set_value(property, new_value)
|
||||
if not has_updated and original_value != new_value:
|
||||
has_updated = true
|
||||
object_snapshot.set_auth(snapshot.is_auth(subject))
|
||||
|
||||
return has_updated
|
||||
|
||||
func _get_latest_for(subjects: Array, tick: int, history: _PerObjectHistory) -> int:
|
||||
var latest := -1
|
||||
|
||||
for subject in subjects:
|
||||
var subject_latest := history.get_latest_tick(tick, subject)
|
||||
if subject_latest < 0:
|
||||
continue
|
||||
|
||||
# Should we track `mini()` instead?
|
||||
latest = maxi(latest, subject_latest)
|
||||
|
||||
return latest
|
||||
|
||||
func _get_earliest_for(subjects: Array, tick: int, history: _PerObjectHistory) -> int:
|
||||
var earliest := -1
|
||||
|
||||
for subject in subjects:
|
||||
var subject_latest := history.get_latest_tick(tick, subject)
|
||||
if subject_latest < 0:
|
||||
return -1
|
||||
|
||||
if earliest < 0:
|
||||
earliest = earliest
|
||||
else:
|
||||
earliest = mini(earliest, subject_latest)
|
||||
|
||||
return earliest
|
||||
@@ -0,0 +1 @@
|
||||
uid://c554fbsy6yos3
|
||||
@@ -0,0 +1,190 @@
|
||||
extends Node
|
||||
class_name _NetworkIdentityServer
|
||||
|
||||
## Tracks identities for use over the network
|
||||
##
|
||||
## Internally, netfox sends packets that refer to specific nodes, e.g. to
|
||||
## synchronize their state. Nodes are referenced based on their path in the
|
||||
## scene tree. To save on bandwidth, node paths are replaced with numeric IDs
|
||||
## when possible.
|
||||
## [br][br]
|
||||
## This class manages these numerical IDs and their broadcast among peers.
|
||||
## [br][br]
|
||||
## Each node that is to be referred to over the network must be registered using
|
||||
## [method register_node]. It is best to deregister freed nodes using [method
|
||||
## deregister_node].
|
||||
## [br][br]
|
||||
## Node registration and deregistration are done automatically by the high-level
|
||||
## nodes ( like RollbackSynchronizer ), and usually doesn't need to be done
|
||||
## manually.
|
||||
|
||||
var _command_server: _NetworkCommandServer
|
||||
|
||||
var _next_id := 0
|
||||
var _identifiers := {} # object to NetworkIdentifier
|
||||
var _push_queue := {} # peer to (full name to id)
|
||||
|
||||
var _identifier_by_name := {} # full name to NetworkIdentifier
|
||||
var _identifier_by_id := {} # peer to (id to NetworkIdentifier)
|
||||
|
||||
var _cmd_ids: _NetworkCommandServer.Command
|
||||
|
||||
static var _logger := NetfoxLogger._for_netfox("NetworkIdentityServer")
|
||||
|
||||
func _init(p_command_server: _NetworkCommandServer = null):
|
||||
_command_server = p_command_server
|
||||
|
||||
func _ready():
|
||||
if not _command_server:
|
||||
_command_server = NetworkCommandServer
|
||||
|
||||
_cmd_ids = _command_server.register_command(_handle_ids, MultiplayerPeer.TRANSFER_MODE_RELIABLE)
|
||||
|
||||
# TODO(#561): Handle peer disconnect by clearing up data
|
||||
|
||||
## Register a node
|
||||
## [br][br]
|
||||
## Once a node is registered, it can be referred to over the network. The same
|
||||
## node must be registered with the same node path on all peers.
|
||||
func register_node(node: Node) -> void:
|
||||
if not node.is_inside_tree():
|
||||
_logger.error("Can't register node %s that is not inside tree!", [node])
|
||||
return
|
||||
_register(node, node.get_path())
|
||||
|
||||
## Deregister a node, freeing all identity data associated with it
|
||||
func deregister_node(node: Node) -> void:
|
||||
_deregister(node)
|
||||
|
||||
## Clear all identities
|
||||
func clear() -> void:
|
||||
_identifiers.clear()
|
||||
_identifier_by_name.clear()
|
||||
_identifier_by_id.clear()
|
||||
_next_id = 0
|
||||
|
||||
## Queue sending the numeric ID of [param node] to [param peer]
|
||||
## [br][br]
|
||||
## This happens automatically when the [param node] is first referred to in a
|
||||
## packet. This method allows frontloading that, at a more feasible moment.
|
||||
## [br][br]
|
||||
## This method does not send any data. To force sending identifiers, use [method
|
||||
## flush_queue].
|
||||
func queue_identifier_for(node: Node, peer: int) -> bool:
|
||||
var identifier := _get_identifier_of(node)
|
||||
if not identifier:
|
||||
return false
|
||||
_queue_for(identifier, peer)
|
||||
return true
|
||||
|
||||
## Broadcast all identities queued for synchronization
|
||||
## [br][br]
|
||||
## Flushed automatically by default
|
||||
func flush_queue() -> void:
|
||||
for peer in _push_queue:
|
||||
_cmd_ids.send(_serialize_ids(_push_queue[peer]), peer)
|
||||
_push_queue.clear()
|
||||
|
||||
func _get_identifier_of(what: Object) -> _NetworkIdentifier:
|
||||
return _identifiers.get(what)
|
||||
|
||||
func _resolve_reference(peer: int, identity_reference: _NetworkIdentityReference, allow_queue: bool = true) -> _NetworkIdentifier:
|
||||
if identity_reference.has_id():
|
||||
return _get_identifier_by_id(peer, identity_reference.get_id())
|
||||
else:
|
||||
var identifier := _get_identifier_by_name(identity_reference.get_full_name())
|
||||
if allow_queue and identifier:
|
||||
_queue_for(identifier, peer)
|
||||
return identifier
|
||||
|
||||
func _queue_for(identifier: _NetworkIdentifier, peer: int) -> void:
|
||||
if not _push_queue.has(peer):
|
||||
_push_queue[peer] = { identifier.get_full_name(): identifier.get_id_for(peer) }
|
||||
else:
|
||||
_push_queue[peer][identifier.get_full_name()] = identifier.get_id_for(peer)
|
||||
|
||||
func _register(what: Object, path: String) -> void:
|
||||
if _identifiers.has(what):
|
||||
return
|
||||
|
||||
var identifier := _NetworkIdentifier.new(what, path, _make_id(), multiplayer.get_unique_id())
|
||||
_identifiers[what] = identifier
|
||||
_identifier_by_name[identifier.get_full_name()] = identifier
|
||||
|
||||
identifier.on_id.connect(func(peer: int, id: int): _update_id_cache(identifier, peer, id))
|
||||
_update_id_cache(identifier, multiplayer.get_unique_id(), identifier.get_local_id())
|
||||
|
||||
func _deregister(what: Object) -> void:
|
||||
if not _identifiers.has(what):
|
||||
return
|
||||
|
||||
var identifier := _identifiers[what] as _NetworkIdentifier
|
||||
_identifiers.erase(what)
|
||||
_identifier_by_name.erase(identifier.get_full_name())
|
||||
_erase_from_id_cache(identifier)
|
||||
|
||||
|
||||
func _get_identifier_by_name(full_name: String) -> _NetworkIdentifier:
|
||||
return _identifier_by_name.get(full_name)
|
||||
|
||||
func _get_identifier_by_id(peer: int, id: int) -> _NetworkIdentifier:
|
||||
return _identifier_by_id.get(peer, {}).get(id, null)
|
||||
|
||||
func _make_id() -> int:
|
||||
_next_id += 1
|
||||
return _next_id
|
||||
|
||||
func _update_id_cache(identifier: _NetworkIdentifier, peer: int, id: int) -> void:
|
||||
if not _identifier_by_id.has(peer):
|
||||
_identifier_by_id[peer] = { id: identifier }
|
||||
else:
|
||||
_identifier_by_id[peer][id] = identifier
|
||||
|
||||
func _erase_from_id_cache(identifier: _NetworkIdentifier) -> void:
|
||||
for peer in identifier.get_known_peers():
|
||||
if not _identifier_by_id.has(peer):
|
||||
# nani?
|
||||
continue
|
||||
|
||||
var cache := _identifier_by_id.get(peer) as Dictionary
|
||||
cache.erase(identifier.get_id_for(peer))
|
||||
if cache.is_empty():
|
||||
_identifier_by_id.erase(peer)
|
||||
|
||||
func _handle_ids(sender: int, data: PackedByteArray) -> void:
|
||||
var ids := _deserialize_ids(data)
|
||||
|
||||
for full_name in ids:
|
||||
var id := ids[full_name] as int
|
||||
var identifier := _get_identifier_by_name(full_name)
|
||||
if not identifier:
|
||||
# Deleted since then
|
||||
_logger.debug("Received identifier for unknown object with full name %s, id #%d", [full_name, id])
|
||||
continue
|
||||
identifier.set_id_for(sender, id)
|
||||
|
||||
func _serialize_ids(ids: Dictionary) -> PackedByteArray:
|
||||
var buffer := StreamPeerBuffer.new()
|
||||
var varuint := NetworkSchemas.varuint()
|
||||
|
||||
for full_name in ids.keys():
|
||||
var id := ids[full_name] as int
|
||||
|
||||
buffer.put_utf8_string(full_name)
|
||||
varuint.encode(ids[full_name], buffer)
|
||||
|
||||
return buffer.data_array
|
||||
|
||||
func _deserialize_ids(data: PackedByteArray) -> Dictionary:
|
||||
var ids := {}
|
||||
var varuint := NetworkSchemas.varuint()
|
||||
var buffer := StreamPeerBuffer.new()
|
||||
buffer.data_array = data
|
||||
|
||||
while buffer.get_available_bytes() > 0:
|
||||
var full_name := buffer.get_utf8_string()
|
||||
var id := varuint.decode(buffer) as int
|
||||
|
||||
ids[full_name] = id
|
||||
|
||||
return ids
|
||||
@@ -0,0 +1 @@
|
||||
uid://pc8gwg1lbusp
|
||||
@@ -0,0 +1,381 @@
|
||||
extends Node
|
||||
class_name _NetworkSynchronizationServer
|
||||
|
||||
## Synchronizes properties over the network
|
||||
##
|
||||
## Handles synchronization of rollback and state properties (
|
||||
## [RollbackSynchronizer] and [StateSynchronizer] ), while respecting visibility
|
||||
## filters and schemas for serialization.
|
||||
## [br][br]
|
||||
## Packets are sent per tick, instead of per object. So for every simulated
|
||||
## rollback tick, a packet is sent with states, and for every recorded input,
|
||||
## a packet is sent with the inputs.
|
||||
## [br][br]
|
||||
## Optionally, diff states can be used, sending only the property values that
|
||||
## have changed, saving on bandwidth.
|
||||
|
||||
# Dependencies
|
||||
var _command_server: _NetworkCommandServer
|
||||
var _history_server: _NetworkHistoryServer
|
||||
var _identity_server: _NetworkIdentityServer
|
||||
var _simulation_server: _RollbackSimulationServer
|
||||
|
||||
# Configuration
|
||||
var _rb_input_properties := _PropertyPool.new()
|
||||
var _rb_state_properties := _PropertyPool.new()
|
||||
var _rb_owned_input_properties := _PropertyPool.new()
|
||||
var _rb_owned_state_properties := _PropertyPool.new()
|
||||
var _sync_state_properties := _PropertyPool.new()
|
||||
var _sync_owned_state_properties := _PropertyPool.new()
|
||||
|
||||
var _visibility_filters := {} # Node to PeerVisibilityFilter
|
||||
|
||||
var _rb_enable_input_broadcast := ProjectSettings.get_setting("netfox/rollback/enable_input_broadcast", false) as bool
|
||||
var _rb_enable_diffs := NetworkRollback.enable_diff_states
|
||||
var _rb_full_interval := ProjectSettings.get_setting("netfox/rollback/full_state_interval", 24) as int
|
||||
var _rb_full_scheduler := _IntervalScheduler.new(_rb_full_interval)
|
||||
|
||||
var _input_redundancy := NetworkRollback.input_redundancy
|
||||
|
||||
var _last_sync_state_sent := _Snapshot.new(0)
|
||||
var _sync_enable_diffs := ProjectSettings.get_setting("netfox/state_synchronizer/enable_diff_states", true) as bool
|
||||
var _sync_full_interval := ProjectSettings.get_setting("netfox/state_synchronizer/full_state_interval", 24) as int
|
||||
var _sync_full_scheduler := _IntervalScheduler.new(_sync_full_interval)
|
||||
|
||||
var _schemas := _NetworkSchema.new()
|
||||
|
||||
var _dense_serializer: _DenseSnapshotSerializer
|
||||
var _sparse_serializer: _SparseSnapshotSerializer
|
||||
var _redundant_serializer: _RedundantSnapshotSerializer
|
||||
|
||||
var _cmd_full_state: NetworkCommandServer.Command
|
||||
var _cmd_diff_state: NetworkCommandServer.Command
|
||||
var _cmd_input: NetworkCommandServer.Command
|
||||
|
||||
var _cmd_full_sync: NetworkCommandServer.Command
|
||||
var _cmd_diff_sync: NetworkCommandServer.Command
|
||||
|
||||
static var _logger := NetfoxLogger._for_netfox("NetworkSynchronizationServer")
|
||||
|
||||
signal _on_input(snapshot: _Snapshot)
|
||||
signal _on_state(snapshot: _Snapshot)
|
||||
|
||||
## Register a [param]property[/param] of [param]node[/param] to be synchronized
|
||||
## as rollback state
|
||||
func register_rollback_state(node: Node, property: NodePath) -> void:
|
||||
_rb_state_properties.add(node, property)
|
||||
if node.is_multiplayer_authority():
|
||||
_rb_owned_state_properties.add(node, property)
|
||||
|
||||
## Deregister a [param]property[/param] of [param]node[/param] from being
|
||||
## synchronized as rollback state
|
||||
func deregister_rollback_state(node: Node, property: NodePath) -> void:
|
||||
_rb_state_properties.erase(node, property)
|
||||
_rb_owned_state_properties.erase(node, property)
|
||||
|
||||
## Register a [param]property[/param] of [param]node[/param] to be synchronized
|
||||
## as rollback input
|
||||
func register_rollback_input(node: Node, property: NodePath) -> void:
|
||||
_rb_input_properties.add(node, property)
|
||||
if node.is_multiplayer_authority():
|
||||
_rb_owned_input_properties.add(node, property)
|
||||
|
||||
## Deregister a [param]property[/param] of [param]node[/param] from being
|
||||
## synchronized as rollback input
|
||||
func deregister_rollback_input(node: Node, property: NodePath) -> void:
|
||||
_rb_input_properties.erase(node, property)
|
||||
_rb_owned_input_properties.erase(node, property)
|
||||
|
||||
## Register a [param]property[/param] of [param]node[/param] to be synchronized
|
||||
## as synchronized state
|
||||
func register_sync_state(node: Node, property: NodePath) -> void:
|
||||
_sync_state_properties.add(node, property)
|
||||
if node.is_multiplayer_authority():
|
||||
_sync_owned_state_properties.add(node, property)
|
||||
|
||||
## Deregister a [param]property[/param] of [param]node[/param] from being
|
||||
## synchronized as synchronized state
|
||||
func deregister_sync_state(node: Node, property: NodePath) -> void:
|
||||
_sync_state_properties.erase(node, property)
|
||||
_sync_owned_state_properties.erase(node, property)
|
||||
|
||||
## Register a [param]serializer[/param] to use when transmitting
|
||||
## [param]property[/param] of [param]node[/param] over the network
|
||||
func register_schema(node: Node, property: NodePath, serializer: NetworkSchemaSerializer) -> void:
|
||||
_schemas.add(node, property, serializer)
|
||||
|
||||
## Deregister any serializers used for [param]property[/param] on
|
||||
## [param]node[/param] when transmitting over the network
|
||||
func deregister_schema(node: Node, property: NodePath) -> void:
|
||||
_schemas.erase(node, property)
|
||||
|
||||
## Deregister all serializers registered for any properties of
|
||||
## [param]node[/param]
|
||||
func deregister_schema_for(node: Node) -> void:
|
||||
_schemas.erase_subject(node)
|
||||
|
||||
## Register a visibility [param]filter[/param] for use with [param]node[/param]
|
||||
func register_visibility_filter(node: Node, filter: PeerVisibilityFilter) -> void:
|
||||
_visibility_filters[node] = filter
|
||||
|
||||
## Deregister the visibility filter used for [param]node[/param]
|
||||
func deregister_visibility_filter(node: Node) -> void:
|
||||
_visibility_filters.erase(node)
|
||||
|
||||
## Deregister any and all settings associated with [param]node[/param]
|
||||
func deregister(node: Node) -> void:
|
||||
_rb_state_properties.erase_subject(node)
|
||||
_rb_input_properties.erase_subject(node)
|
||||
_rb_owned_state_properties.erase_subject(node)
|
||||
_rb_owned_input_properties.erase_subject(node)
|
||||
_sync_state_properties.erase_subject(node)
|
||||
_visibility_filters.erase(node)
|
||||
_schemas.erase_subject(node)
|
||||
|
||||
func _is_node_visible_to(peer: int, node: Node) -> bool:
|
||||
var filter := _visibility_filters.get(node) as PeerVisibilityFilter
|
||||
if not filter:
|
||||
return true
|
||||
else:
|
||||
return filter.get_visible_peers().has(peer)
|
||||
|
||||
func _synchronize_input(tick: int) -> void:
|
||||
# We don't own inputs, nothing to synchronize
|
||||
if _rb_owned_input_properties.is_empty():
|
||||
return
|
||||
|
||||
var snapshots := [] as Array[_Snapshot]
|
||||
var notified_peers := _Set.new()
|
||||
|
||||
if not _rb_enable_input_broadcast:
|
||||
# If input broadcast is off, find which peers need to know our inputs
|
||||
# That is all peers who own state controlled by our input
|
||||
|
||||
# Grab owned input objects
|
||||
for input_subject in _rb_owned_input_properties.get_subjects():
|
||||
# Grab state objects controlled by input
|
||||
var controlled_nodes := RollbackSimulationServer._get_controlled_by(input_subject)
|
||||
|
||||
# Notify peers owning nodes about the input
|
||||
for node in controlled_nodes:
|
||||
notified_peers.add(node.get_multiplayer_authority())
|
||||
else:
|
||||
# If input broadcast is on, send inputs to everyone
|
||||
for peer in multiplayer.get_peers():
|
||||
notified_peers.add(peer)
|
||||
|
||||
# Make sure to not send input to ourselves
|
||||
notified_peers.erase(multiplayer.get_unique_id())
|
||||
|
||||
# Prepare snapshot package
|
||||
for offset in _input_redundancy:
|
||||
# Grab snapshot from NetworkHistoryServer
|
||||
var snapshot := NetworkHistoryServer._get_rollback_input_snapshot(tick - offset)
|
||||
if not snapshot:
|
||||
break
|
||||
|
||||
_logger.trace("Submitting input: %s", [snapshot])
|
||||
snapshots.append(snapshot)
|
||||
|
||||
_logger.trace("Submitting input to peers: %s", [notified_peers])
|
||||
for peer in notified_peers:
|
||||
var data := _redundant_serializer.write_for(peer, snapshots, _rb_owned_input_properties)
|
||||
_cmd_input.send(data, peer)
|
||||
|
||||
func _synchronize_state(tick: int) -> void:
|
||||
# We don't own state, nothing to synchronize
|
||||
if _rb_owned_state_properties.is_empty():
|
||||
return
|
||||
|
||||
# Grab snapshot from NetworkHistoryServer
|
||||
var snapshot := NetworkHistoryServer._get_rollback_state_snapshot(tick)
|
||||
if not snapshot:
|
||||
# No data for tick
|
||||
return
|
||||
|
||||
if snapshot.is_empty():
|
||||
# Nothing to send
|
||||
return
|
||||
|
||||
# Figure out whether to send full- or diff state
|
||||
var is_full := _rb_full_scheduler.is_now()
|
||||
if not _rb_enable_diffs:
|
||||
is_full = true
|
||||
|
||||
# Check if we have history to diff to
|
||||
var reference_snapshot := NetworkHistoryServer._get_rollback_state_snapshot(tick - 1)
|
||||
if not reference_snapshot:
|
||||
is_full = true
|
||||
|
||||
if is_full:
|
||||
# Send full states
|
||||
for peer in multiplayer.get_peers():
|
||||
var filter := func(subject): return _is_node_visible_to(peer, subject)
|
||||
|
||||
var data := _dense_serializer.write_for(peer, snapshot, _rb_owned_state_properties, filter)
|
||||
if data.is_empty():
|
||||
# Peer can't see anything, send nothing
|
||||
continue
|
||||
|
||||
_cmd_full_state.send(data, peer)
|
||||
|
||||
NetworkPerformance.push_full_state_props(snapshot.size())
|
||||
NetworkPerformance.push_sent_state_props(snapshot.size())
|
||||
else:
|
||||
var diff := _Snapshot.make_patch(reference_snapshot, snapshot)
|
||||
if diff.is_empty():
|
||||
# Nothing changed, don't send anything
|
||||
return
|
||||
|
||||
# Send diff states
|
||||
for peer in multiplayer.get_peers():
|
||||
var filter := func(subject): return _is_node_visible_to(peer, subject)
|
||||
|
||||
var data := _sparse_serializer.write_for(peer, diff, _rb_owned_state_properties, filter)
|
||||
if data.is_empty():
|
||||
# Peer can't see any changes, send nothing
|
||||
continue
|
||||
|
||||
_cmd_diff_state.send(data, peer)
|
||||
|
||||
NetworkPerformance.push_full_state_props(snapshot.size())
|
||||
NetworkPerformance.push_sent_state_props(diff.size())
|
||||
|
||||
func _synchronize_sync_state(tick: int) -> void:
|
||||
# We don't own sync state, nothing to synchronize
|
||||
if _sync_owned_state_properties.is_empty():
|
||||
return
|
||||
|
||||
# Grab snapshot from NetworkHistoryServer
|
||||
var snapshot := NetworkHistoryServer._get_synchronizer_state_snapshot(tick)
|
||||
if not snapshot:
|
||||
return
|
||||
|
||||
# Figure out whether to send full- or diff state
|
||||
var is_full := _sync_full_scheduler.is_now()
|
||||
if not _sync_enable_diffs:
|
||||
is_full = true
|
||||
|
||||
if is_full:
|
||||
# Send full states
|
||||
for peer in multiplayer.get_peers():
|
||||
var filter := func(subject): return _is_node_visible_to(peer, subject)
|
||||
|
||||
var data := _dense_serializer.write_for(peer, snapshot, _sync_owned_state_properties, filter)
|
||||
if data.is_empty():
|
||||
# Peer can't see anything, send nothing
|
||||
continue
|
||||
|
||||
_cmd_full_sync.send(data, peer)
|
||||
|
||||
NetworkPerformance.push_full_state_props(snapshot.size())
|
||||
NetworkPerformance.push_sent_state_props(snapshot.size())
|
||||
else:
|
||||
var diff := _Snapshot.make_patch(_last_sync_state_sent, snapshot)
|
||||
|
||||
# Send diffs
|
||||
for peer in multiplayer.get_peers():
|
||||
var filter := func(subject): return _is_node_visible_to(peer, subject)
|
||||
|
||||
var data := _sparse_serializer.write_for(peer, diff, _sync_owned_state_properties, filter)
|
||||
if data.is_empty():
|
||||
# Peer can't see anything, send nothing
|
||||
continue
|
||||
|
||||
_cmd_diff_sync.send(data, peer)
|
||||
|
||||
NetworkPerformance.push_full_state_props(snapshot.size())
|
||||
NetworkPerformance.push_sent_state_props(diff.size())
|
||||
|
||||
# Remember last sent state for diffing
|
||||
# NOTE: This is a shared instance, theoretically shouldn't screw things up
|
||||
_last_sync_state_sent = snapshot
|
||||
|
||||
func _init(
|
||||
p_command_server: _NetworkCommandServer = null,
|
||||
p_history_server: _NetworkHistoryServer = null,
|
||||
p_identity_server: _NetworkIdentityServer = null,
|
||||
p_simulation_server: _RollbackSimulationServer = null
|
||||
):
|
||||
_command_server = p_command_server
|
||||
_history_server = p_history_server
|
||||
_identity_server = p_identity_server
|
||||
_simulation_server = p_simulation_server
|
||||
|
||||
func _ready():
|
||||
# Ensure dependencies
|
||||
if not _command_server: _command_server = NetworkCommandServer
|
||||
if not _history_server: _history_server = NetworkHistoryServer
|
||||
if not _identity_server: _identity_server = NetworkIdentityServer
|
||||
if not _simulation_server: _simulation_server = RollbackSimulationServer
|
||||
|
||||
# Setup serializers
|
||||
_dense_serializer = _DenseSnapshotSerializer.new(_schemas, _identity_server)
|
||||
_sparse_serializer = _SparseSnapshotSerializer.new(_schemas, _identity_server)
|
||||
_redundant_serializer = _RedundantSnapshotSerializer.new(_schemas, _identity_server)
|
||||
|
||||
# Setup commands
|
||||
_cmd_full_state = _command_server.register_command(_handle_full_state, MultiplayerPeer.TRANSFER_MODE_UNRELIABLE)
|
||||
_cmd_diff_state = _command_server.register_command(_handle_diff_state, MultiplayerPeer.TRANSFER_MODE_UNRELIABLE)
|
||||
_cmd_input = _command_server.register_command(_handle_input, MultiplayerPeer.TRANSFER_MODE_UNRELIABLE)
|
||||
|
||||
_cmd_full_sync = _command_server.register_command(_handle_full_sync, MultiplayerPeer.TRANSFER_MODE_UNRELIABLE_ORDERED)
|
||||
_cmd_diff_sync = _command_server.register_command(_handle_diff_sync, MultiplayerPeer.TRANSFER_MODE_UNRELIABLE_ORDERED)
|
||||
|
||||
func _handle_input(sender: int, data: PackedByteArray):
|
||||
var buffer := StreamPeerBuffer.new()
|
||||
buffer.data_array = data
|
||||
|
||||
var snapshots := _redundant_serializer.read_from(sender, _rb_input_properties, buffer, true)
|
||||
|
||||
for snapshot in snapshots:
|
||||
snapshot.sanitize(sender)
|
||||
|
||||
_logger.trace("Ingesting input: %s", [snapshot])
|
||||
if NetworkHistoryServer._merge_rollback_input(snapshot):
|
||||
_on_input.emit(snapshot)
|
||||
|
||||
func _handle_full_state(sender: int, data: PackedByteArray):
|
||||
var buffer := StreamPeerBuffer.new()
|
||||
buffer.data_array = data
|
||||
|
||||
var snapshot := _dense_serializer.read_from(sender, _rb_state_properties, buffer, true)
|
||||
|
||||
_ingest_state(sender, snapshot)
|
||||
|
||||
func _handle_diff_state(sender: int, data: PackedByteArray):
|
||||
var buffer := StreamPeerBuffer.new()
|
||||
buffer.data_array = data
|
||||
|
||||
var diff := _sparse_serializer.read_from(sender, _rb_state_properties, buffer)
|
||||
_logger.trace("Received diff state for @%d", [diff.tick])
|
||||
|
||||
_ingest_state(sender, diff)
|
||||
|
||||
func _handle_full_sync(sender: int, data: PackedByteArray):
|
||||
var buffer := StreamPeerBuffer.new()
|
||||
buffer.data_array = data
|
||||
|
||||
var snapshot := _dense_serializer.read_from(sender, _sync_state_properties, buffer, true)
|
||||
snapshot.sanitize(sender)
|
||||
|
||||
NetworkHistoryServer._merge_synchronizer_state(snapshot)
|
||||
_logger.trace("Ingested sync state: %s", [snapshot])
|
||||
|
||||
func _handle_diff_sync(sender: int, data: PackedByteArray):
|
||||
var buffer := StreamPeerBuffer.new()
|
||||
buffer.data_array = data
|
||||
|
||||
var snapshot := _sparse_serializer.read_from(sender, _sync_state_properties, buffer)
|
||||
snapshot.sanitize(sender)
|
||||
|
||||
NetworkHistoryServer._merge_synchronizer_state(snapshot)
|
||||
_logger.trace("Ingested sync diff: %s", [snapshot])
|
||||
|
||||
func _ingest_state(sender: int, snapshot: _Snapshot) -> void:
|
||||
snapshot.sanitize(sender)
|
||||
|
||||
NetworkHistoryServer._merge_rollback_state(snapshot)
|
||||
_logger.trace("Ingested state: %s", [snapshot])
|
||||
|
||||
_on_state.emit(snapshot)
|
||||
@@ -0,0 +1 @@
|
||||
uid://yrhqsbd5ubhs
|
||||
@@ -0,0 +1,201 @@
|
||||
extends Node
|
||||
class_name _RollbackSimulationServer
|
||||
|
||||
## Runs gameplay simulation during rollback
|
||||
##
|
||||
## Rollback involves restoring the game state to an earlier point in time and
|
||||
## re-running game logic from there. This class tracks which nodes participate
|
||||
## in rollback, which of them need to be actually simulated, and runs the
|
||||
## simulation itself.
|
||||
## [br][br]
|
||||
## Node simulation honors scene tree order. Processing order and physics
|
||||
## processing order are [b]not[/b] considered.
|
||||
## [br][br]
|
||||
## To participate in rollback, call [method register] using the method to call
|
||||
## to simulate the node. Only one [Callable] can be actively registered per
|
||||
## node.
|
||||
|
||||
var _history_server: _NetworkHistoryServer
|
||||
|
||||
var _callbacks := {} # node to callback
|
||||
var _simulated_ticks := {} # node to array of ticks
|
||||
|
||||
var _input_graph := _Graph.new() # Links inputs to objects controlled by them
|
||||
|
||||
# Currently simulated object
|
||||
var _current_object: Object = null
|
||||
# Predicted nodes for next simulation
|
||||
var _predicted_nodes := _Set.new()
|
||||
|
||||
var _group := StringName("__nf_rollback_sim" + str(get_instance_id()))
|
||||
|
||||
static var _logger := NetfoxLogger._for_netfox("RollbackSimulationServer")
|
||||
|
||||
## Register a [param]callback[/param] to run as part of the rollback loop
|
||||
func register(callback: Callable) -> void:
|
||||
if not is_instance_valid(callback.get_object()):
|
||||
_logger.error("Trying to register callback that belongs to an invalid object!")
|
||||
return
|
||||
|
||||
assert(callback.get_object() is Node, "Only nodes supported for now!")
|
||||
|
||||
_callbacks[callback.get_object()] = callback
|
||||
|
||||
## Deregister a [param]callback[/param] from the rollback loop
|
||||
func deregister(callback: Callable) -> void:
|
||||
if not callback or not callback.is_valid(): return
|
||||
|
||||
var object := callback.get_object()
|
||||
|
||||
if not is_instance_valid(object): return
|
||||
if _callbacks[object] != callback: return
|
||||
|
||||
_callbacks.erase(object)
|
||||
_input_graph.erase(object)
|
||||
_simulated_ticks.erase(object)
|
||||
|
||||
## Deregister a [param]node[/param] from the rollback loop
|
||||
func deregister_node(node: Node) -> void:
|
||||
if _callbacks.has(node):
|
||||
deregister(_callbacks[node])
|
||||
_input_graph.erase(node)
|
||||
|
||||
## Register [param]input[/param] as providing input for [param]node[/param]
|
||||
func register_rollback_input_for(node: Node, input: Node) -> void:
|
||||
_input_graph.link(input, node)
|
||||
|
||||
## Deregister [param]input[/param] from providing input for [param]node[/param]
|
||||
func deregister_rollback_input(node: Node, input: Node) -> void:
|
||||
_input_graph.unlink(input, 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):
|
||||
return false
|
||||
return _predicted_nodes.has(_current_object)
|
||||
|
||||
## Get the currently simulated object
|
||||
func get_simulated_object() -> Object:
|
||||
return _current_object
|
||||
|
||||
## Simulate a tick
|
||||
func simulate(delta: float, tick: int) -> void:
|
||||
_current_object = null
|
||||
|
||||
var input_snapshot := NetworkHistoryServer._get_rollback_input_snapshot(tick)
|
||||
var state_snapshot := NetworkHistoryServer._get_rollback_state_snapshot(tick)
|
||||
var nodes := _get_nodes_to_simulate(input_snapshot)
|
||||
_predicted_nodes.clear()
|
||||
|
||||
# Sort based on SceneTree order
|
||||
for node in nodes:
|
||||
node.add_to_group(_group)
|
||||
nodes = get_tree().get_nodes_in_group(_group)
|
||||
|
||||
# Determine predicted nodes
|
||||
for node in _callbacks.keys():
|
||||
if _is_predicting(input_snapshot, node):
|
||||
_predicted_nodes.add(node)
|
||||
|
||||
# Run callbacks and clear group
|
||||
for node in nodes:
|
||||
_current_object = node
|
||||
|
||||
var callback := _callbacks[node] as Callable
|
||||
var is_fresh := _is_tick_fresh_for(node, tick)
|
||||
callback.call(delta, tick, is_fresh)
|
||||
node.remove_from_group(_group)
|
||||
|
||||
_current_object = null
|
||||
_set_tick_simulated_for(node, tick)
|
||||
|
||||
# Metrics
|
||||
NetworkPerformance.push_rollback_nodes_simulated(nodes.size())
|
||||
|
||||
func _get_nodes_to_simulate(input_snapshot: _Snapshot) -> Array[Node]:
|
||||
var result: Array[Node] = []
|
||||
if not input_snapshot:
|
||||
return []
|
||||
|
||||
var tick := input_snapshot.tick
|
||||
for node in _callbacks.keys():
|
||||
var inputs := [] as Array[Node]
|
||||
inputs.assign(_input_graph.get_linked_to(node))
|
||||
|
||||
if inputs.is_empty():
|
||||
# Node has no input, simulate it
|
||||
result.append(node)
|
||||
continue
|
||||
|
||||
if NetworkRollback.is_mutated(node, tick):
|
||||
# Node is mutated, must simulate
|
||||
result.append(node)
|
||||
continue
|
||||
|
||||
if not input_snapshot.has_subjects(inputs, true):
|
||||
# We don't have input for node, don't simulate
|
||||
continue
|
||||
|
||||
result.append(node)
|
||||
|
||||
return result
|
||||
|
||||
func _is_predicting(input_snapshot: _Snapshot, node: Node) -> bool:
|
||||
var input_nodes := [] as Array[Node]
|
||||
input_nodes.assign(_input_graph.get_linked_to(node))
|
||||
|
||||
var is_owned := node.is_multiplayer_authority()
|
||||
var is_inputless := input_nodes.is_empty()
|
||||
var has_input := false
|
||||
|
||||
if not is_inputless and input_snapshot:
|
||||
has_input = input_snapshot.has_subjects(input_nodes, true)
|
||||
|
||||
if not is_owned and has_input:
|
||||
# We don't own the node, but we own input for it - not (input) predicting
|
||||
return false
|
||||
if not is_owned:
|
||||
# We don't own the node, so we can only guess - i.e. predict
|
||||
return true
|
||||
if is_owned and is_inputless:
|
||||
# We own the node, node doesn't depend on input, we're sure
|
||||
return false
|
||||
if is_owned and not has_input:
|
||||
# We own the node, node depends on input, we don't have data for input - predict
|
||||
return true
|
||||
# We own the node and we have data for node's input - we're sure
|
||||
return false
|
||||
|
||||
func _is_tick_fresh_for(node: Node, tick: int) -> bool:
|
||||
if not _simulated_ticks.has(node):
|
||||
return true
|
||||
var ticks := _simulated_ticks.get(node) as Array[int]
|
||||
return not ticks.has(tick)
|
||||
|
||||
func _set_tick_simulated_for(node: Node, tick: int) -> void:
|
||||
if not _simulated_ticks.has(node):
|
||||
_simulated_ticks[node] = [tick] as Array[int]
|
||||
else:
|
||||
_simulated_ticks[node].append(tick)
|
||||
|
||||
func _trim_ticks_simulated(beginning: int) -> void:
|
||||
for object in _simulated_ticks:
|
||||
_simulated_ticks[object] = _simulated_ticks[object]\
|
||||
.filter(func(tick): return tick >= beginning)
|
||||
|
||||
func _get_controlled_by(input: Node) -> Array[Node]:
|
||||
var result := [] as Array[Node]
|
||||
result.assign(_input_graph.get_linked_from(input))
|
||||
return result
|
||||
|
||||
func _get_inputs_of(node: Node) -> Array[Node]:
|
||||
var result := [] as Array[Node]
|
||||
result.assign(_input_graph.get_linked_to(node))
|
||||
return result
|
||||
|
||||
func _init(p_history_server: _NetworkHistoryServer = null):
|
||||
_history_server = p_history_server
|
||||
|
||||
func _ready():
|
||||
# Ensure dependencies
|
||||
if not _history_server: _history_server = NetworkHistoryServer
|
||||
@@ -0,0 +1 @@
|
||||
uid://d5vx6blln3jr
|
||||
@@ -22,43 +22,20 @@ class_name StateSynchronizer
|
||||
## for every tick.
|
||||
## [br][br]
|
||||
## Only considered if [member _NetworkRollback.enable_diff_states] is true.
|
||||
## @deprecated: This can now be configured in the project settings.
|
||||
@export_range(0, 128, 1, "or_greater")
|
||||
var full_state_interval: int = 24 # TODO: Don't tie to a network rollback setting?
|
||||
var full_state_interval: int = 24
|
||||
|
||||
## Ticks to wait between unreliably acknowledging diff states.
|
||||
## [br][br]
|
||||
## This can reduce the amount of properties sent in diff states, due to clients
|
||||
## more often acknowledging received states. To avoid introducing hickups, these
|
||||
## are sent unreliably.
|
||||
## [br][br]
|
||||
## If set to 0, diff states will never be acknowledged. If set to 1, all diff
|
||||
## states will be acknowledged. If set higher, ack's will be sent regularly, but
|
||||
## not for every diff state.
|
||||
## [br][br]
|
||||
## If enabled, it's worth to tune this setting until network traffic is actually
|
||||
## reduced.
|
||||
## [br][br]
|
||||
## Only considered if [member _NetworkRollback.enable_diff_states] is true.
|
||||
## @deprecated: This is no longer used.
|
||||
@export_range(0, 128, 1, "or_greater")
|
||||
var diff_ack_interval: int = 0 # TODO: Don't tie to a network rollback setting?
|
||||
var diff_ack_interval: int = 0
|
||||
|
||||
## Decides which peers will receive updates
|
||||
var visibility_filter := PeerVisibilityFilter.new()
|
||||
|
||||
var _property_cache: PropertyCache
|
||||
var _property_config: _PropertyConfig = _PropertyConfig.new()
|
||||
var _properties_dirty: bool = false
|
||||
|
||||
var _state_history := _PropertyHistoryBuffer.new()
|
||||
|
||||
# Collaborators
|
||||
var _full_state_encoder: _SnapshotHistoryEncoder
|
||||
var _diff_state_encoder: _DiffHistoryEncoder
|
||||
|
||||
# State
|
||||
var _ackd_state: Dictionary = {}
|
||||
var _next_full_state_tick: int
|
||||
var _next_diff_ack_tick: int
|
||||
var _properties := _PropertyPool.new()
|
||||
var _schema_nodes := _Set.new()
|
||||
|
||||
var _is_initialized: bool = false
|
||||
|
||||
@@ -68,24 +45,21 @@ static var _logger := NetfoxLogger._for_netfox("StateSynchronizer")
|
||||
## [br][br]
|
||||
## Call this after any change to configuration.
|
||||
func process_settings() -> void:
|
||||
_property_cache = PropertyCache.new(root)
|
||||
_property_config.set_properties_from_paths(properties, _property_cache)
|
||||
# Remove old configuration
|
||||
for node in _properties.get_subjects():
|
||||
for property in _properties.get_properties_of(node):
|
||||
NetworkHistoryServer.deregister_sync_state(node, property)
|
||||
NetworkSynchronizationServer.deregister_sync_state(node, property)
|
||||
|
||||
_full_state_encoder = _SnapshotHistoryEncoder.new(_state_history, _property_cache)
|
||||
_diff_state_encoder = _DiffHistoryEncoder.new(_state_history, _property_cache)
|
||||
# Register new configuration
|
||||
_properties.set_from_paths(root, properties)
|
||||
for node in _properties.get_subjects():
|
||||
NetworkSynchronizationServer.register_visibility_filter(node, visibility_filter)
|
||||
NetworkIdentityServer.register_node(node)
|
||||
|
||||
_diff_state_encoder.add_properties(_property_config.get_properties())
|
||||
|
||||
_next_full_state_tick = NetworkTime.tick
|
||||
_next_diff_ack_tick = NetworkTime.tick
|
||||
|
||||
# Scatter full state sends, so not all nodes send at the same tick
|
||||
if is_inside_tree():
|
||||
_next_full_state_tick += hash(root.get_path()) % maxi(1, full_state_interval)
|
||||
_next_diff_ack_tick += hash(root.get_path()) % maxi(1, diff_ack_interval)
|
||||
else:
|
||||
_next_full_state_tick += hash(root.name) % maxi(1, full_state_interval)
|
||||
_next_diff_ack_tick += hash(root.name) % maxi(1, diff_ack_interval)
|
||||
for property in _properties.get_properties_of(node):
|
||||
NetworkHistoryServer.register_sync_state(node, property)
|
||||
NetworkSynchronizationServer.register_sync_state(node, property)
|
||||
|
||||
_is_initialized = true
|
||||
|
||||
@@ -103,9 +77,44 @@ func add_state(node: Variant, property: String) -> void:
|
||||
_properties_dirty = true
|
||||
_reprocess_settings.call_deferred()
|
||||
|
||||
## Set the schema for transmitting properties over the network.
|
||||
## [br][br]
|
||||
## The [param schema] must be a dictionary, with the keys being property path
|
||||
## strings, and the values are the associated [NetworkSchemaSerializer] objects.
|
||||
## Properties are interpreted relative to the [member root] node. Properties not
|
||||
## specified in the schema will use a generic fallback serializer. By using the
|
||||
## right serializer for the right property, bandwidth usage can be lowered.
|
||||
## [br][br]
|
||||
## See [NetworkSchemas] for many common serializers.
|
||||
## [br][br]
|
||||
## Example:
|
||||
## [codeblock]
|
||||
## state_synchronizer.set_schema({
|
||||
## ":transform": NetworkSchemas.transform3f32(),
|
||||
## ":velocity": NetworkSchemas.vec3f32()
|
||||
## })
|
||||
## [/codeblock]
|
||||
func set_schema(schema: Dictionary) -> void:
|
||||
# Remove previous schema
|
||||
for node in _schema_nodes:
|
||||
NetworkSynchronizationServer.deregister_schema_for(node)
|
||||
_schema_nodes.clear()
|
||||
|
||||
# Register new schema
|
||||
for prop in schema:
|
||||
var prop_entry := PropertyEntry.parse(root, prop)
|
||||
var serializer := schema[prop] as NetworkSchemaSerializer
|
||||
NetworkSynchronizationServer.register_schema(prop_entry.node, prop_entry.property, serializer)
|
||||
_schema_nodes.add(prop_entry.node)
|
||||
|
||||
func _notification(what) -> void:
|
||||
if what == NOTIFICATION_EDITOR_PRE_SAVE:
|
||||
update_configuration_warnings()
|
||||
elif what == NOTIFICATION_PREDELETE:
|
||||
for node in _properties.get_subjects():
|
||||
NetworkSynchronizationServer.deregister(node)
|
||||
NetworkHistoryServer.deregister(node)
|
||||
NetworkIdentityServer.deregister(node)
|
||||
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
if not root:
|
||||
@@ -120,14 +129,6 @@ func _get_configuration_warnings() -> PackedStringArray:
|
||||
add_state(node, prop)
|
||||
)
|
||||
|
||||
func _connect_signals() -> void:
|
||||
NetworkTime.after_tick.connect(_after_tick)
|
||||
NetworkTime.after_tick_loop.connect(_after_loop)
|
||||
|
||||
func _disconnect_signals() -> void:
|
||||
NetworkTime.after_tick.disconnect(_after_tick)
|
||||
NetworkTime.after_tick_loop.disconnect(_after_loop)
|
||||
|
||||
func _enter_tree() -> void:
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
@@ -137,27 +138,13 @@ func _enter_tree() -> void:
|
||||
if not visibility_filter.get_parent():
|
||||
add_child(visibility_filter)
|
||||
|
||||
_connect_signals.call_deferred()
|
||||
process_settings.call_deferred()
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
|
||||
_disconnect_signals()
|
||||
|
||||
func _after_tick(_dt: float, tick: int) -> void:
|
||||
if is_multiplayer_authority():
|
||||
# Submit snapshot
|
||||
var state := _PropertySnapshot.extract(_property_config.get_properties())
|
||||
_state_history.set_snapshot(tick, state)
|
||||
_broadcast_state(tick, state)
|
||||
elif not _state_history.is_empty():
|
||||
var state := _state_history.get_history(tick)
|
||||
state.apply(_property_cache)
|
||||
|
||||
func _after_loop() -> void:
|
||||
_state_history.trim(NetworkTime.tick - NetworkRollback.history_limit) # TODO: Don't tie to rollback?
|
||||
func _ready():
|
||||
# Reprocess authority
|
||||
# Important if nodes are pre-placed in the scene - node starts as owned by
|
||||
# us ( offline peer is 1 ), but once we connect, we no longer own the node
|
||||
multiplayer.connected_to_server.connect(process_settings)
|
||||
|
||||
func _reprocess_settings() -> void:
|
||||
if not _properties_dirty:
|
||||
@@ -165,100 +152,3 @@ func _reprocess_settings() -> void:
|
||||
|
||||
_properties_dirty = false
|
||||
process_settings()
|
||||
|
||||
func _broadcast_state(tick: int, state: _PropertySnapshot) -> void:
|
||||
var is_sending_diffs := NetworkRollback.enable_diff_states # TODO: Don't tie to a rollback setting?
|
||||
var is_full_state_tick := not is_sending_diffs or (full_state_interval > 0 and tick > _next_full_state_tick)
|
||||
|
||||
if is_full_state_tick:
|
||||
# Broadcast new full state
|
||||
for peer in visibility_filter.get_rpc_target_peers():
|
||||
_send_full_state(tick, peer)
|
||||
|
||||
# Adjust next full state if sending diffs
|
||||
if is_sending_diffs:
|
||||
_next_full_state_tick = tick + full_state_interval
|
||||
else:
|
||||
# Send diffs to each peer
|
||||
for peer in visibility_filter.get_visible_peers():
|
||||
var reference_tick := _ackd_state.get(peer, -1) as int
|
||||
if reference_tick < 0 or not _state_history.has(reference_tick):
|
||||
# Peer hasn't ack'd any tick, or we don't have the ack'd tick
|
||||
# Send full state
|
||||
_logger.trace("Reference tick @%d not found for peer #%s, sending full tick", [reference_tick, peer])
|
||||
_send_full_state(tick, peer)
|
||||
continue
|
||||
|
||||
# Prepare diff
|
||||
var diff_state_data := _diff_state_encoder.encode(tick, reference_tick, _property_config.get_properties())
|
||||
|
||||
if _diff_state_encoder.get_full_snapshot().size() == _diff_state_encoder.get_encoded_snapshot().size():
|
||||
# State is completely different, send full state
|
||||
_send_full_state(tick, peer)
|
||||
else:
|
||||
# Send only diff
|
||||
_submit_diff_state.rpc_id(peer, diff_state_data, tick, reference_tick)
|
||||
|
||||
# Push metrics
|
||||
NetworkPerformance.push_full_state(_diff_state_encoder.get_full_snapshot())
|
||||
NetworkPerformance.push_sent_state(_diff_state_encoder.get_encoded_snapshot())
|
||||
|
||||
func _send_full_state(tick: int, peer: int = 0) -> void:
|
||||
var full_state_snapshot := _state_history.get_snapshot(tick).as_dictionary()
|
||||
var full_state_data := _full_state_encoder.encode(tick, _property_config.get_properties())
|
||||
|
||||
_submit_full_state.rpc_id(peer, full_state_data, tick)
|
||||
|
||||
if peer <= 0:
|
||||
NetworkPerformance.push_full_state_broadcast(full_state_snapshot)
|
||||
NetworkPerformance.push_sent_state_broadcast(full_state_snapshot)
|
||||
else:
|
||||
NetworkPerformance.push_full_state(full_state_snapshot)
|
||||
NetworkPerformance.push_sent_state(full_state_snapshot)
|
||||
|
||||
# `serialized_state` is a serialized _PropertySnapshot
|
||||
@rpc("any_peer", "unreliable_ordered", "call_remote")
|
||||
func _submit_full_state(data: Array, tick: int) -> void:
|
||||
if not _is_initialized: return
|
||||
|
||||
var sender := multiplayer.get_remote_sender_id()
|
||||
var snapshot := _full_state_encoder.decode(data, _property_config.get_properties_owned_by(sender))
|
||||
|
||||
if not _full_state_encoder.apply(tick, snapshot, sender):
|
||||
# Invalid data
|
||||
return
|
||||
|
||||
if NetworkRollback.enable_diff_states:
|
||||
_ack_full_state.rpc_id(sender, tick)
|
||||
|
||||
# State is a serialized _PropertySnapshot (Dictionary[String, Variant])
|
||||
@rpc("any_peer", "unreliable_ordered", "call_remote")
|
||||
func _submit_diff_state(data: PackedByteArray, tick: int, reference_tick: int) -> void:
|
||||
if not _is_initialized: return
|
||||
|
||||
var sender = multiplayer.get_remote_sender_id()
|
||||
var diff_snapshot := _diff_state_encoder.decode(data, _property_config.get_properties_owned_by(sender))
|
||||
if not _diff_state_encoder.apply(tick, diff_snapshot, reference_tick, sender):
|
||||
# Invalid data
|
||||
return
|
||||
|
||||
if NetworkRollback.enable_diff_states:
|
||||
if diff_ack_interval > 0 and tick > _next_diff_ack_tick:
|
||||
_ack_diff_state.rpc_id(sender, tick)
|
||||
_next_diff_ack_tick = tick + diff_ack_interval
|
||||
|
||||
@rpc("any_peer", "reliable", "call_remote")
|
||||
func _ack_full_state(tick: int) -> void:
|
||||
var sender_id := multiplayer.get_remote_sender_id()
|
||||
_ackd_state[sender_id] = tick
|
||||
|
||||
_logger.trace("Peer %d ack'd full state for tick %d", [sender_id, tick])
|
||||
|
||||
@rpc("any_peer", "unreliable_ordered", "call_remote")
|
||||
func _ack_diff_state(tick: int) -> void:
|
||||
if not _is_initialized: return
|
||||
|
||||
var sender_id := multiplayer.get_remote_sender_id()
|
||||
_ackd_state[sender_id] = tick
|
||||
|
||||
_logger.trace("Peer %d ack'd diff state for tick %d", [sender_id, tick])
|
||||
|
||||
@@ -40,7 +40,7 @@ signal on_tickrate_mismatch(peer: int, tickrate: int)
|
||||
## [br][br]
|
||||
## Called by [_NetworkTime], no need to call manually.
|
||||
func run() -> void:
|
||||
if multiplayer.is_server():
|
||||
if _is_authority():
|
||||
# Broadcast tickrate
|
||||
_submit_tickrate.rpc(NetworkTime.tickrate)
|
||||
|
||||
@@ -61,7 +61,7 @@ func _ready() -> void:
|
||||
name = "NetworkTickrateHandshake"
|
||||
|
||||
func _handle_new_peer(peer: int) -> void:
|
||||
if multiplayer.is_server():
|
||||
if _is_authority():
|
||||
_submit_tickrate.rpc_id(peer, NetworkTime.tickrate)
|
||||
|
||||
func _handle_tickrate_mismatch(peer: int, tickrate: int) -> void:
|
||||
@@ -74,21 +74,25 @@ func _handle_tickrate_mismatch(peer: int, tickrate: int) -> void:
|
||||
NetworkTime.tickrate, peer, tickrate
|
||||
])
|
||||
DISCONNECT:
|
||||
if multiplayer.is_server():
|
||||
if _is_authority():
|
||||
_logger.warning("Peer #%d's tickrate of %dtps differs from expected %dtps! Disconnecting.", [
|
||||
peer, tickrate, NetworkTime.tickrate
|
||||
])
|
||||
multiplayer.multiplayer_peer.disconnect_peer(peer)
|
||||
ADJUST:
|
||||
if not multiplayer.is_server():
|
||||
if not _is_authority():
|
||||
_logger.info("Local tickrate %dtps differs from tickrate of host at %dtps! Adjusting.", [
|
||||
NetworkTime.tickrate, tickrate
|
||||
])
|
||||
# TODO: Make tickrate mutable at user's digression
|
||||
ProjectSettings.set_setting(&"netfox/time/tickrate", tickrate)
|
||||
NetworkTime._tickrate = tickrate
|
||||
SIGNAL:
|
||||
on_tickrate_mismatch.emit(peer, tickrate)
|
||||
|
||||
func _is_authority() -> bool:
|
||||
# HACK: This method is here to ease testing; pretending to be a client is messy from a unit test
|
||||
return multiplayer.is_server()
|
||||
|
||||
@rpc("any_peer", "reliable", "call_remote")
|
||||
func _submit_tickrate(tickrate: int) -> void:
|
||||
var sender := multiplayer.get_remote_sender_id()
|
||||
|
||||
Reference in New Issue
Block a user