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
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

@@ -0,0 +1,30 @@
# BaseNetInput
Base class for Input nodes used with rollback.
During rollback, multiple logical ticks are simulated in the span of a single
network tick. Since these are just logical ticks, no actual input arrives during
them from the input devices.
The solution is to gather input before the tick loop, and use that input for
any new ticks simulated during the rollback.
## Gathering input
This class provides a virtual `_gather` method that you can override. Set the
variables configured in [RollbackSynchronizer] in your own implementation:
```gdscript
extends BaseNetInput
var movement: Vector3 = Vector3.ZERO
func _gather():
movement = Vector3(
Input.get_axis("move_west", "move_east"),
0,
Input.get_axis("move_north", "move_south")
)
```
[RollbackSynchronizer]: ../../netfox/nodes/rollback-synchronizer.md
@@ -0,0 +1,80 @@
# NetworkSimulator
During testing, it is crucial to test your game under realistic network
conditions, including latency and potentially packet loss.
Network conditions can be simulated using [clumsy], [netem], or with *netfox*'s
*NetworkSimulator*. It auto-connects instances when launched from the editor,
and simulates various network configurations.
## How to Use
Enable and configure *Autoconnect* in the project settings.
![Autoconnect Project settings](../assets/network-simulator.png)
When your game launches one instance will start an [ENetMultiplayerPeer] server
and the rest will connect to it.
Either a `NetworkSimulator.server_created` or
`NetworkSimulator.client_connected` signal will be fired which you can use to
bootstrap your game code to.
!!!note
*NetworkSimulator* will only work when the game is ran from the editor.
Otherwise it will disable itself, regardless of project settings. This is a
failsafe to avoid affecting production builds of your games.
## Configuration
Hostname
: The hosting address. Usually `127.0.0.1` but can be changed to `*`, if you
want other machines to be able to join.
Server Port
: Which port to listen on. A second server port with latency / loss will open
one number higher if they are set to more than zero.
Use Compression
: Will make use of ENET's range encoder to keep packet sizes down.
Simulated Latency ( ms )
: Traffic delay, in milliseconds.
Simlated Packet Loss Chance
: What percentage of packets will to drop, simulating bad network conditions.
## Running in CI and other environments
In certain cases, you might not need the autoconnect feature to run, even if
it's enabled in the project settings.
One example could be running checks on your project in CI, or wanting to run a
single script.
In these cases, Godot will still identify itself as running in editor, so the
autoconnect feature will start hosting, keeping the process alive. This leads
to timeout errors, or the process running indefinitely.
To avoid such cases, set any of the following environment variables to any
non-empty string:
- `CI`
- `NETFOX_CI`
- `NETFOX_NO_AUTOCONNECT`
If *NetworkSimulator* detects any of the above environment variables, it will
deactivate, regardless of project settings.
!!!tip
Github Actions automatically sets the `CI` environment variable.
*NetworkSimulator* will automatically disable itself when running in a
Github Actions workflow.
See the [Github blog entry] for more details.
[clumsy]: https://jagt.github.io/clumsy/
[netem]: https://man7.org/linux/man-pages/man8/tc-netem.8.html
[ENetMultiplayerPeer]: https://docs.godotengine.org/en/4.1/classes/class_enetmultiplayerpeer.html
[Github blog entry]: https://github.blog/changelog/2020-04-15-github-actions-sets-the-ci-environment-variable-to-true/
+137
View File
@@ -0,0 +1,137 @@
# NetworkWeapon
Class to simplify writing networked weapons.
A weapon, in this context, is anything that can be fired and spawn objects (
projectiles ) upon being fired.
## Responsive projectiles
Upon firing, sending a request to the server and waiting for the response with
the projectile would introduce a delay. Doing a full-on state synchronization
with [MultiplayerSynchronizer] or [RollbackSynchronizer] can be unfeasible with
too many projectiles, and unnecessary, since most of the time, projectiles act
and move the same way regardless of their surroundings.
Instead, upon firing, a projectile is spawned instantly. At the same time, a
request is sent to the server. If the server accepts the projectile, it will
spawn it and broadcasts its starting state. Since the server's state is the
source of truth, the projectile's local state will be updated with the
difference. This is called *reconciliation*.
If the client requests a projectile with an unlikely state, it will be
rejected. This is to avoid players cheating, for example by requesting
projectiles at a more advantageous position than they're at.
If the server is too strict with what difference is considered acceptable and
what not, legitimate players may get cases where they fire a projectile which
disappears after a short time period.
## Implementing a weapon
*NetworkWeapon* provides multiple functions to override. Make sure that all
these methods work the same way on every player's game, otherwise players will
experience glitches.
*_can_fire* returns a bool, indicating whether the weapon can be fired. For
example, this method can return false if the weapon was fired recently and is
still on cooldown. **Do not** update state here. Use *_after_fire* instead.
*_can_peer_use* indicates whether a given peer can fire the weapon. Due to the
way RPCs are set up under the hood, any of the players can try to fire a
weapon. Use this method to check if the player trying to fire has permission,
e.g. a player is not trying to use someone else's weapon.
*_after_fire* is called after the weapon is successfully fired. Can be used to
update state ( e.g. last time the weapon was fired ) and play sound effects.
*_spawn* creates the projectile. Make sure to return the created node.
*_get_data* must return the projectile's starting state in a dictionary. This
can contain any property that is relevant to the projectile and must be
synchronized. For example, *global_transform* is important to ensure that the
projectile starts from the right position. On the other hand, projectile speed
does not need to be captured if it's the same for every projectile.
*_apply_data* must apply the captured properties to a projectile.
*_is_reconcilable* checks if the difference between two projectile states ( as
captured by *_get_data* ) is close enough to be allowed. Can be used to reject
cheating.
*_reconcile* adjusts the projectile based on the difference between the local
and server state.
## Specializations
*NetworkWeapon* extends [Node]. This also means that anything extending
*NetworkWeapon* is also a node, and thus can't have a position for example.
Two specialized classes are provided - *NetworkWeapon3D*, and *NetworkWeapon2D*
- extending Node3D and Node2D respectively.
This way, weapons can have transforms and have a presence in the game world.
They also take care of reconciliation, implementing *_get_data*, *_apply_data*,
*_is_reconcilable*, and *_reconcile*. These can be overridden, but make sure to
to call the base class with *super(...)*.
Reconciliation is based on distance, and can be configured with the
*distance_threshold* property.
Under the hood, these specializations create a special *NetworkWeapon* node,
that proxies all the method calls back to the specialization. This is a
workaround to build multiple inheritance in a single inheritance language.
## Compensating latency
Whenever the weapon is fired, it takes time for that event to arrive at the
host. To combat this, along with the weapon being fired, the firing's tick is
also sent. This way, the host doesn't only know that the weapon was fired, but
it also knows *when*.
To retrieve the exact tick, call *get_fired_tick()*.
This can be used to adjust the created projectile's simulation, e.g. by
simulating it from its spawn to the current tick in `_after_fire()`:
```gdscript
func _after_fire(projectile: Node3D):
last_fire = get_fired_tick()
for t in range(get_fired_tick(), NetworkTime.tick):
if projectile.is_queued_for_deletion():
break
projectile._tick(NetworkTime.ticktime, t)
```
!!!note
To track the tick the weapon was last fired ( e.g. for cooldowns ), make sure
to use `get_fired_tick()`, instead of `NetworkTime.tick`.
## Hitscan weapons
Use *NetworkWeaponHitscan3D* to build networked hitscan weapons. It builds upon
the same principle as *NetworkWeapon*, but modified for hitscan.
Most of the callbacks are the same, with the following differences:
*_spawn* is not used, as there's no projectiles involved.
*_on_fire* is called whenever the weapon is successfully fired. Can be used to
implement effects on firing, such as sound or visual effects.
*_on_hit* is called whenever the weapon hits a target. Depending on
configuration, this may be another player, a different character, or just level
geometry. The raycast result is passed as a parameter to distinguish between
hits.
Reconciliation is handled under the hood - *_get_data*, *_apply_data*,
*_is_reconcilable*, and *_reconcile* do not need to be implemented.
Hitscan weapons don't implement *get_fired_tick()*, as there's no projectile to
simulate.
[MultiplayerSynchronizer]: https://docs.godotengine.org/en/stable/classes/class_multiplayersynchronizer.html
[RollbackSynchronizer]: ../../netfox/nodes/rollback-synchronizer.md
[Node]: https://docs.godotengine.org/en/stable/classes/class_node.html
+86
View File
@@ -0,0 +1,86 @@
# Physics
At the time of writing official Godot releases have no support for manually
stepping physics simulations. As such if you want to use physics nodes with
rollback such as RigidBody you will need to run either a fork that supports
stepping or use an alternate physics addon that exposes stepping.
!!!tip
An example game demonstrating physics and rollback is available: [Godot
Rocket League].
## Known Options
- **Godot with Stepping PR**: Build the Godot editor and relevant export
templates manually with the [physics stepping PR] applied. See the [Godot
documentation] for compilation instructions.
- **Rapier Physics Addon**: A third-party 2D/3D physics engine for Godot with
stepping support. Visit [godot.rapier.rs] for details.
- **Blazium Fork**: A Godot fork with enhanced physics features, including
stepping. Learn more at [blazium.app].
!!!tip
For using Godot with the Stepping Physics PR applied, a [community run
repository] is available with a custom build. Note that this repository and
its builds are provided as-is, and are not associated with Godot nor the PR's
author.
!!!note
The current version of the Rapier drivers were tested against Godot Rapier
version **0.8.26**.
## Enabling Physics Engine Rollback
To enable physics rollback, add the appropriate physics driver node to the root
of your scene tree based on your physics engine. Because stepping methods are
unavailable in standard Godot the classes are hidden to avoid compile errors.
You will need to enable them in the editor by going to *Project -> Tools ->
Enable physics driver*
- PhysicsDriver2D or PhysicsDriver3D for Godots default physics or Blazium.
- RapierPhysicsDriver2D or RapierPhysicsDriver3D for the Rapier Physics Addon.
These nodes disable Godots default physics processing and step the physics
simulation at **netfox**s network tick rate.
![Enable physics driver menu](../assets/physics-enable.png)
## Physics Driver Configuration
**Physics Factor** - Controls the number of physics steps per network tick.
For example, if your network tick rate is 30 Hz (one tick every ~33ms) but you
need a 60 Hz physics simulation for smoother collisions, set this to 2 to run
two physics steps per tick.
**Rollback Physics Space** - When enabled, rolls back all physics objects in
the scene tree. Depending on how complex your scene tree is you may wish to
only rollback specific nodes for performance rather than the entire simulation
space.
## NetworkRigidBody
NetworkRigidBody2D and NetworkRigidBody3D nodes enable RigidBody
synchronization with RollbackSynchronizer, keeping clients in sync with the
servers physics simulation. These nodes can replace standard RigidBody nodes
with some minor setup.
!!!note
Avoid using StateSyncronizer with NetworkRigidBody, as it doesn't participate
in rollback it will end up stepping faster than other nodes.
To make use of NetworkRigidBody you need to:
1. Configure your RollbackSynchronizer to include the NetworkRigidBody's
`physics_state` as a state property.
2. Move physics-related logic from `_physics_process` to
`_physics_rollback_tick`.
![State configuration for NetworkRigidBody](../assets/network-rigid-body.png)
[Godot Rocket League]: https://github.com/albertok/godot-rocket-league
[community run repository]: https://github.com/albertok/godot/releases
[physics stepping PR]: https://github.com/godotengine/godot/pull/76462
[Godot documentation]: https://docs.godotengine.org/en/stable/contributing/development/compiling/index.html
[godot.rapier.rs]: https://godot.rapier.rs/
[blazium.app]: https://blazium.app/
@@ -0,0 +1,66 @@
# RewindableRandomNumberGenerator
A random number generator that can be used inside the [rollback tick loop].
An important point for writing code that works well in rollback is that it
behaves the same given the same circumstances, no matter how many times it's
run. It also must behave the same no matter which peer is running it.
Godot's built-in [RandomNumberGenerator] is not aware of rollback, so it will
consistently return different numbers for each tick, and potentially on each
peer.
To fix that, the *RewindableRandomNumberGenerator* generates the same random
sequences on each peer, and for each tick.
It implements most of the same methods, so it's close to a drop-in replacement.
## Creating the generator
The *RewindableRandomNumberGenerator* requires a seed upon initialization. This
seed must be the same on all peers.
!!!warning
If two *different* RNGs use the *same* seed, they will also generate the
same random numbers. Make sure that you use different seeds for different
objects.
For RNG's used by singletons, hard-coding different seed values is a very
simple approach:
```gd
var rng := RewindableRandomNumberGenerator.new(15)
```
If these objects also have names or similar, consistent identifiers, a simple
hash works well too:
```gd
var rng := RewindableRandomNumberGenerator.new(hash("Exit beacon"))
```
For dynamically spawned objects, or just to avoid the possibility of a human
error, the node path can be hashed:
```gd
var rng := RewindableRandomNumberGenerator.new(hash(get_path()))
```
This assumes that the same node will be spawned under the same path on all
peers, which is also a requirement for RPCs to work.
## Generating random numbers
The *RewindableRandomNumberGenerator* can be used in the same way as Godot's
built-in [RandomNumberGenerator]. All the per-peer and per-tick consistency is
ensured under the hood:
```gd
var rng := RewindableRandomNumberGenerator.new(0)
var dice_roll := rng.randi_range(1, 6)
```
[rollback tick loop]: ../../netfox/guides/network-rollback.md
[RandomNumberGenerator]: https://docs.godotengine.org/en/stable/classes/class_randomnumbergenerator.html
@@ -0,0 +1,214 @@
# RewindableStateMachine
Rollback-aware state machine implementation.
State machines are often used in games to implement different behaviors.
However, most implementations are not prepared for rollbacks. This class
provides an extensible implementation that can be used alongside a
[RollbackSynchronizer].
For a full example, see [multiplayer-state-machine].
## Creating a state machine
The first step is to add the RewindableStateMachine to your scene. It also
requires a RollbackSynchronizer that manages its `state` property. Unless these
conditions are satisfied, an editor warning will be displayed.
!!!note
Editor warnings are only updated when the node tree changes. Configuration
changes don't trigger an update. You may need to reload the scene after
fixing a warning, or make a tree change, like deleting and re-adding a node
by cutting and pasting.
![RewindableStateMachine with
RollbackSynchronizer](../assets/rewindable-state-machine-rollback.png)
Notice the RollbackSynchronizer added as a sibling to the
RewindableStateMachine, and having its `state` property configured.
## Implementing states
States are where the custom gameplay logic can be implemented. Each state must
be an extension of the RewindableState class, and added as a child to the
RewindableStateMachine.
States react to the game world using the following callbacks:
`tick(delta, tick, is_fresh)`
: Called for every rollback tick the state is active.
`enter(previous_state, tick)`
: Called when entering the state.
`exit(next_state, tick)`
: Called when exiting the state.
`can_enter(previous_state)`
: Called before entering the state. The state is only entered if this method
returns true.
`display_enter(previous_state, tick)`
: Called before displaying the state.
`display_exit(next_state, tick)`
: Called before displaying a different state.
You can override any of these callbacks to implement your custom behaviors.
For example, the snippet below implements an idle state, that transitions to
other states based on movement inputs:
```gdscript
extends RewindableState
@export var input: PlayerInputStateMachine
func tick(delta, tick, is_fresh):
if input.movement != Vector3.ZERO:
state_machine.transition(&"Move")
elif input.jump:
state_machine.transition(&"Jump")
```
Transitions are based on *node names*, i.e. calling `transition(&"Move")` will
transition to a state node called *Move*.
![RewindableStates under a state
machine](../assets/rewindable-state-children.png)
States must be added as children under a RewindableStateMachine to work.
## Using signals instead of classes
*RewindableState* nodes also emit signals during their lifetime. This enables
an alternate style of implementing states, by connecting handlers to different
signals. This can be useful if you want to keep all your logic in a single
script, among others.
Each of these signals correspond to a callback explained above:
* `on_enter()``enter()`
* `on_tick()``tick()`
* `on_exit()``exit()`
* `on_display_enter()``display_enter()`
* `on_display_exit()``display_exit()`
## Adding states
Once implemented, add the state nodes as children of the
*RewindableStateMachine* in the Scene Tree. When doing this programmatically,
make sure to set the state's `owner` to the target *RewindableStateMachine*.
Without the owner set, the *RewindableStateMachine* won't recognize the state.
## Display State vs State
There's two sets of callbacks for state transition - `enter()`/`exit()` and
`display_enter()`/`display_exit()`.
The `enter()`/`exit()` callbacks are intended for implementing game logic. The
`display_enter()`/`display_exit()` are intended for implementing presentation
logic - visuals, animations, sound effects, etc.
The same applies to `on_state_changed` vs. `on_display_state_changed`.
Let's take an example. The game is currently on tick @8. It needs to re-run
ticks @0 to @8 during rollback. In these ticks, the player moves a bit,
performs a jump, and then stops after moving a bit more:
```puml
@startuml
concise "Player" as P
@0
P is Idle
@1
P is Moving
@3
P is Jumping
@5
P is Moving
@8
P is Idle
@enduml
```
This will trigger the following state changes:
* Tick@1: Idle → Moving
* Tick@3: Moving → Jumping
* Tick@5: Jumping → Moving
* Tick@8: Moving → Idle
For each of the above, the `on_state_changed` signal will be emitted, and the
`enter()`/`exit()` callbacks will be triggered.
This makes the above callbacks ideal for game logic, e.g. adding an upward
velocity to the player when they enter the `Jumping` state.
Note that the *displayed* state does not change. Before the rollback loop, the
player's state was `Idle`. After the rollback loop, the player's state is also
`Idle`. Even though the player has ran and performed a jump, it wouldn't make
sense to change their animation or play any sound effect.
Let's take a different rollback example:
```puml
@startuml
concise "Player" as P
@5
P is Moving
@8
P is Idle
@9
P is Jumping
@enduml
```
In this case, the display state *did* change. Before the rollback loop, the
player's state was `Moving`. After the rollback loop, the player's state is
`Jumping`. It would make sense to change the player's animation and play a
jumping sound effect.
This can be done by using the display state callbacks - the
`on_display_state_changed` signal, and the `display_enter()`/`display_exit()`
methods.
## Caveats
RewindableStateMachine runs in the [rollback tick loop], which means that all
the [Rollback Caveats] apply.
In addition, rollback ticks are only ran for nodes that have known inputs for
the given tick, and *need* to be simulated - either on the server to determine
the new state, or on the client to predict. In practice, ticks are usually only
ran on the host owning state and the client owning inputs. The rest of the
peers use the state broadcast by the host.
**This means that transition callbacks are not always ran.** This is by design
and expected ( see [#327] ).
As a best practice, in the `enter()`, `exit()` callbacks and the
`on_state_changed` signal, only change game state - i.e. properties that are
configured as state in [RollbackSynchronizer].
To update visuals - e.g. change animation, spawn effects, etc. -, use either
the `on_display_state_changed` signal, or the `display_enter()` and
`display_exit()` callbacks to react to state transitions.
[multiplayer-state-machine]: https://github.com/foxssake/netfox/tree/main/examples/multiplayer-state-machine
[RollbackSynchronizer]: ../../netfox/nodes/rollback-synchronizer.md
[rollback tick loop]: ../../netfox/guides/network-rollback.md#network-rollback-loop
[Rollback Caveats]: ../../netfox/tutorials/rollback-caveats.md
[#327]: https://github.com/foxssake/netfox/issues/327#issuecomment-2491251374
+33
View File
@@ -0,0 +1,33 @@
# WindowTiler
A developer convenience feature that automatically tiles the launched windows
when working from the editor.
![Window Tiler](../assets/window-tiler.gif)
## Limitations
### Borderless mode on Linux
Setting window position and size works inconsistently under Linux at the time
of writing. Your mileage may vary based on your desktop environment and
distribution.
In case the windows don't tile properly with *Borderless* enabled, disabling it
is a fallback.
### Window decorations
At the time of writing, there is no known and consistent way to compensate for
window decoration size and offset. In practice, this means that windows may
slightly overlap.
## Configuration
*Auto Tile Windows* Enables auto tiling from editor launches.
*Screen* Which screen number to move and tile the windows to.
*Borderless* Enable borderless mode to make the most out of the screen real
estate.