Phase 7: netfox + godot-jolt stack upgrade
Stack installed: - netfox v1.35.3 (core + extras + noray + internals) - godot-jolt v0.16.0-stable Architecture: - Server: ENet transport (works headless, no netfox deps) - Client/Editor: netfox rollback (RollbackSynchronizer, TickInterpolator) New/modified: - docs/migration-netfox-plan.md — migration architecture - scripts/network/network_manager.gd — netfox-aware ENet fallback - scripts/network/player.gd — clean base player - client/characters/player_netfox.gd — rollback player w/ WeaponManager - client/characters/input/player_net_input.gd — BaseNetInput subclass - client/characters/character/fps_character_controller.gd — netfox input feed - client/weapons/ — weapon data, registry, TacticalWeaponHitscan, WeaponManager - client/scripts/round_replicator.gd — client-side round state bridge - server/scripts/round_manager.gd — improved state machine - server/scripts/plugin_api/plugin_manager.gd — refined plugin system - config: enemy_tag, ally_tag for meatball targeting Removed: old C++ SimulationServer GDExtension (replaced by netfox rollback)
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
@tool
|
||||
@icon("res://addons/netfox.extras/icons/rewindable-state-machine.svg")
|
||||
extends Node
|
||||
class_name RewindableStateMachine
|
||||
|
||||
## A state machine that can be used with rollback.
|
||||
##
|
||||
## It relies on [RollbackSynchronizer] to manage its state. State transitions
|
||||
## are only triggered by gameplay code, and not by rollback reverting to an
|
||||
## earlier state.
|
||||
## [br][br]
|
||||
## For this node to work correctly, a [RollbackSynchronizer] must be added as
|
||||
## a sibling, and it must have the [RewindableStateMachine]'s [member state]
|
||||
## property configured as a state property.
|
||||
## [br][br]
|
||||
## To implement states, extend the [RewindableState] class and add it as a child
|
||||
## node.
|
||||
##
|
||||
## @tutorial(RewindableStateMachine Guide): https://foxssake.github.io/netfox/latest/netfox.extras/guides/rewindable-state-machine/
|
||||
|
||||
## Name of the current state.
|
||||
##
|
||||
## Can be an empty string if no state is active. Only modify directly if you
|
||||
## need to skip [method transition]'s callbacks.
|
||||
@export var state: StringName = "":
|
||||
get: return _state_object.name if _state_object != null else ""
|
||||
set(v): _set_state(v)
|
||||
|
||||
## Emitted during state transitions.
|
||||
##
|
||||
## This signal can be used to run gameplay code on state changes.
|
||||
## [br][br]
|
||||
## This signal is emitted whenever a transition happens during rollback, which
|
||||
## means it may be emitted multiple times for the same transition if it gets
|
||||
## resimulated during rollback.
|
||||
## [br][br]
|
||||
## [b]State changes are not necessarily emitted on all peers.[/b]
|
||||
## See: [url=https://foxssake.github.io/netfox/netfox.extras/guides/rewindable-state-machine/#caveats]RewindableStateMachine caveats[/url]
|
||||
signal on_state_changed(old_state: RewindableState, new_state: RewindableState)
|
||||
|
||||
## Emitted after the displayed state has changed.
|
||||
##
|
||||
## This signal can be used to update visuals based on state changes.
|
||||
## [br][br]
|
||||
## This signal is emitted whenever the state after a tick loop has changed.
|
||||
signal on_display_state_changed(old_state: RewindableState, new_state: RewindableState)
|
||||
|
||||
static var _logger: NetfoxLogger = NetfoxLogger._for_extras("RewindableStateMachine")
|
||||
|
||||
var _state_object: RewindableState = null
|
||||
var _previous_state_object: RewindableState = null
|
||||
var _available_states: Dictionary = {}
|
||||
var _prevent_transition: bool = false
|
||||
var _prevent_callable: Callable = func(): _prevent_transition = true
|
||||
|
||||
## Transition to a new state specified by [param new_state_name] and return
|
||||
## true.
|
||||
##
|
||||
## Finds the given state by name and transitions to it if possible. The new
|
||||
## state's [method RewindableState.can_enter] callback decides if it can be
|
||||
## entered from the current state.
|
||||
## [br][br]
|
||||
## Upon transitioning, [method RewindableState.exit] is called on the old state,
|
||||
## and [method RewindableState.enter] is called on the new state. In addition,
|
||||
## [signal on_state_changed] is emitted.
|
||||
## [br][br]
|
||||
## Does nothing if transitioning to the currently active state. Emits a warning
|
||||
## and does nothing when transitioning to an unknown state.
|
||||
func transition(new_state_name: StringName) -> bool:
|
||||
# Check if target state is valid
|
||||
if state == new_state_name:
|
||||
return false
|
||||
|
||||
if not _available_states.has(new_state_name):
|
||||
_logger.warning("Attempted to transition from state '%s' into unknown state '%s'", [state, new_state_name])
|
||||
return false
|
||||
|
||||
var from_state = _state_object
|
||||
var new_state: RewindableState = _available_states[new_state_name]
|
||||
_prevent_transition = false
|
||||
|
||||
# Validate transition
|
||||
if from_state:
|
||||
if !new_state.can_enter(_state_object):
|
||||
return false
|
||||
|
||||
# Emit exit signal, allow handlers to prevent transition
|
||||
_state_object.on_exit.emit(new_state, NetworkRollback.tick, _prevent_callable)
|
||||
if _prevent_transition: return false
|
||||
|
||||
new_state.on_enter.emit(from_state, NetworkRollback.tick, _prevent_callable)
|
||||
if _prevent_transition: return false
|
||||
|
||||
# Transition valid, run callbacks
|
||||
if is_instance_valid(from_state):
|
||||
from_state.exit(new_state, NetworkRollback.tick)
|
||||
new_state.enter(from_state, NetworkRollback.tick)
|
||||
|
||||
# Set new state
|
||||
_state_object = new_state
|
||||
on_state_changed.emit(from_state, new_state)
|
||||
|
||||
return true
|
||||
|
||||
## Update the internal cache of known states
|
||||
## [br][br]
|
||||
## Automatically called on ready and when a child node is added or removed. Call
|
||||
## manually to force an update.
|
||||
func update_states() -> void:
|
||||
_available_states.clear()
|
||||
|
||||
for child in find_children("*", "RewindableState", false):
|
||||
_available_states[child.name] = child
|
||||
|
||||
func _notification(what: int):
|
||||
# Use notification instead of _ready, so users can write their own _ready
|
||||
# callback without having to call super()
|
||||
if Engine.is_editor_hint(): return
|
||||
|
||||
match what:
|
||||
NOTIFICATION_CHILD_ORDER_CHANGED:
|
||||
update_states()
|
||||
NOTIFICATION_ENTER_TREE:
|
||||
# Compare states after tick loop
|
||||
NetworkTime.after_tick_loop.connect(_after_tick_loop)
|
||||
update_states()
|
||||
NOTIFICATION_EXIT_TREE:
|
||||
# Disconnect handlers
|
||||
NetworkTime.after_tick_loop.disconnect(_after_tick_loop)
|
||||
|
||||
func _get_configuration_warnings():
|
||||
const MISSING_SYNCHRONIZER_ERROR := \
|
||||
"RewindableStateMachine is not managed by a RollbackSynchronizer! Add it as a sibling node to fix this."
|
||||
const INVALID_SYNCHRONIZER_CONFIG_ERROR := \
|
||||
"RollbackSynchronizer configuration is invalid, it can't manage this state machine!" +\
|
||||
"\nNote: You may need to reload this scene after fixing for this warning to disappear."
|
||||
const MISSING_PROPERTY_ERROR := \
|
||||
"State is not managed by RollbackSynchronizer! Add `state` property to the synchronizer to fix this. " +\
|
||||
"\nNote: You may need to reload this scene after fixing for this warning to disappear."
|
||||
|
||||
# Check if there's a rollback synchronizer
|
||||
var rollback_synchronizer_node = get_parent().find_children("*", "RollbackSynchronizer", false).pop_front()
|
||||
if not rollback_synchronizer_node:
|
||||
return [MISSING_SYNCHRONIZER_ERROR]
|
||||
|
||||
var rollback_synchronizer := rollback_synchronizer_node as RollbackSynchronizer
|
||||
|
||||
# Check if its configuration is valid
|
||||
# TODO: Expose this as a property?
|
||||
if not rollback_synchronizer.root:
|
||||
return [INVALID_SYNCHRONIZER_CONFIG_ERROR]
|
||||
|
||||
# Check if it manages our `state` property
|
||||
for state_property_path in rollback_synchronizer.state_properties:
|
||||
var property = PropertyEntry.parse(rollback_synchronizer.root, state_property_path)
|
||||
if property.node == self and property.property == "state":
|
||||
return []
|
||||
|
||||
return [MISSING_PROPERTY_ERROR]
|
||||
|
||||
func _rollback_tick(delta: float, tick: int, is_fresh: bool) -> void:
|
||||
if _state_object:
|
||||
_state_object.tick(delta, tick, is_fresh)
|
||||
_state_object.on_tick.emit(delta, tick, is_fresh)
|
||||
|
||||
func _after_tick_loop():
|
||||
if _state_object != _previous_state_object:
|
||||
on_display_state_changed.emit(_previous_state_object, _state_object)
|
||||
|
||||
if _previous_state_object:
|
||||
_previous_state_object.on_display_exit.emit(_state_object, NetworkTime.tick)
|
||||
_previous_state_object.display_exit(_state_object, NetworkTime.tick)
|
||||
|
||||
_state_object.on_display_enter.emit(_previous_state_object, NetworkTime.tick)
|
||||
_state_object.display_enter(_previous_state_object, NetworkTime.tick)
|
||||
|
||||
_previous_state_object = _state_object
|
||||
|
||||
func _set_state(new_state: StringName) -> void:
|
||||
if not new_state:
|
||||
return
|
||||
|
||||
if not _available_states.has(new_state):
|
||||
_logger.warning("Attempted to jump to unknown state: %s", [new_state])
|
||||
return
|
||||
|
||||
_state_object = _available_states[new_state]
|
||||
@@ -0,0 +1 @@
|
||||
uid://byrgwv2o7hstx
|
||||
@@ -0,0 +1,114 @@
|
||||
@tool
|
||||
@icon("res://addons/netfox.extras/icons/rewindable-state.svg")
|
||||
extends Node
|
||||
class_name RewindableState
|
||||
|
||||
## Base class for states to be used with [RewindableStateMachine].
|
||||
##
|
||||
## Provides multiple callback methods for a state's lifecycle, which can be
|
||||
## overridden by extending classes.
|
||||
## [br][br]
|
||||
## Must have a [RewindableStateMachine] as a parent.
|
||||
##
|
||||
## @tutorial(RewindableStateMachine Guide): https://foxssake.github.io/netfox/latest/netfox.extras/guides/rewindable-state-machine/
|
||||
|
||||
## Emitted when entering the state
|
||||
signal on_enter(previous_state: RewindableState, tick: int, prevent: Callable)
|
||||
|
||||
## Emitted on every rollback tick while the state is active
|
||||
signal on_tick(delta: float, tick: int, is_fresh: bool)
|
||||
|
||||
## Emitted when exiting the state
|
||||
signal on_exit(next_state: RewindableState, tick: int, prevent: Callable)
|
||||
|
||||
## Emitted before displaying this state
|
||||
signal on_display_enter(previous_state: RewindableState, tick: int)
|
||||
|
||||
## Emitted before displaying another state
|
||||
signal on_display_exit(next_state: RewindableState, tick: int)
|
||||
|
||||
## The [RewindableStateMachine] this state belongs to.
|
||||
## [br][br]
|
||||
## [i]read-only[/i]
|
||||
var state_machine: RewindableStateMachine:
|
||||
get: return _state_machine
|
||||
|
||||
var _state_machine: RewindableStateMachine
|
||||
|
||||
## Callback to run a single tick.
|
||||
##
|
||||
## This method is called by the [RewindableStateMachine] during the rollback
|
||||
## tick loop to update game state.
|
||||
## [br][br]
|
||||
## [i]override[/i] to implement game logic
|
||||
func tick(delta: float, tick: int, is_fresh: bool) -> void:
|
||||
pass
|
||||
|
||||
## Callback for entering the state.
|
||||
##
|
||||
## This method is called whenever the state machine enters this state.
|
||||
## [br][br]
|
||||
## It is best practice to only modify game state here, i.e. properties that are
|
||||
## configured as state in a [RollbackSynchronizer].
|
||||
## [br][br]
|
||||
## [i]override[/i] to implement game logic reacting to state transitions
|
||||
func enter(previous_state: RewindableState, tick: int) -> void:
|
||||
pass
|
||||
|
||||
## Callback for exiting the state.
|
||||
##
|
||||
## This method is called whenever the state machine exits this state.
|
||||
## [br][br]
|
||||
## It is best practice to only modify game state here, i.e. properties that are
|
||||
## configured as state in a [RollbackSynchronizer].
|
||||
## [br][br]
|
||||
## [i]override[/i] to implement game logic reacting to state transitions
|
||||
func exit(next_state: RewindableState, tick: int) -> void:
|
||||
pass
|
||||
|
||||
## Callback for validating state transitions.
|
||||
##
|
||||
## Whenever the [RewindableStateMachine] attempts to enter this state, it will
|
||||
## call this method to ensure that the transition is valid.
|
||||
## [br][br]
|
||||
## If this method returns true, the transition is valid and the state machine
|
||||
## will enter this state. Otherwise, the transition is invalid, and nothing
|
||||
## happens.
|
||||
## [br][br]
|
||||
## [i]override[/i] to implement custom transition validation logic
|
||||
func can_enter(previous_state: RewindableState) -> bool:
|
||||
# Add your validation logic here
|
||||
# Return true if the state machine can transition to the next state
|
||||
return true
|
||||
|
||||
## Callback for displaying the state.
|
||||
##
|
||||
## After each tick loop, the [RewindableStateMachine] checks the final state,
|
||||
## i.e. the state that will be active until the next tick loop. If that state
|
||||
## has changed [b]to[/b] this one, the [RewindableStateMachine] will call this
|
||||
## method.
|
||||
## [br][br]
|
||||
## [i]override[/i] to implement visuals / effects reacting to state transitions
|
||||
func display_enter(previous_state: RewindableState, tick: int) -> void:
|
||||
pass
|
||||
|
||||
## Callback for displaying a different state.
|
||||
##
|
||||
## After each tick loop, the [RewindableStateMachine] checks the final state,
|
||||
## i.e. the state that will be active until the next tick loop. If that state
|
||||
## has changed [b]from[/b] this one, the [RewindableStateMachine] will call this
|
||||
## method.
|
||||
## [br][br]
|
||||
## [i]override[/i] to implement visuals / effects reacting to state transitions
|
||||
func display_exit(next_state: RewindableState, tick: int) -> void:
|
||||
pass
|
||||
|
||||
func _get_configuration_warnings():
|
||||
return [] if get_parent() is RewindableStateMachine else ["This state should be a child of a RewindableStateMachine."]
|
||||
|
||||
func _notification(what: int):
|
||||
# Use notification instead of _ready, so users can write their own _ready
|
||||
# callback without having to call super()
|
||||
if what == NOTIFICATION_READY:
|
||||
if _state_machine == null and get_parent() is RewindableStateMachine:
|
||||
_state_machine = get_parent()
|
||||
@@ -0,0 +1 @@
|
||||
uid://usyufdtn83hc
|
||||
Reference in New Issue
Block a user