Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

When a player sends a state RPC, the state is serialized, and on receiving the RPC it is deserialized. #254

Closed
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions addons/netfox/netfox.gd
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ var SETTINGS = [
"value": 64,
"type": TYPE_INT
},
{
"name": "netfox/rollback/serialized_inputs_history_limit",
"value": 64,
"type": TYPE_INT
},
{
"name": "netfox/rollback/serialized_states_history_limit",
"value": 64,
"type": TYPE_INT
},
{
"name": "netfox/rollback/input_redundancy",
"value": 3,
Expand All @@ -84,6 +94,12 @@ var SETTINGS = [
"value": true,
"type": TYPE_BOOL,
"hint_string": "Enabling this, the input is serialized before sending it, instead of sending a dictionary of string properties and its values. Enabling this is recommended to save bandwidth, at the slight cost of CPU."
},
{
"name": "netfox/serialization/enable_state_serialization",
"value": true,
"type": TYPE_BOOL,
"hint_string": "Enabling this, the state is serialized before sending it, instead of sending a dictionary of string properties and its values. Enabling this is recommended to save bandwidth, at the slight cost of CPU. Note that this serialization isn't diff state, but the state itself just in binary format."
}
]

Expand Down
12 changes: 12 additions & 0 deletions addons/netfox/rollback/network-rollback.gd
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ var history_limit: int:
set(v):
push_error("Trying to set read-only variable history_limit")

var serialized_states_history_limit: int:
get:
return ProjectSettings.get_setting("netfox/rollback/serialized_states_history_limit", 24)
set(v):
push_error("Trying to set read-only variable serialized_states_history_limit")

var serialized_inputs_history_limit: int:
get:
return ProjectSettings.get_setting("netfox/rollback/serialized_inputs_history_limit", 24)
set(v):
push_error("Trying to set read-only variable serialized_inputs_history_limit")
## Offset into the past for display.
##
## After the rollback, we have the option to not display the absolute latest
Expand Down Expand Up @@ -49,6 +60,7 @@ var tick: int:
push_error("Trying to set read-only variable tick")

var enable_input_serialization: bool = ProjectSettings.get_setting("netfox/serialization/enable_input_serialization", true)
var enable_state_serialization: bool = ProjectSettings.get_setting("netfox/serialization/enable_state_serialization", true)

## Event emitted before running the network rollback loop
signal before_loop()
Expand Down
62 changes: 48 additions & 14 deletions addons/netfox/rollback/rollback-synchronizer.gd
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ var _auth_state_props: Array[PropertyEntry] = []
var _auth_input_props: Array[PropertyEntry] = []
var _nodes: Array[Node] = []

var _states: Dictionary = {}
var _inputs: Dictionary = {}
var _states: Dictionary = {} #<tick, Dictionary<String, Variant>>
var _inputs: Dictionary = {} #<tick, Dictionary<String, Variant>>
var _serialized_inputs: Dictionary = {} #<tick, PackedByteArray>
var _serialized_states: Dictionary = {} #<tick, PackedByteArray>
var serialized_inputs_to_send: Array[PackedByteArray] = []
var _latest_state: int = -1
var _earliest_input: int
Expand Down Expand Up @@ -51,7 +52,7 @@ func process_settings():

# Gather state props - all state props are recorded
for property in state_properties:
var pe = _property_cache.get_entry(property)
var pe: PropertyEntry = _property_cache.get_entry(property)
_record_state_props.push_back(pe)

process_authority()
Expand Down Expand Up @@ -83,10 +84,9 @@ func process_authority():
# Only record input that is our own
for property in input_properties:
var pe = _property_cache.get_entry(property)
_record_input_props.push_back(pe)
if pe.node.is_multiplayer_authority():
_auth_input_props.push_back(pe)
else:
_record_input_props.push_back(pe)
Copy link
Contributor Author

@TheYellowArchitect TheYellowArchitect Aug 26, 2024

Choose a reason for hiding this comment

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

