Skip to content

Conversation

@elementbound
Copy link
Contributor

@elementbound elementbound commented Jan 6, 2026

This is an experimental PR, that refactors much of netfox's internals, inspired by Godot's server pattern.

While Godot had its own requirements, the interpretation here is that:

  • Servers are singletons that provide access to a group of related functionalities
  • Servers act as an interface, similar to a facade, as an outer boundary to internal details
  • Nodes act as convenient wrappers to servers, meaning they expose some config and then do some server calls based on said config

Hopefully this allows:

  • Simplified history tracking
  • Greatly simplified RollbackSynchronizer, StateSynchronizer, and PredictiveSynchronizer
  • Easier testing
  • Batching - sending state in one RPC per tick, instead of one RPC per RBS per tick
  • Nested RBS's
  • An easier fix for RollbackSynchronizer removes mutated states on rollback #383
  • Buffering incoming data in case the target node doesn't exist yet, instead of printing errors
  • Easier implementation of Rollback liveness #468

Also lays the foundations for some potentially wild features like:

  • RPCs on objects
  • Rollback on objects

TODO:

  • Diff states
  • Redundant inputs
  • Binary state serialization
  • Binary input serialization
  • Support schemas
  • Support visibility filtering
  • Networked IDs
  • Command bus
  • StateSynchronizer
  • PredictiveSynchronizer
  • Update metrics
  • Toggleable input broadcast
  • Mutations?
  • Separate into multiple PRs

Pending fixes:

  • Input prediction
  • Diff states

Sanity tasks:

@Archists-dev
Copy link

Great stuff!!

# Resolve to identifier
var identifier := NetworkIdentityServer.resolve_reference(peer, idref)
if not identifier:
# TODO: Handle unknown IDs gracefully
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Follow-up PR

if buffer == null:
buffer = StreamPeerBuffer.new()

# TODO: How about encoding the first snapshot as-is, and then the rest as diffs
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Follow-up PR

# Resolve to identifier
var identifier := NetworkIdentityServer.resolve_reference(peer, idref)
if not identifier:
# TODO: Handle unknown IDs gracefully
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Follow-up PR

func record_state(tick: int) -> void:
var input_snapshot := get_rollback_input_snapshot(tick - 1)
_record(tick, _rb_state_snapshots, _rb_state_properties, false, func(subject: Node):
if not subject.is_multiplayer_authority():
Copy link
Contributor

Choose a reason for hiding this comment

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

Server always has authority to broadcast states

Suggested change
if not subject.is_multiplayer_authority():
if subject.multiplayer.is_server():
return true
if not subject.is_multiplayer_authority():

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Might have to investigate why is_predicting() was returning true in this case. Otherwise we risk overwriting a client's prediction with ticks that the server had no input for.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Without the suggested change, RollbackSimulationServer.is_predicting() would return true for the other player's car on the server, because of this clause:

	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

So as long as we haven't received input from the client, we consider the recorded simulation predicted, which - in theory - makes sense. I guess next thing to find out is why we need to forego the above.

On clients, is_predicting() returns false for the player's car with this clause:

	if not is_owned and has_input:
		# We don't own the node, but we own input for it - not (input) predicting
		return false

Comment on lines +387 to 389
NetworkHistoryServer.restore_rollback_input(tick)
NetworkHistoryServer.restore_rollback_state(tick)
on_prepare_tick.emit(tick)
Copy link
Contributor

Choose a reason for hiding this comment

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

Signals first

Suggested change
NetworkHistoryServer.restore_rollback_input(tick)
NetworkHistoryServer.restore_rollback_state(tick)
on_prepare_tick.emit(tick)
on_prepare_tick.emit(tick)
NetworkHistoryServer.restore_rollback_input(tick)
NetworkHistoryServer.restore_rollback_state(tick)

Comment on lines +399 to 400
RollbackSimulationServer.simulate(NetworkTime.ticktime, tick)
on_process_tick.emit(tick)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
RollbackSimulationServer.simulate(NetworkTime.ticktime, tick)
on_process_tick.emit(tick)
on_process_tick.emit(tick)
RollbackSimulationServer.simulate(NetworkTime.ticktime, tick)

Comment on lines +405 to 407
NetworkHistoryServer.record_state(tick + 1)
NetworkSynchronizationServer.synchronize_state(tick + 1)
on_record_tick.emit(tick + 1)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
NetworkHistoryServer.record_state(tick + 1)
NetworkSynchronizationServer.synchronize_state(tick + 1)
on_record_tick.emit(tick + 1)
on_record_tick.emit(tick + 1)
NetworkHistoryServer.record_state(tick + 1)
NetworkSynchronizationServer.synchronize_state(tick + 1)

Comment on lines +411 to 413
NetworkHistoryServer.restore_rollback_state(display_tick)
RollbackSimulationServer.trim_ticks_simulated(history_start)
after_loop.emit()
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
NetworkHistoryServer.restore_rollback_state(display_tick)
RollbackSimulationServer.trim_ticks_simulated(history_start)
after_loop.emit()
after_loop.emit()
NetworkHistoryServer.restore_rollback_state(display_tick)
RollbackSimulationServer.trim_ticks_simulated(history_start)

Comment on lines +193 to +194
"name": "NetworkSynchronizationServer",
"path": ROOT + "/servers/rollback-synchronization-server.gd"
Copy link
Contributor

Choose a reason for hiding this comment

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

File change fix

Suggested change
"name": "NetworkSynchronizationServer",
"path": ROOT + "/servers/rollback-synchronization-server.gd"
"name": "NetworkSynchronizationServer",
"path": ROOT + "/servers/network-synchronization-server.gd"

Comment on lines +559 to 560
NetworkHistoryServer.restore_synchronizer_state(tick)
before_tick_loop.emit()
Copy link
Contributor

Choose a reason for hiding this comment

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

signals before rs's

Suggested change
NetworkHistoryServer.restore_synchronizer_state(tick)
before_tick_loop.emit()
before_tick_loop.emit()
NetworkHistoryServer.restore_synchronizer_state(tick)

Comment on lines +566 to 568
NetworkHistoryServer.record_sync_state(tick + 1)
NetworkSynchronizationServer.synchronize_sync_state(tick + 1)
after_tick.emit(ticktime, tick)
Copy link
Contributor

Choose a reason for hiding this comment

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

here too

Suggested change
NetworkHistoryServer.record_sync_state(tick + 1)
NetworkSynchronizationServer.synchronize_sync_state(tick + 1)
after_tick.emit(ticktime, tick)
after_tick.emit(ticktime, tick)
NetworkHistoryServer.record_sync_state(tick + 1)
NetworkSynchronizationServer.synchronize_sync_state(tick + 1)

Comment on lines +575 to 576
NetworkHistoryServer.restore_synchronizer_state(tick)
after_tick_loop.emit()
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
NetworkHistoryServer.restore_synchronizer_state(tick)
after_tick_loop.emit()
after_tick_loop.emit()
NetworkHistoryServer.restore_synchronizer_state(tick)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants