Fresh start: replace with naxIO/netfox-cs-sample foundation

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>
This commit is contained in:
2026-07-02 20:55:20 -04:00
parent ce39b237c3
commit b0c83af092
4416 changed files with 57418 additions and 902676 deletions
+181
View File
@@ -0,0 +1,181 @@
# Noray
Singleton providing [noray] integration.
*noray* is a backend application that orchestrates connection between players.
To do this, players send a connection request to *noray*, and in turn *noray*
sends the players' external addresses to eachother. It is then up to the
players to conduct a handshake process.
If the handshake fails, players can request a *relay* from *noray*. In these
cases, *noray* will receive data from one player and forward it to the other,
acting as a middle man.
## Identifiers
*noray* identifies players with two different IDs: OpenID and PrivateID.
*OpenID* is public, and can be shared with other players. This ID is used to
identify hosts when connecting to games.
*PrivateID* is only sent to the player it identifies and should **never** be
shared. Acts similar to a password, and is used to authorize commands.
## Relays and NAT Punchthrough
*noray* provides two methods of connecting players.
*NAT Punchthrough* relies on the NAT table. Players must continuously send data
to eachother until either two-way communication is established, or a timeout is
reached. For certain router setups, NAT punchthrough does not work.
See: [NAT Punch-through for Multiplayer Games]
For *relays*, *noray* allocates a specific port to a given player. When *noray*
receives data on this port, it will forward it as-is to the player. As long as
*noray* is accessible over the internet, relays should work reliably no matter
the router setup.
## Registering with noray
To start using *noray*, connect to a *noray* server, request IDs by
registering, and then register the remote address:
```gdscript
var host = "some.noray.host"
var port = 8890
var err = OK
# Connect to noray
err = await Noray.connect_to_host(host, port)
if err != OK:
return err # Failed to connect
# Register host
Noray.register_host()
await Noray.on_pid
# Register remote address
# This is where noray will direct traffic
err = await Noray.register_remote()
if err != OK:
return err # Failed to register
```
By calling `Noray.register_host()`, a request is sent to *noray*. Once a
response is received, both the `on_pid` and `on_oid` signals are fired, for
receiving the PrivateID and OpenID respectively.
The remote address must be registered so that *noray* knows where to direct
other players wanting to connect. This process also sets `Noray.local_port`,
which is where traffic can be received through *noray*.
## Starting a host
To host a game, start listening on *noray*'s local port:
```gdscript
var peer = ENetMultiplayerPeer.new()
var err = peer.create_server(Noray.local_port)
if err != OK:
return false # Failed to listen on port
```
The rest is handled by *noray*.
## Starting a client
To connect to a game, send a request to *noray* with the host's OpenID.
```gdscript
var oid = "abcd1234"
# Connect using NAT punchthrough
Noray.connect_nat(oid)
# Or connect using relay
Noray.connect_relay(oid)
```
Once the request is sent, *noray* will send a message to both the client and
the host players to connect to each other. The actual connection is done by
handling signals.
!!!note
*noray* provides no functionality to share OpenIDs. For development, you
can display the OpenID in a textbox, letting players copy it and share over
their preferred messaging app.
## Handling signals
When a connect message is received, the appropriate signal is fired.
*on_connect_nat* is fired to connect with NAT punchthrough.
*on_connect_relay* is fired to connect to a relay.
In both cases, a public address is passed to the signal handler, in the form of
an address string and a port. Handlers must conduct a handshake ( e.g. with
[PacketHandshake] ) and connect if successful.
Client example:
```gdscript
func _ready():
Noray.on_connect_nat.connect(_handle_connect)
Noray.on_connect_relay.connect(_handle_connect)
func _handle_connect(address: String, port: int) -> Error:
# Do a handshake
var udp = PacketPeerUDP.new()
udp.bind(Noray.local_port)
udp.set_dest_address(address, port)
var err = await PacketHandshake.over_packet_peer(udp)
udp.close()
if err != OK:
return err
# Connect to host
var peer = ENetMultiplayerPeer.new()
err = peer.create_client(address, port, 0, 0, 0, Noray.local_port)
if err != OK:
return err
return OK
```
!!!note
Make sure to **always** specifiy the local port for the client - this is
the only port noray recognizes, and failing to specify it will result in
broken connectivity.
Host example:
```gdscript
func _ready():
Noray.on_connect_nat.connect(_handle_connect)
Noray.on_connect_relay.connect(_handle_connect)
func _handle_connect(address: String, port: int) -> Error:
var peer = get_tree().get_multiplayer().multiplayer_peer as ENetMultiplayerPeer
var err = await PacketHandshake.over_enet(peer.host, address, port)
if err != OK:
return err
return OK
```
!!!note
The host handshake is a bit different, as it can't receive manual packets,
only send them. So it assumes that the target is always responsive, and
just blasts them with a bunch of packets. If the target is indeed
responsive, it can connect. If not, nothing happens, as expected.
[noray]: https://github.com/foxssake/noray
[NAT Punch-through for Multiplayer Games]: https://keithjohnston.wordpress.com/2014/02/17/nat-punch-through-for-multiplayer-games/
[PacketHandshake]: ./packet-handshake.md
@@ -0,0 +1,116 @@
# PacketHandshake
Singleton implementing handshake over UDP.
The point of the handshake itself is to confirm two-way connection between
two parties - i.e. both parties can receive message from the other and
receive acknowledgement from the other that their messages have arrived.
This is an important step before establishing connection for actual game
play, as this lets both the client's and server's routers ( if any ) know
that traffic is expected and should be let through.
## NAT punchthrough
Most players are behind a router. Routers are directly connected to the
internet, and protect machines behind them from unwanted traffic.
When routers receive packets from an unknown source, those packets are rejected
and don't reach the player's device. When data was sent to that address first,
routers see traffic as a reply and allow incoming data.
To take an example, if a random PC starts sending traffic your way, the router
will reject it. If you send data to the host behind godotengine.org, your
router will allow incoming traffic from it. Otherwise, you wouldn't be able to
open the website in your browser, as the incoming HTTP response would be
rejected.
This can be used to our advantage. If both players start sending traffic
towards eachother, eventually the routers will assume it's a response to some
request and allow the traffic.
> This is a very simplified description of how routers work. NAT punchthrough
> does not always work. For further reading, see [Network address translation].
## Handshake process
To confirm two-way connectivity, a string is sent back and forth, encoding the
player's knowledge about the connection:
The *Read flag* is set once we have received data from the other player.
The *Write flag* is set once we send data to the other player. Since data is
always sent, this flag is always set.
The *Duplex flag* is set when we have received data from the other player
knowing that they have also received data from us. This means that data flows
both ways.
The handshake process is successful when both players have the *Duplex flag*
set *and* both players know that the other player has the *Duplex flag* set.
Each flag is encoded as its specific character or a hyphen. The encoded string
is prepended with a dollar sign. For example:
* *$rw-* means that we have sent and received data from the other player
* *$rwx* means that the *read*, *write*, and *duplex* flags are all set
Here's the handshake process illustrated:
```puml
@startuml
actor "Player A" as A
entity "Router A" as RA
boundary Internet as Net
entity "Router B" as RB
actor "Player B" as B
A ->x RB : $-w-
note over RB: Packet denied
B ->x RA : $-w-
note over RA: Packet denied
note over RA, RB: NAT table updated on both routers
A -> B: $-w-
note over RB: Packed allowed
B -> A: $-w-
note over RB: Packed allowed
A -> B: $rwx
B -> A: $rwx
note over Net #lightgreen: Connection established
@enduml
```
## Handshake over PacketPeer
To run the handshake over raw UDP, call `PacketHandshake.over_packet_peer()`. The
specified PacketPeer will be used to send data until two-way connectivity is
confirmed or the timeout is reached. Between every packet sent, it takes a
short pause.
!!!note
The PacketPeer must already be configured with a target address.
## Handshake over ENet
If the game is already running, the handshake must be done over the already
active connection. For this case, use `PacketHandshake.over_enet_peer()`. If
the [ENetMultiplayerPeer] is not accessible from where you want to do the
handshake, use `PacketHandshake.over_enet()`.
This connection can't be used to receive custom packets, only to send them. So
the target address will be spammed with traffic confirming two-way connectivity
until timeout. Handshake will always be considered successful.
If the connectivity exists, players will simply connect. Otherwise,
connectivity will fail as expected, regardless of the handshake results.
[Network address translation]: https://en.wikipedia.org/wiki/Network_address_translation
[ENetMultiplayerPeer]: https://docs.godotengine.org/en/4.1/classes/class_enetmultiplayerpeer.html