Files
tactical-shooter/addons/netfox/peer-visibility-filter.gd
T
shawn e7299b17e9 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)
2026-07-02 17:39:22 -04:00

190 lines
6.6 KiB
GDScript

extends Node
class_name PeerVisibilityFilter
## Tracks visibility for multiplayer peers
##
## Similar in how [MultiplayerSynchronizer] handles visibility. It decides peer
## visibility based on individual overrides and filters.
## [br][br]
## By default, each peer's visibility is determined by
## [member default_visibility]. [br][br]
## The default visibility can be overridden for individual peers using
## [method set_visibility_for] and [method unset_visibility_for].
## [br][br]
## Individual overrides can still be rejected by [i]filters[/i], which are
## callables that can dynamically determine the visibility for each peer. If any
## of the registered filters return [code]false[/code], the peer will not be
## visible. Filters can be managed using [member add_visibility_filter] and
## [member remove_visibility_filter].
## [br][br]
## To avoid taking up too much CPU time, visibilities are only recalculated on
## a peer join or peer leave event by default. This can be changed by setting
## [member update_mode]. Visibilities can also be manually updated using
## [member update_visibility].
## Contains different options for when to automatically update visibility
enum UpdateMode {
NEVER, ## Only update visibility when manually triggered
ON_PEER, ## Update visibility when a peer joins or leaves
PER_TICK_LOOP, ## Update visibility before each tick loop
PER_TICK, ## Update visibility before each network tick
PER_ROLLBACK_TICK ## Update visibility [b]after[/b] each rollback tick
}
## Make all peers visible by default if true
var default_visibility: bool = true
## Sets whether and when automatic visibility updates should happen
var update_mode: UpdateMode = UpdateMode.ON_PEER:
get = get_update_mode, set = set_update_mode
var _visibility_filters: Array[Callable] = []
var _visibility_overrides: Dictionary = {}
var _update_mode: UpdateMode = UpdateMode.ON_PEER
var _visible_peers: Array[int] = []
var _rpc_target_peers: Array[int] = []
## Register a visibility filter
## [br][br]
## The [param filter] must take a single [code]peer_id[/code] parameter, and
## return true if the given peer should be visible. The same [param filter]
## won't be added multiple times.
func add_visibility_filter(filter: Callable) -> void:
if not _visibility_filters.has(filter):
_visibility_filters.append(filter)
## Remove a visibility filter
## [br][br]
## If the visibility filter wasn't already registered, nothing happens.
func remove_visibility_filter(filter: Callable) -> void:
_visibility_filters.erase(filter)
## Remove all previously registered visibility filters
func clear_visibility_filters() -> void:
_visibility_filters.clear()
## Return true if the peer is visible
## [br][br]
## This method always reevaluates visibility.
func get_visibility_for(peer: int) -> bool:
for filter in _visibility_filters:
if not filter.call(peer):
return false
return _visibility_overrides.get(peer, default_visibility)
## Set visibility override for a given [param peer]
func set_visibility_for(peer: int, visibility: bool) -> void:
if peer == 0:
default_visibility = visibility
else:
_visibility_overrides[peer] = visibility
## Remove visibility override for a given [param peer]
## [br][br]
## If the [param peer] had no override previously, nothing happens.
func unset_visibility_for(peer: int) -> void:
_visibility_overrides.erase(peer)
## Recalculate visibility for each known peer
func update_visibility(peers: PackedInt32Array = multiplayer.get_peers()) -> void:
# Find visible peers
_visible_peers.clear()
for peer in peers:
if get_visibility_for(peer):
_visible_peers.append(peer)
# Decide how many RPC calls are needed to cover visible peers
if _visible_peers.size() == peers.size():
# Everyone is visible -> broadcast
_rpc_target_peers = [MultiplayerPeer.TARGET_PEER_BROADCAST]
elif _visible_peers.size() == peers.size() - 1:
# Only a single peer is missing, exclude that
for peer in peers:
if not _visible_peers.has(peer):
_rpc_target_peers = [-peer]
break
else:
# Custom list, can't optimize RPC call count
_rpc_target_peers = _visible_peers
# Don't include self in RPC target list
if multiplayer:
_rpc_target_peers.erase(multiplayer.get_unique_id())
## Return a list of visible peers
## [br][br]
## This list is only recalculated when [method update_visibility] runs, either
## by calling it manually, or via [member update_mode].
func get_visible_peers() -> Array[int]:
return _visible_peers
## Return a list of visible peers for use with RPCs
## [br][br]
## In contrast to [method get_visible_peers], this method will utilize Godot's
## RPC target peer rules to produce a shorter list if possible. For example, if
## all peers are visible, it will simply return [code][0][/code], indicating
## a broadcast.
## [br][br]
## This list will never explicitly include the local peer.
func get_rpc_target_peers() -> Array[int]:
return _rpc_target_peers
## Set update mode
func set_update_mode(mode: UpdateMode) -> void:
_disconnect_update_handlers(_update_mode)
_connect_update_handlers(mode)
_update_mode = mode
## Return the update mode
func get_update_mode() -> UpdateMode:
return _update_mode
func _enter_tree():
_connect_update_handlers(update_mode)
if multiplayer:
update_visibility()
func _exit_tree():
_disconnect_update_handlers(update_mode)
func _disconnect_update_handlers(mode: UpdateMode) -> void:
match mode:
UpdateMode.NEVER: pass
UpdateMode.ON_PEER:
multiplayer.peer_connected.disconnect(_handle_peer_connect)
multiplayer.peer_disconnected.disconnect(_handle_peer_disconnect)
UpdateMode.PER_TICK_LOOP:
NetworkTime.before_tick_loop.disconnect(update_visibility)
UpdateMode.PER_TICK:
NetworkTime.before_tick.disconnect(_handle_tick)
UpdateMode.PER_ROLLBACK_TICK:
NetworkRollback.after_process_tick.disconnect(_handle_rollback_tick)
_:
assert(false, "Unhandled update mode! %d" % [update_mode])
func _connect_update_handlers(mode: UpdateMode) -> void:
match mode:
UpdateMode.NEVER: pass
UpdateMode.ON_PEER:
multiplayer.peer_connected.connect(_handle_peer_connect)
multiplayer.peer_disconnected.connect(_handle_peer_disconnect)
UpdateMode.PER_TICK_LOOP:
NetworkTime.before_tick_loop.connect(update_visibility)
UpdateMode.PER_TICK:
NetworkTime.before_tick.connect(_handle_tick)
UpdateMode.PER_ROLLBACK_TICK:
NetworkRollback.after_process_tick.connect(_handle_rollback_tick)
_:
assert(false, "Unhandled update mode! %d" % [update_mode])
func _handle_peer_connect(__) -> void:
update_visibility()
func _handle_peer_disconnect(__) -> void:
update_visibility()
func _handle_tick(_dt, _t) -> void:
update_visibility()
func _handle_rollback_tick(__) -> void:
update_visibility()