Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
97 commits
Select commit Hold shift + click to select a range
7ee3e67
unconditional sim
elementbound Dec 4, 2025
fdc5e4a
history server
elementbound Dec 4, 2025
527dd95
sync server
elementbound Dec 4, 2025
86793e7
fix recorded property identity issue
elementbound Dec 4, 2025
c5c2d0f
wip
elementbound Dec 6, 2025
dd41aa0
track auth in snapshot exp
elementbound Dec 6, 2025
a24f266
don't half-ass auth flag on snapshots + emit events for rollback loop…
elementbound Dec 6, 2025
841a4e5
simulate only if input
elementbound Dec 7, 2025
7736da7
snapshot pimping
elementbound Dec 7, 2025
fad0b1b
gut rbs
elementbound Dec 7, 2025
19b421f
lowkey support for predict, though issue not fixed
elementbound Dec 7, 2025
555227c
holy shit i just fucked up the predict check placement
elementbound Dec 8, 2025
baa2419
is fresh
elementbound Dec 8, 2025
0f5eebe
RBS.is_predicting
elementbound Dec 20, 2025
0b1497f
input prediction methods for RBS
elementbound Dec 20, 2025
c44d5cf
input age fixes
elementbound Dec 21, 2025
d021626
latest known input and state ticks
elementbound Dec 21, 2025
d48e32f
add some todos
elementbound Dec 21, 2025
82e6a07
diff states
elementbound Jan 6, 2026
10dee7a
input redundancy, i guess
elementbound Jan 7, 2026
356e9aa
network identity server for the eventual use
elementbound Jan 7, 2026
3babbae
Merge remote-tracking branch 'origin/main' into exp/server-pattern
elementbound Jan 8, 2026
0a5b57e
extra schemas and vague ideas about binary serialization
elementbound Jan 8, 2026
60f6594
binary state + busted input prediction?
elementbound Jan 8, 2026
5772499
schemas for state
elementbound Jan 8, 2026
1b6846d
include size in state snapshots
elementbound Jan 10, 2026
cc1766a
binary input serialization
elementbound Jan 10, 2026
64b79c4
we got diff states, now all we need is to make it work
elementbound Jan 10, 2026
87b1f74
man i dont fucking know but here's some tests anyway
elementbound Jan 10, 2026
18c153d
temp fix, this will suck to figure out
elementbound Jan 12, 2026
b9ea51c
command bus
elementbound Jan 12, 2026
a22dab0
uhh
elementbound Jan 12, 2026
deae15a
fix diff state sim fuckery - forgot to `filter_to_auth()` on the snap…
elementbound Jan 13, 2026
c112a6a
chill on eager assert
elementbound Jan 13, 2026
c789729
notes and fixes
elementbound Jan 13, 2026
6294710
jfc once again I forgot about nodes with state but no sim
elementbound Jan 14, 2026
2f94adf
visibility filtering
elementbound Jan 15, 2026
1aa8c92
hacked in state synchronizer without diff states
elementbound Jan 17, 2026
d251bd1
diff states for state synchronizer
elementbound Jan 17, 2026
b7859a8
fix interval scheduling
elementbound Jan 17, 2026
9066be5
restore traffic metrics and fix funky issue
elementbound Jan 17, 2026
99b982d
simplify diff states
elementbound Jan 17, 2026
ef7c140
use commands for netids
elementbound Jan 17, 2026
7707165
quick graph impl to support ( at least in theory ) multiple inputs pe…
elementbound Jan 17, 2026
4914829
input broadcast toggle
elementbound Jan 17, 2026
3f3497f
blazing fast graph queries
elementbound Jan 17, 2026
c8d961d
small fxs
elementbound Jan 17, 2026
09d7773
network identity server tests
elementbound Jan 18, 2026
d54674d
perf test for network identity server resolve
elementbound Jan 18, 2026
bd9d010
optimize network identity resolution
elementbound Jan 18, 2026
e86e8a3
some cleanup
elementbound Jan 18, 2026
443b8f8
netid fxs
elementbound Jan 18, 2026
902daee
reusable property pool
elementbound Jan 18, 2026
15b585d
extract serializer logic
elementbound Jan 18, 2026
cc62eb3
avoid snapshot filtering unless really necessary
elementbound Jan 18, 2026
eceb533
cleanup
elementbound Jan 19, 2026
24ad6b4
cleanups
elementbound Jan 19, 2026
51f0b70
mid fuckery
elementbound Jan 19, 2026
57c830b
serializer sanity tests
elementbound Jan 19, 2026
e4c7394
settings + input delay
elementbound Jan 19, 2026
58162f5
cleanups and renames
elementbound Jan 19, 2026
4935118
even less todos
elementbound Jan 19, 2026
1cda83b
use commands for time sync
elementbound Jan 19, 2026
13171c7
use history buffer for storing snapshots, as ring-buffer
elementbound Jan 19, 2026
a6c0e1a
down by a bunch of todos
elementbound Jan 19, 2026
edc1b93
less todos + prevent input rewriting
elementbound Jan 19, 2026
3218b99
more serializer tests
elementbound Jan 19, 2026
2da31b0
almost done w/ tests
elementbound Jan 19, 2026
fc875e2
some strides and cleanups in testing
elementbound Jan 20, 2026
f39d483
software engineering
elementbound Jan 20, 2026
2340cbd
fix autoload paths
elementbound Jan 22, 2026
02049eb
suggestions
elementbound Feb 4, 2026
8225e4a
link todos
elementbound Feb 4, 2026
272e12a
fxs
elementbound Feb 9, 2026
4f80948
wp
elementbound Feb 9, 2026
373f878
start rewriting to per-object history
elementbound Feb 10, 2026
c9d4f4c
wip typa shi
elementbound Mar 1, 2026
8221fb2
migrate rocket league changes
elementbound Mar 1, 2026
e682cd8
cleanups and fxs
elementbound Mar 2, 2026
09dd8f8
uids
elementbound Mar 2, 2026
8e771fe
migrate predictive synchronizer
elementbound Mar 4, 2026
893daa9
merge main
elementbound Mar 6, 2026
7e053ae
bv
elementbound Mar 6, 2026
7ff0ac7
fxs
elementbound Mar 6, 2026
5cf02d4
uids
elementbound Mar 6, 2026
1500530
some cleanups
elementbound Mar 9, 2026
d33f41f
more cleanups
elementbound Mar 9, 2026
9b36792
uh
elementbound Mar 9, 2026
e457e16
remove fixed cmd ids
elementbound Mar 9, 2026
b56e493
cleanup history server
elementbound Mar 9, 2026
8bace0e
document and privatize history server
elementbound Mar 10, 2026
af02317
fxs
elementbound Mar 10, 2026
4eba50f
document and privatize sim server
elementbound Mar 10, 2026
e7a5b92
privatize snapshot classes
elementbound Mar 11, 2026
373cc79
document and privatize sync server
elementbound Mar 11, 2026
8c4ca32
cleanup
elementbound Mar 11, 2026
5521af3
docs
elementbound Mar 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion addons/netfox.extras/plugin.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
name="netfox.extras"
description="Game-specific utilities for Netfox"
author="Tamas Galffy and contributors"
version="1.39.0"
version="1.40.0"
script="netfox-extras.gd"
95 changes: 95 additions & 0 deletions addons/netfox.internals/bitset.gd
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions addons/netfox.internals/bitset.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://c46ub48lgga12
57 changes: 57 additions & 0 deletions addons/netfox.internals/graph.gd
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions addons/netfox.internals/graph.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://djknl4n6akxue
172 changes: 118 additions & 54 deletions addons/netfox.internals/history-buffer.gd
Original file line number Diff line number Diff line change
@@ -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

