e7299b17e9
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)
238 lines
7.8 KiB
GDScript
238 lines
7.8 KiB
GDScript
extends Node
|
|
class_name RewindableAction
|
|
|
|
## Represents actions that may or may not happen, in a way compatible with
|
|
## rollback.
|
|
##
|
|
## @experimental: This class is experimental!
|
|
## @tutorial(RewindableAction Guide): https://foxssake.github.io/netfox/latest/netfox/nodes/rewindable-action/
|
|
|
|
# Status enum
|
|
enum {
|
|
INACTIVE,
|
|
CONFIRMING,
|
|
ACTIVE,
|
|
CANCELLING
|
|
}
|
|
|
|
var _active_ticks: _Set = _Set.new()
|
|
var _last_set_tick: int = -1
|
|
|
|
# Maps the set of changed ticks ( int ) to their state ( active / inactive )
|
|
var _state_changes: Dictionary = {}
|
|
# Maps the set of ticks queued for change ( int ) to their state ( active / inactive )
|
|
var _queued_changes: Dictionary = {}
|
|
|
|
var _has_confirmed: bool = false
|
|
var _has_cancelled: bool = false
|
|
|
|
# Maps ticks ( int ) to context values ( any type )
|
|
var _context: Dictionary = {}
|
|
var _mutated_objects: _Set = _Set.new()
|
|
|
|
var _logger := NetfoxLogger._for_netfox("RewindableAction")
|
|
|
|
# Process:
|
|
# @0: Client sends an input with fire @0
|
|
# Client toggles action to true @0
|
|
# Client runs logic locally @0
|
|
# @1: Server receives input with fire @0
|
|
# Server toggles action to true @0
|
|
# Server broadcasts toggle @0
|
|
# @2: Client receives confirm @0
|
|
# Other clients receive toggle @0
|
|
# Other clients show effect @0
|
|
|
|
## Returns the [param status] enum as string
|
|
static func status_string(status: int) -> String:
|
|
match status:
|
|
INACTIVE: return "INACTIVE"
|
|
CONFIRMING: return "CONFIRMING"
|
|
ACTIVE: return "ACTIVE"
|
|
CANCELLING: return "CANCELLING"
|
|
_: return "?"
|
|
|
|
## Toggles the action for a given [param tick]
|
|
func set_active(active: bool, tick: int = NetworkRollback.tick) -> void:
|
|
_last_set_tick = tick
|
|
|
|
if is_active(tick) == active:
|
|
return
|
|
|
|
# Update local state
|
|
if active: _active_ticks.add(tick)
|
|
else: _active_ticks.erase(tick)
|
|
|
|
# Save changes for a single loop
|
|
_state_changes[tick] = active
|
|
|
|
## Check if the action is happening for the given [param tick]
|
|
func is_active(tick: int = NetworkRollback.tick) -> bool:
|
|
return _active_ticks.has(tick)
|
|
|
|
## Check the action's status for the given [param tick]
|
|
## [br][br]
|
|
## Returns [constant ACTIVE] if the action is happening.[br]
|
|
## Returns [constant INACTIVE] if the action is not happening.[br]
|
|
## Returns [constant CONFIRMING] if the action was previously known as not
|
|
## happening, but now it is.[br]
|
|
## Returns [constant CANCELLING] if the action was previously known to be
|
|
## happening, but now it is not.[br]
|
|
## [br]
|
|
## The [constant CONFIRMING] and [constant CANCELLING] statuses may occur if the
|
|
## action was just toggled, or data was received from the action's authority.
|
|
func get_status(tick: int = NetworkRollback.tick) -> int:
|
|
var currently_active := is_active(tick)
|
|
var state_change = _state_changes.get(tick)
|
|
var queued_change = _queued_changes.get(tick)
|
|
|
|
if queued_change != null:
|
|
return CONFIRMING if queued_change else CANCELLING
|
|
if state_change != null:
|
|
return CONFIRMING if state_change else CANCELLING
|
|
return ACTIVE if currently_active else INACTIVE
|
|
|
|
## Returns true if the action has been in [constant CONFIRMING] status during
|
|
## the last tick loop
|
|
func has_confirmed() -> bool:
|
|
return _has_confirmed
|
|
|
|
## Returns true if the action has been in [constant CANCELLING] status during
|
|
## the last tick loop
|
|
func has_cancelled() -> bool:
|
|
return _has_cancelled
|
|
|
|
## Get the action's current status as a string
|
|
## [br][br]
|
|
## See also: [member get_status]
|
|
func get_status_string(tick: int = NetworkRollback.tick) -> String:
|
|
return status_string(get_status(tick))
|
|
|
|
## Returns true if the action has any stored context for the given [param tick]
|
|
func has_context(tick: int = NetworkRollback.tick) -> bool:
|
|
return _context.has(tick)
|
|
|
|
## Get the context stored for the given [param tick], or null
|
|
func get_context(tick: int = NetworkRollback.tick) -> Variant:
|
|
return _context.get(tick)
|
|
|
|
## Store [param value] as the context for the given [param tick]
|
|
func set_context(value: Variant, tick: int = NetworkRollback.tick) -> void:
|
|
_context[tick] = value
|
|
|
|
## Erase the context for the given [param tick]
|
|
func erase_context(tick: int = NetworkRollback.tick) -> void:
|
|
_context.erase(tick)
|
|
|
|
## Whenever the action happens, mutate the [param target] object
|
|
## [br][br]
|
|
## See also: [method _NetworkRollback.mutate]
|
|
func mutate(target: Object) -> void:
|
|
_mutated_objects.add(target)
|
|
|
|
## Remove the [param target] object from the list of objects to [method mutate]
|
|
## [br][br]
|
|
## See also: [method _NetworkRollback.mutate]
|
|
func dont_mutate(target: Object) -> void:
|
|
_mutated_objects.erase(target)
|
|
|
|
func _connect_signals() -> void:
|
|
NetworkRollback.before_loop.connect(_before_rollback_loop)
|
|
NetworkRollback.after_loop.connect(_after_loop)
|
|
|
|
func _disconnect_signals() -> void:
|
|
NetworkRollback.before_loop.disconnect(_before_rollback_loop)
|
|
NetworkRollback.after_loop.disconnect(_after_loop)
|
|
|
|
func _enter_tree() -> void:
|
|
_connect_signals()
|
|
|
|
func _exit_tree() -> void:
|
|
_disconnect_signals()
|
|
|
|
func _notification(what: int) -> void:
|
|
match what:
|
|
NOTIFICATION_PATH_RENAMED, NOTIFICATION_ENTER_TREE:
|
|
# Update logger name
|
|
_logger.name = "RewindableAction:" + name
|
|
|
|
func _before_rollback_loop() -> void:
|
|
_last_set_tick = -1
|
|
|
|
if not _queued_changes.is_empty():
|
|
# Resimulate from earliest change
|
|
var earliest_change = _queued_changes.keys().min()
|
|
NetworkRollback.notify_resimulation_start(earliest_change)
|
|
_logger.trace("Submitted earliest tick %d from %s", [earliest_change, _queued_changes])
|
|
|
|
# Queue mutations
|
|
for mutated in _mutated_objects:
|
|
NetworkRollback.mutate(mutated, earliest_change)
|
|
|
|
# Apply queue
|
|
for tick in _queued_changes:
|
|
set_active(_queued_changes[tick], tick)
|
|
|
|
# Queue earliest event
|
|
if not _active_ticks.is_empty():
|
|
var earliest_active = _active_ticks.min()
|
|
for mutated in _mutated_objects:
|
|
NetworkRollback.mutate(mutated, earliest_active)
|
|
|
|
func _after_loop() -> void:
|
|
# Trim history
|
|
for tick in _active_ticks:
|
|
if tick < NetworkRollback.history_start:
|
|
_active_ticks.erase(tick)
|
|
for tick in _context.keys():
|
|
if tick < NetworkRollback.history_start:
|
|
_context.erase(tick)
|
|
|
|
# Update confirmed / cancelled flags
|
|
_has_confirmed =\
|
|
_state_changes.values().any(func(it): return it == true) or \
|
|
_queued_changes.values().any(func(it): return it == true)
|
|
|
|
_has_cancelled =\
|
|
_state_changes.values().any(func(it): return it == false) or \
|
|
_queued_changes.values().any(func(it): return it == false)
|
|
|
|
# Clear changes
|
|
_state_changes.clear()
|
|
_queued_changes.clear()
|
|
|
|
# Submit
|
|
if is_multiplayer_authority() and _last_set_tick >= 0:
|
|
var serialize_from := NetworkRollback.history_start
|
|
var serialize_to := _last_set_tick
|
|
if not _active_ticks.is_empty():
|
|
serialize_to = maxi(_active_ticks.max(), serialize_to)
|
|
|
|
var active_tick_bytes = _TicksetSerializer.serialize(serialize_from, serialize_to, _active_ticks)
|
|
_submit_state.rpc(active_tick_bytes)
|
|
|
|
@rpc("authority", "unreliable_ordered", "call_remote")
|
|
func _submit_state(bytes: PackedByteArray) -> void:
|
|
# Decode incoming data
|
|
var parsed := _TicksetSerializer.deserialize(bytes)
|
|
|
|
var history_start: int = parsed[0]
|
|
var last_known_tick: int = parsed[1]
|
|
var active_ticks: _Set = parsed[2]
|
|
|
|
# Find differences and queue changes
|
|
var earliest_tick := maxi(history_start, NetworkRollback.history_start)
|
|
# Don't compare past last event, as to not cancel events the host simply doesn't know about
|
|
var latest_tick = maxi(last_known_tick, NetworkRollback.history_start)
|
|
|
|
# Add a tolerance of 4 ticks for checking if the tickset is in the future
|
|
# Server time might be ahead a tick or two under really small latencies,
|
|
# e.g. LAN
|
|
if earliest_tick > NetworkTime.tick + 4 or latest_tick > NetworkTime.tick + 4:
|
|
_logger.debug("Received tickset for range @%d>%d, which has ticks in the future!", [earliest_tick, latest_tick])
|
|
|
|
for tick in range(earliest_tick, latest_tick + 1):
|
|
var is_tick_active = active_ticks.has(tick)
|
|
if is_tick_active != is_active(tick):
|
|
_queued_changes[tick] = is_tick_active
|