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
-31
View File
@@ -1,31 +0,0 @@
# Contributors
This addon, and the entirety of [netfox] is a shared effort of [Fox's Sake
Studio], and the community. The following is the list of community contributors
involved with netfox:
* Alberto Klocker <albertok@gmail.com>
* Andrew Davis <jonandrewdavis@gmail.com>
* Bryan Lee <42545742+bryanmylee@users.noreply.github.com>
* Dan Schuman <danschuman@gmail.com>
* Dustie <77035922+DustieDog@users.noreply.github.com>
* Eric Volpone <ericvolpone@gmail.com>
* Gordon MacPherson <gordon@gordonite.tech>
* Jake Cattrall <krazyjakee@gmail.com>
* Jon Stevens <1030863+jonstvns@users.noreply.github.com>
* Joseph Michael Ware <9at25jnr3@mozmail.com>
* Matias Kumpulainen <kumpulainen.matias@gmail.com>
* Nicolas Batty <nicolas.batty@gmail.com>
* Ricky <77863853+RickyYCheng@users.noreply.github.com>
* Riordan-DC <44295008+Riordan-DC@users.noreply.github.com>
* Russ <622740+russ-@users.noreply.github.com>
* Ryan Roden-Corrent <github@rcorre.net>
* TheYellowArchitect <dmalandris@uth.gr>
* TheYellowArchitect <hello@theyellowarchitect.com>
* camperotactico <alberto.vgdd@gmail.com>
* gk98s <89647115+gk98s@users.noreply.github.com>
* zibetnu <9at25jnr3@mozmail.com>
[netfox]: https://github.com/foxssake/netfox
[Fox's Sake Studio]: https://github.com/foxssake/
+95
View File
@@ -0,0 +1,95 @@
extends RefCounted
class_name _Bitset
# Stores a list of booleans, representing them efficiently as a PackedByteArray
var _data: PackedByteArray
var _bit_count: int
static func of_bools(values: Array) -> _Bitset:
var result := _Bitset.new(values.size())
for i in values.size():
if values[i]:
result.set_bit(i)
return result
func _init(bit_count: int):
var bytes := bit_count / 8
if bit_count % 8 > 0:
bytes += 1
_data = PackedByteArray()
_data.resize(bytes)
_bit_count = bit_count
func bit_count() -> int:
return _bit_count
func is_empty() -> bool:
return _bit_count == 0
func is_not_empty() -> bool:
return _bit_count != 0
func get_bit(idx: int) -> bool:
assert(idx < _bit_count, "Accessing bit %d on bitset of size %d!" % [idx, _bit_count])
var byte_idx := idx / 8
var bit_idx := idx % 8
return (_data[byte_idx] >> bit_idx) & 0x1 != 0
func set_bit(idx: int) -> void:
assert(idx < _bit_count, "Accessing bit %d on bitset of size %d!" % [idx, _bit_count])
var byte_idx := idx / 8
var bit_idx := idx % 8
_data[byte_idx] |= 0x1 << bit_idx
func clear_bit(idx: int) -> void:
assert(idx < _bit_count, "Accessing bit %d on bitset of size %d!" % [idx, _bit_count])
var byte_idx := idx / 8
var bit_idx := idx % 8
_data[byte_idx] &= ~(0x1 << bit_idx)
func toggle_bit(idx: int) -> void:
assert(idx < _bit_count, "Accessing bit %d on bitset of size %d!" % [idx, _bit_count])
var byte_idx := idx / 8
var bit_idx := idx % 8
_data[byte_idx] ^= 0x1 << bit_idx
func get_set_indices() -> Array[int]:
var result := [] as Array[int]
for i in _data.size():
var byte := _data[i]
if byte & 0x01: result.append(i * 8 + 0)
if byte & 0x02: result.append(i * 8 + 1)
if byte & 0x04: result.append(i * 8 + 2)
if byte & 0x08: result.append(i * 8 + 3)
if byte & 0x10: result.append(i * 8 + 4)
if byte & 0x20: result.append(i * 8 + 5)
if byte & 0x40: result.append(i * 8 + 6)
if byte & 0x80: result.append(i * 8 + 7)
return result
func equals(other) -> bool:
if other is _Bitset:
return other._bit_count == _bit_count and other._data == _data
else:
return false
func _to_string() -> String:
if is_empty():
return "Bitset(n=0)"
else:
var body := ""
for i in _bit_count:
if i != 0 and i % 4 == 0:
body += " "
if get_bit(i): body += "1"
else: body += "0"
return "Bitset(n=%d, %s)" % [_bit_count, body]
+1
View File
@@ -0,0 +1 @@
uid://c46ub48lgga12
+57
View File
@@ -0,0 +1,57 @@
extends RefCounted
class_name _Graph
# Represents a graph, in the sense of a set of nodes, arbitrarily connected by
# links
var _links_from := {} # `from` to `to[]`
var _links_to := {} # `to` to `from[]`
func link(from: Variant, to: Variant) -> void:
if has_link(from, to):
return
_append(_links_from, from, to)
_append(_links_to, to, from)
func unlink(from: Variant, to: Variant) -> void:
_erase(_links_from, from, to)
_erase(_links_to, to, from)
func erase(node: Variant) -> void:
var links_to := _links_from.get(node, [])
var links_from := _links_to.get(node, [])
_links_from.erase(node)
_links_to.erase(node)
for link in links_to:
_erase(_links_to, link, node)
for link in links_from:
_erase(_links_from, link, node)
func get_linked_from(from: Variant) -> Array:
return _links_from.get(from, [])
func get_linked_to(to: Variant) -> Array:
return _links_to.get(to, [])
func has_link(from: Variant, to: Variant) -> bool:
return get_linked_from(from).has(to)
func _append(pool: Dictionary, key: Variant, value: Variant) -> void:
if not pool.has(key):
pool[key] = [value]
else:
pool[key].append(value)
func _erase(pool: Dictionary, key: Variant, value: Variant) -> void:
if not pool.has(key):
return
var values := pool[key] as Array
values.erase(value)
if values.is_empty():
pool.erase(key)
+1
View File
@@ -0,0 +1 @@
uid://djknl4n6akxue
+113 -49
View File
@@ -1,70 +1,134 @@
extends RefCounted
class_name _HistoryBuffer
# Maps ticks (int) to arbitrary data
var _buffer: Dictionary = {}
# Maps ticks (int) to arbitrary data, stored in a sliding ring buffer
func get_snapshot(tick: int):
if _buffer.has(tick):
return _buffer[tick]
else:
return null
var _capacity := 64
var _buffer := []
var _previous := []
func set_snapshot(tick: int, data):
_buffer[tick] = data
var _tail := 0
var _head := 0
func get_buffer() -> Dictionary:
return _buffer
static func of(capacity: int, data: Dictionary) -> _HistoryBuffer:
var history_buffer := _HistoryBuffer.new(capacity)
for idx in data:
history_buffer.set_at(idx, data[idx])
return history_buffer
func get_earliest_tick() -> int:
return _buffer.keys().min()
func _init(capacity: int = 64):
_capacity = capacity
_buffer.resize(_capacity)
_previous.resize(_capacity)
func get_latest_tick() -> int:
return _buffer.keys().max()
func duplicate(deep: bool = false) -> _HistoryBuffer:
var buffer := _HistoryBuffer.new(_capacity)
func get_closest_tick(tick: int) -> int:
if _buffer.has(tick):
return tick
buffer._buffer = _buffer.duplicate(deep)
buffer._previous = _previous.duplicate()
buffer._tail = _tail
buffer._head = _head
if _buffer.is_empty():
return -1
return buffer
var earliest_tick = get_earliest_tick()
if tick < earliest_tick:
return earliest_tick
func push(value: Variant) -> void:
_buffer[_head % _capacity] = value
_previous[_head % _capacity] = _head
_head += 1
_tail += maxi(0, size() - capacity())
var latest_tick = get_latest_tick()
if tick > latest_tick:
return latest_tick
func pop() -> Variant:
assert(is_not_empty(), "History buffer is empty!")
return _buffer.keys() \
.filter(func (key): return key < tick) \
.max()
var value = _buffer[_tail % _capacity]
_tail += 1
return value
func get_history(tick: int):
var closest_tick = get_closest_tick(tick)
return _buffer.get(closest_tick)
func set_at(at: int, value: Variant) -> void:
# Why does this need so many branches?
if is_empty():
# Buffer is empty, jump to specified index
_tail = at
_head = at
push(value)
elif at < _head - capacity():
# Trying to set something that would wrap back around and overwrite
# current data
return
elif at == _head:
# Simply adding a new item
push(value)
elif at < _head:
_buffer[at % _capacity] = value
# Update prev-buffer
for i in range(at, _head):
if _previous[i % _capacity] == i:
break
_previous[i % _capacity] = at
_tail = mini(_tail, at)
elif at >= _head + _capacity:
# We're leaving all data behind
_tail = at
_head = at
func trim(earliest_tick_to_keep: int):
var ticks := _buffer.keys()
for tick in ticks:
if tick < earliest_tick_to_keep:
_buffer.erase(tick)
_previous.fill(null)
_buffer.fill(null)
func clear():
_buffer.clear()
push(value)
elif at >= _head:
# Skipping forward a bit
var previous := _head - 1
while _head < at:
_previous[_head % _capacity] = previous
_head += 1
_tail += maxi(0, size() - _capacity)
push(value)
func has_at(at: int) -> bool:
if is_empty(): return false
if at < _head - capacity(): return false
if at >= _head: return false
return _previous[at % _capacity] == at
func get_at(at: int, default: Variant = null) -> Variant:
if not has_at(at):
return default
return _buffer[at % _capacity]
func has_latest_at(at: int) -> bool:
if is_empty(): return false
if at < _tail: return false
return true
func size() -> int:
return _buffer.size()
return _head - _tail
func capacity() -> int:
return _capacity
func get_earliest_index() -> int:
return _tail
func get_latest_index() -> int:
return _head - 1
func get_latest_index_at(at: int) -> int:
if not has_latest_at(at):
return -1
if at >= _head:
return get_latest_index()
return _previous[at % _capacity]
func get_latest_at(at: int) -> Variant:
return get_at(get_latest_index_at(at))
func clear():
_tail = _head
func is_empty() -> bool:
return _buffer.is_empty()
return size() == 0
func has(tick) -> bool:
return _buffer.has(tick)
func ticks() -> Array:
return _buffer.keys()
func erase(tick):
_buffer.erase(tick)
func is_not_empty() -> bool:
return not is_empty()
@@ -0,0 +1,22 @@
extends RefCounted
class_name _IntervalScheduler
# Returns true on every nth `is_now()` call
var interval := 1
var _idx := 0
func _init(p_interval: int = 1):
interval = p_interval
func is_now() -> bool:
if interval <= 0:
return false
elif interval == 1:
return true
elif _idx + 1 >= interval:
_idx = 0
return true
else:
_idx += 1
return false
@@ -0,0 +1 @@
uid://egfuyvoj0r2s
+1 -1
View File
@@ -3,5 +3,5 @@
name="netfox.internals"
description="Shared internals for netfox addons"
author="Tamas Galffy and contributors"
version="1.35.3"
version="1.40.2"
script="plugin.gd"
+8
View File
@@ -10,6 +10,11 @@ static func of(items: Array) -> _Set:
result.add(item)
return result
func duplicate(deep: bool = false) -> _Set:
var result := _Set.new()
result._data = _data.duplicate(deep)
return result
func add(value):
_data[value] = true
@@ -46,6 +51,9 @@ func equals(other) -> bool:
func _to_string():
return "Set" + str(values())
func _to_vest():
return _data.keys()
func _iter_init(arg) -> bool:
_iterator_idx = 0
return _can_iterate()