Not critical enough to backport it to #251
From what I understood, _record_state_props records the properties of a node, regardless of authority.
_record_input_props was unused (yet I needed it for that exact purpose, to learn the properties of a node I don't have authority on, as the receiver), so I replicated the same as _record_state_props

But this opens a new issue. Why do _auth_input_props and _auth_state_props even exist?!
They are currently used exclusively as booleans, to get if we have authority or not.
And it's not like properties are added realtime - if they are, they could as well be added onto _record_state_props and _record_input_props without problems.

My suggestion is to straight up remove the variables _auth_input_props and _auth_state_props and replace them with booleans (e.g. node.get_multiplayer_authority()) unless I am missing something


func _ready():
process_settings()
Expand Down Expand Up @@ -150,21 +150,35 @@ func _process_tick(tick: int):
NetworkRollback.process_rollback(node, NetworkTime.ticktime, tick, is_fresh)
_freshness_store.notify_processed(node, tick)



func _record_tick(tick: int):
# Broadcast state we own
if not _auth_state_props.is_empty():
var broadcast = {}

var state_to_broadcast = {}

#if (multiplayer.get_unique_id() > 1):
#print("Record tick state sending, peer id is: %s" % multiplayer.get_unique_id())
for property in _auth_state_props:
if _can_simulate(property.node, tick - 1):
# Only broadcast if we've simulated the node
broadcast[property.to_string()] = property.get_value()
state_to_broadcast[property.to_string()] = property.get_value()

if broadcast.size() > 0:
# Broadcast as new state
if state_to_broadcast.size() > 0:
_latest_state = max(_latest_state, tick)
_states[tick] = PropertySnapshot.merge(_states.get(tick, {}), broadcast)
_submit_state.rpc(broadcast, tick)
_states[tick] = PropertySnapshot.merge(_states.get(tick, {}), state_to_broadcast)

if (NetworkRollback.enable_state_serialization):
var serialized_current_state: PackedByteArray = PropertiesSerializer.serialize_multiple_properties(_auth_state_props, tick)
_serialized_states[tick] = serialized_current_state

# Broadcast as new state
for picked_peer_id in multiplayer.get_peers():
_submit_serialized_state.rpc_id(picked_peer_id, serialized_current_state)
else:
# Broadcast as new state
for picked_peer_id in multiplayer.get_peers():
_submit_state.rpc_id(picked_peer_id, state_to_broadcast, tick)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same thing as inputs. Locally, not a single RPC should be sent/received. Because we already have the state locally. So we must simply RPC to other peers, because for this tick we are RPCing, we already have set locally the variables latest_state, _states[tick]

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@rpc's call_remote makes the old rpc call work no problems, so this is needless.


# Record state for specified tick ( current + 1 )
if not _record_state_props.is_empty() and tick > _latest_state:
Expand Down Expand Up @@ -222,8 +236,16 @@ func history_cleanup() -> void:
_inputs.erase(_inputs.keys().min())

if (NetworkRollback.enable_input_serialization):
while _serialized_inputs.size() > NetworkRollback.history_limit:
_serialized_inputs.erase(_serialized_inputs.keys().min())
if (NetworkRollback.serialized_inputs_history_limit > 0):
Copy link
Contributor Author

@TheYellowArchitect TheYellowArchitect Aug 26, 2024

Choose a reason for hiding this comment

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

0 or -1 value is for saving all states/inputs (PackedByteArray format is optimal)
This is required for saving replays (well, the inputs at least, but now states are required instead until that .fire() issue is solved #253)

while _serialized_inputs.size() > NetworkRollback.serialized_inputs_history_limit:
#Would be faster if we cached the earliest key in an integer instead of searching min() each tick!
_serialized_inputs.erase(_serialized_inputs.keys().min())

if (NetworkRollback.enable_state_serialization):
if (NetworkRollback.serialized_states_history_limit > 0):
while _serialized_states.size() > NetworkRollback.serialized_states_history_limit:
#Would be faster if we cached the earliest key in an integer instead of searching min() each tick!
_serialized_states.erase(_serialized_states.keys().min())

_freshness_store.trim()

Expand Down Expand Up @@ -334,6 +356,18 @@ func _submit_raw_input(input: Dictionary, tick: int):
else:
_logger.warning("Received invalid input from %s for tick %s for %s" % [sender, tick, root.name])

@rpc("any_peer", "unreliable_ordered", "call_remote")
func _submit_serialized_state(serialized_state: PackedByteArray):
var received_tick: int = serialized_state.decode_u32(0)
var state_values_size: int = serialized_state.decode_u8(4)
var serialized_state_values: PackedByteArray = serialized_state.slice(5, 5 + state_values_size)
var byte_index: int = 5
var deserialized_state_of_this_tick: Dictionary

deserialized_state_of_this_tick = PropertiesSerializer.deserialize_multiple_properties(serialized_state_values, _record_state_props)

_submit_state(deserialized_state_of_this_tick, received_tick)

@rpc("any_peer", "unreliable_ordered", "call_remote")
func _submit_state(state: Dictionary, tick: int):
if tick > NetworkTime.tick:
Expand Down
10 changes: 5 additions & 5 deletions addons/netfox/serialization/properties-serializer.gd
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
extends Object
class_name PropertiesSerializer

static func deserialize_multiple_properties(serialized_input: PackedByteArray, auth_input_pros: Array[PropertyEntry]) -> Dictionary:
static func deserialize_multiple_properties(serialized_properties: PackedByteArray, auth_properties_template: Array[PropertyEntry]) -> Dictionary:
var reconstructed_dictionary: Dictionary = {} #Exclusively for that tick

print("Deserializing multiple properties of serialized input %s and array %s" % [serialized_input, auth_input_pros])
#print("Deserializing multiple properties of serialized input %s and array %s" % [serialized_properties, auth_properties_template])
var property_type: Variant.Type
var property_type_byte_size: int
var serialized_property: PackedByteArray
var input_byte_index: int = 0
for picked_property_entry in auth_input_pros:
for picked_property_entry in auth_properties_template:
property_type = picked_property_entry.type
property_type_byte_size = ValueToBytes.get_byte_size(property_type)
serialized_property = serialized_input.slice(input_byte_index, input_byte_index + property_type_byte_size)
serialized_property = serialized_properties.slice(input_byte_index, input_byte_index + property_type_byte_size)

var picked_value = ValueToBytes.deserialize(serialized_property, property_type)
input_byte_index += property_type_byte_size

print("path is %s and value is %s" % [picked_property_entry._path, picked_value])
#print("path is %s and value is %s" % [picked_property_entry._path, picked_value])
reconstructed_dictionary[picked_property_entry._path] = picked_value
return reconstructed_dictionary

Expand Down
43 changes: 23 additions & 20 deletions addons/netfox/serialization/value-to-bytes.gd
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ static func deserialize(serialized_value: PackedByteArray, type: Variant.Type) -

return null


static var cache_transform3d: PackedByteArray #So a PackedByteArray of 48 bytes isn't made per frame!

#This should be a godot native function imo
static func serialize(value: Variant) -> PackedByteArray:
Expand Down Expand Up @@ -162,10 +162,29 @@ static func serialize(value: Variant) -> PackedByteArray:
serialized_value.encode_float(16, (value as Transform2D).origin.x)
serialized_value.encode_float(20, (value as Transform2D).origin.y)
TYPE_BASIS:
serialized_value = encode_basis(value)
serialized_value.resize(36)
serialized_value.encode_float(0, (value as Basis).x.x)
serialized_value.encode_float(4, (value as Basis).x.y)
serialized_value.encode_float(8, (value as Basis).x.z)
serialized_value.encode_float(12, (value as Basis).y.x)
serialized_value.encode_float(16, (value as Basis).y.y)
serialized_value.encode_float(20, (value as Basis).y.z)
serialized_value.encode_float(24, (value as Basis).z.x)
serialized_value.encode_float(28, (value as Basis).z.y)
serialized_value.encode_float(32, (value as Basis).z.z)
TYPE_TRANSFORM3D:
serialized_value = encode_basis(value)
serialized_value.resize(48)
if (cache_transform3d.is_empty()): #I doubt this is faster than a malloc(48), but probably it is...
cache_transform3d.resize(48)
serialized_value = cache_transform3d
serialized_value.encode_float(0, (value as Transform3D).basis.x.x)
serialized_value.encode_float(4, (value as Transform3D).basis.x.y)
serialized_value.encode_float(8, (value as Transform3D).basis.x.z)
serialized_value.encode_float(12, (value as Transform3D).basis.y.x)
serialized_value.encode_float(16, (value as Transform3D).basis.y.y)
serialized_value.encode_float(20, (value as Transform3D).basis.y.z)
serialized_value.encode_float(24, (value as Transform3D).basis.z.x)
serialized_value.encode_float(28, (value as Transform3D).basis.z.y)
serialized_value.encode_float(32, (value as Transform3D).basis.z.z)
serialized_value.encode_float(36, (value as Transform3D).origin.x)
serialized_value.encode_float(40, (value as Transform3D).origin.y)
serialized_value.encode_float(44, (value as Transform3D).origin.z)
Expand All @@ -184,19 +203,3 @@ static func serialize(value: Variant) -> PackedByteArray:

return serialized_value

static func encode_basis(value: Basis) -> PackedByteArray:
var serialized_value: PackedByteArray
serialized_value.resize(36)
serialized_value.encode_float(0, (value as Basis).x.x)
serialized_value.encode_float(4, (value as Basis).x.y)
serialized_value.encode_float(8, (value as Basis).x.z)
serialized_value.encode_float(12, (value as Basis).y.x)
serialized_value.encode_float(16, (value as Basis).y.y)
serialized_value.encode_float(20, (value as Basis).y.z)
serialized_value.encode_float(24, (value as Basis).z.x)
serialized_value.encode_float(28, (value as Basis).z.y)
serialized_value.encode_float(32, (value as Basis).z.z)

return serialized_value

#func deserialize(serialized_properties: PackedByteArray, properties_template: PropertyEntry) -> Variant:
2 changes: 1 addition & 1 deletion addons/netfox/state-synchronizer.gd
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func _ready():
func _after_tick(_dt, tick):
if is_multiplayer_authority():
# Submit snapshot
var state = PropertySnapshot.extract(_props)
var state: Dictionary = PropertySnapshot.extract(_props)
_submit_state.rpc(state, tick)
else:
# Apply last received state
Expand Down
Loading