var _capacity := 64
var _buffer := []
var _previous := []

var _tail := 0
var _head := 0

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 _init(capacity: int = 64):
_capacity = capacity
_buffer.resize(_capacity)
_previous.resize(_capacity)

func duplicate(deep: bool = false) -> _HistoryBuffer:
var buffer := _HistoryBuffer.new(_capacity)

buffer._buffer = _buffer.duplicate(deep)
buffer._previous = _previous.duplicate()
buffer._tail = _tail
buffer._head = _head

return buffer

func push(value: Variant) -> void:
_buffer[_head % _capacity] = value
_previous[_head % _capacity] = _head
_head += 1
_tail += maxi(0, size() - capacity())

func pop() -> Variant:
assert(is_not_empty(), "History buffer is empty!")

var value = _buffer[_tail % _capacity]
_tail += 1
return value

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

_previous.fill(null)
_buffer.fill(null)

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 get_snapshot(tick: int):
if _buffer.has(tick):
return _buffer[tick]
else:
return null

func set_snapshot(tick: int, data):
_buffer[tick] = data

func get_buffer() -> Dictionary:
return _buffer
func size() -> int:
return _head - _tail

func get_earliest_tick() -> int:
return _buffer.keys().min()
func capacity() -> int:
return _capacity

func get_latest_tick() -> int:
return _buffer.keys().max()
func get_earliest_index() -> int:
return _tail

func get_closest_tick(tick: int) -> int:
if _buffer.has(tick):
return tick
func get_latest_index() -> int:
return _head - 1

if _buffer.is_empty():
func get_latest_index_at(at: int) -> int:
if not has_latest_at(at):
return -1
if at >= _head:
return get_latest_index()

var earliest_tick = get_earliest_tick()
if tick < earliest_tick:
return earliest_tick

var latest_tick = get_latest_tick()
if tick > latest_tick:
return latest_tick

return _buffer.keys() \
.filter(func (key): return key < tick) \
.max()
return _previous[at % _capacity]

func get_history(tick: int):
var closest_tick = get_closest_tick(tick)
return _buffer.get(closest_tick)

func trim(earliest_tick_to_keep: int):
var ticks := _buffer.keys()
for tick in ticks:
if tick < earliest_tick_to_keep:
_buffer.erase(tick)
func get_latest_at(at: int) -> Variant:
return get_at(get_latest_index_at(at))

func clear():
_buffer.clear()

func size() -> int:
return _buffer.size()
_tail = _head

func is_empty() -> bool:
return _buffer.is_empty()

func has(tick) -> bool:
return _buffer.has(tick)

func ticks() -> Array:
return _buffer.keys()
return size() == 0

func erase(tick):
_buffer.erase(tick)
func is_not_empty() -> bool:
return not is_empty()
22 changes: 22 additions & 0 deletions addons/netfox.internals/interval-scheduler.gd
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions addons/netfox.internals/interval-scheduler.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://egfuyvoj0r2s
2 changes: 1 addition & 1 deletion addons/netfox.internals/plugin.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
name="netfox.internals"
description="Shared internals for netfox addons"
author="Tamas Galffy and contributors"
version="1.39.0"
version="1.40.0"
script="plugin.gd"
Loading
Loading