b0c83af092
Complete replacement of the tactical-shooter project with the netfox-cs-sample (MIT) — a CS 1.6 inspired multiplayer FPS built with Godot 4 and netfox. ## What's new - Full CS-style gameplay: teams (T/CT), rounds, economy, buy menu - 6 weapons: Knife, Glock, USP, AK-47, M4A1, AWP - Bomb plant/defuse with 2 bombsites - Flashbang & smoke grenades - Proper netfox rollback netcode at 64 tick - Network popup UI for host/join - HUD, crosshair, round timer, scoreboard - All netfox singletons registered as autoloads (works in exported builds) ## Architecture - Listen-server (host from client, no dedicated server binary) - Multiplayer-fps game lives at examples/multiplayer-fps/ - Netfox addons registered as autoloads for exported build compat - Godot 4.7 with Forward+ renderer ## Removed - Old headless-server architecture (client_main, server_main, player.gd, etc.) - Custom netfox bootstrap with ENet fallback - Old ChaffGames FPS template (2,420 lines, 844 KB) - SimulationServer GDExtension stub - Godot-jolt physics (netfox sample uses default Godot physics) - Duplicate weapon_data.gd, anti_cheat.gd, round_manager.gd, etc. - Server browser API Python venv (87 MB) - test_range map and modular assets ## Preserved - Git history - Server config at config/default_server_config.cfg - Windows export preset - Build directory (gitignored) Co-authored-by: naxIO <naxIO@users.noreply.github.com>
180 lines
6.6 KiB
GDScript
180 lines
6.6 KiB
GDScript
extends Node
|
|
class_name _NetworkCommandServer
|
|
|
|
## Transmits commands over the network
|
|
##
|
|
## Commands are a simpler, lightweight alternative to RPCs. Commands consist of
|
|
## a single byte for ID, and the raw binary data. The ID lets the receiving peer
|
|
## decide what to execute, with the binary data serving as the input.
|
|
## [br][br]
|
|
## Being a simpler construct makes commands a good fit for regular, fundamental
|
|
## operations.
|
|
## [br][br]
|
|
## Commands are, by default, transmitted over regular RPCs. To use less data,
|
|
## commands can also be transmitted as raw packets, using
|
|
## [method SceneMultiplayer.send_bytes]. This is an opt-in feature - if the game
|
|
## is already using [method SceneMultiplayer.send_bytes], it needs to be aware
|
|
## of commands, and must check each packet whether it's a command or one of its
|
|
## own packets. To check if a packet is a command, use [method
|
|
## is_command_packet].
|
|
|
|
var _packet_prefix := PackedByteArray([0, 78, 70]) # "\0nf"
|
|
var _next_idx := 0
|
|
var _rpc_transport := _RPCTransport.new()
|
|
var _packet_transport := _PacketTransport.new(_packet_prefix)
|
|
var _commands := {} # id to `Command`
|
|
|
|
var _use_raw := ProjectSettings.get_setting("netfox/general/use_raw_commands", false) as bool
|
|
|
|
static var _logger := NetfoxLogger._for_netfox("NetworkCommandServer")
|
|
|
|
func _ready():
|
|
add_child(_rpc_transport, true)
|
|
add_child(_packet_transport, true)
|
|
|
|
_rpc_transport.on_receive.connect(_handle_command)
|
|
_packet_transport.on_receive.connect(_handle_command)
|
|
|
|
## Register a command at the next available ID
|
|
func register_command(handler: Callable, mode: MultiplayerPeer.TransferMode = MultiplayerPeer.TRANSFER_MODE_RELIABLE, channel: int = 0) -> Command:
|
|
var idx := _next_idx
|
|
_next_idx += 1
|
|
return register_command_at(idx, handler)
|
|
|
|
## Register a command at a specific index
|
|
## [br][br]
|
|
## A specific ID should only be registered once. Doing otherwise will trigger an
|
|
## assert in the editor, but will overwrite the previous command in release.
|
|
func register_command_at(idx: int, handler: Callable, mode: MultiplayerPeer.TransferMode = MultiplayerPeer.TRANSFER_MODE_RELIABLE, channel: int = 0) -> Command:
|
|
assert(not _commands.has(idx), "Command #%d is already taken!" % idx)
|
|
var command := Command.new(self, idx, handler, mode, channel)
|
|
_commands[idx] = command
|
|
|
|
_next_idx = maxi(_next_idx, idx + 1)
|
|
|
|
return command
|
|
|
|
## Send a command with index and data
|
|
func send_command(idx: int, data: PackedByteArray, target_peer: int = 0, mode: MultiplayerPeer.TransferMode = MultiplayerPeer.TRANSFER_MODE_RELIABLE, channel: int = 0) -> void:
|
|
if _use_raw:
|
|
_packet_transport.send(idx, data, target_peer, mode, channel)
|
|
else:
|
|
_rpc_transport.send(idx, data, target_peer, mode, channel)
|
|
|
|
## Return true if [param packet] is a command packet
|
|
## [br][br]
|
|
## Always returns [code]true[/code] if RPCs are used for transmitting commands.
|
|
func is_command_packet(packet: PackedByteArray) -> bool:
|
|
if not _use_raw:
|
|
return true
|
|
return _packet_transport.is_command_packet(packet)
|
|
|
|
## Return the prefix bytes for command packets
|
|
## [br][br]
|
|
## Can be used to avoid conflicts between command packets and game packets.
|
|
func get_command_packet_prefix() -> PackedByteArray:
|
|
return _packet_prefix
|
|
|
|
func _handle_command(sender: int, idx: int, data: PackedByteArray) -> void:
|
|
var command := _commands.get(idx) as Command
|
|
if not command:
|
|
_logger.error("Received unknown command #%d!", [idx])
|
|
return
|
|
command._handle(sender, data)
|
|
|
|
## Networked command
|
|
##
|
|
## Provides a convenient interface for sending and managing networked commands.
|
|
## [br][br]
|
|
## Should not be instantiated manually.
|
|
class Command:
|
|
var _command_server: _NetworkCommandServer
|
|
|
|
var _idx: int
|
|
var _handler: Callable
|
|
var _mode: MultiplayerPeer.TransferMode
|
|
var _channel: int
|
|
|
|
func _init(p_command_server: _NetworkCommandServer, p_idx: int, p_handler: Callable, p_mode: MultiplayerPeer.TransferMode, p_channel: int):
|
|
_command_server = p_command_server
|
|
_idx = p_idx
|
|
_handler = p_handler
|
|
_mode = p_mode
|
|
_channel = p_channel
|
|
|
|
## Send command
|
|
func send(data: PackedByteArray, target_peer: int = 0) -> void:
|
|
_command_server.send_command(_idx, data, target_peer, _mode, _channel)
|
|
|
|
func _handle(sender: int, data: PackedByteArray) -> void:
|
|
_handler.call(sender, data)
|
|
|
|
class _Transport extends Node:
|
|
signal on_receive(idx: int, data: PackedByteArray)
|
|
|
|
func send(idx: int, data: PackedByteArray, target_peer: int, mode: MultiplayerPeer.TransferMode, channel: int) -> void:
|
|
pass
|
|
|
|
class _PacketTransport extends _Transport:
|
|
var _packet_prefix: PackedByteArray
|
|
|
|
func _init(p_packet_prefix: PackedByteArray):
|
|
_packet_prefix = p_packet_prefix
|
|
|
|
func _ready():
|
|
(multiplayer as SceneMultiplayer).peer_packet.connect(_handle_packet)
|
|
|
|
func send(idx: int, data: PackedByteArray, target_peer: int, mode: MultiplayerPeer.TransferMode, channel: int) -> void:
|
|
var buffer := StreamPeerBuffer.new()
|
|
buffer.put_data(_packet_prefix)
|
|
buffer.put_u8(idx)
|
|
buffer.put_data(data)
|
|
|
|
(multiplayer as SceneMultiplayer).send_bytes(buffer.data_array, target_peer, mode, channel)
|
|
|
|
func is_command_packet(packet: PackedByteArray) -> bool:
|
|
if packet.size() < _packet_prefix.size():
|
|
return false
|
|
|
|
for i in _packet_prefix.size():
|
|
if packet[i] != _packet_prefix[i]:
|
|
return false
|
|
return true
|
|
|
|
func _handle_packet(peer: int, packet: PackedByteArray) -> void:
|
|
var buffer := StreamPeerBuffer.new()
|
|
buffer.data_array = packet
|
|
|
|
# Check header
|
|
if not is_command_packet(packet):
|
|
return
|
|
|
|
# Grab data
|
|
buffer.seek(_packet_prefix.size())
|
|
var idx := buffer.get_u8()
|
|
var data := buffer.get_partial_data(buffer.get_available_bytes())[1] as PackedByteArray
|
|
|
|
on_receive.emit(peer, idx, data)
|
|
|
|
class _RPCTransport extends _Transport:
|
|
func send(idx: int, data: PackedByteArray, target_peer: int, mode: MultiplayerPeer.TransferMode, _channel: int) -> void:
|
|
match mode:
|
|
MultiplayerPeer.TRANSFER_MODE_UNRELIABLE: _submit_unreliable.rpc_id(target_peer, idx, data)
|
|
MultiplayerPeer.TRANSFER_MODE_UNRELIABLE_ORDERED: _submit_unreliable_ordered.rpc_id(target_peer, idx, data)
|
|
MultiplayerPeer.TRANSFER_MODE_RELIABLE: _submit_reliable.rpc_id(target_peer, idx, data)
|
|
|
|
@rpc("any_peer", "call_remote", "unreliable")
|
|
func _submit_unreliable(idx: int, data: PackedByteArray):
|
|
var sender := multiplayer.get_remote_sender_id()
|
|
on_receive.emit(sender, idx, data)
|
|
|
|
@rpc("any_peer", "call_remote", "unreliable_ordered")
|
|
func _submit_unreliable_ordered(idx: int, data: PackedByteArray):
|
|
var sender := multiplayer.get_remote_sender_id()
|
|
on_receive.emit(sender, idx, data)
|
|
|
|
@rpc("any_peer", "call_remote", "reliable")
|
|
func _submit_reliable(idx: int, data: PackedByteArray):
|
|
var sender := multiplayer.get_remote_sender_id()
|
|
on_receive.emit(sender, idx, data)
|