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
+112
View File
@@ -0,0 +1,112 @@
extends VestTest
func get_suite_name():
return "PeerVisibilityFilter"
func suite():
var peers: Array[int] = [1, 2, 3, 4]
var peers_packed_array := PackedInt32Array(peers)
define("Visibility override", func():
test("should return all peers on default visibility", func():
var filter := PeerVisibilityFilter.new()
filter.default_visibility = true
filter.update_visibility(peers_packed_array)
assert_that(filter.get_visible_peers()).is_equal_to(peers)
assert_that(filter.get_rpc_target_peers()).is_equal_to([0])
)
test("should return no peers on default invisibility", func():
var filter := PeerVisibilityFilter.new()
filter.default_visibility = false
filter.update_visibility(peers_packed_array)
assert_that(filter.get_visible_peers()).is_empty()
assert_that(filter.get_rpc_target_peers()).is_empty()
)
test("should return force-visible peers on default invisibility", func():
var filter := PeerVisibilityFilter.new()
filter.default_visibility = false
filter.set_visibility_for(2, true)
filter.update_visibility(peers_packed_array)
expect(filter.get_visibility_for(2), "Peer#2 should be visible!")
expect_not(filter.get_visibility_for(1), "Peer#1 should not be visible!")
assert_that(filter.get_visible_peers()).is_equal_to([2])
assert_that(filter.get_rpc_target_peers()).is_equal_to([2])
)
test("should not return force-invisible peers on default visibility", func():
var filter := PeerVisibilityFilter.new()
filter.default_visibility = true
filter.set_visibility_for(2, false)
filter.update_visibility(peers_packed_array)
expect_not(filter.get_visibility_for(2), "Peer#2 should not be visible!")
expect(filter.get_visibility_for(1), "Peer#1 should be visible!")
assert_that(filter.get_visible_peers()).is_equal_to([1, 3, 4])
assert_that(filter.get_rpc_target_peers()).is_equal_to([-2])
)
test("should return peers with multiple excludes", func():
var filter := PeerVisibilityFilter.new()
filter.default_visibility = true
filter.set_visibility_for(2, false)
filter.set_visibility_for(4, false)
filter.update_visibility(peers_packed_array)
expect_not(filter.get_visibility_for(2), "Peer#2 should not be visible!")
expect(filter.get_visibility_for(1), "Peer#1 should be visible!")
assert_that(filter.get_visible_peers()).is_equal_to([1, 3])
assert_that(filter.get_rpc_target_peers()).is_equal_to([1, 3])
)
)
define("Visibility filters", func():
test("should pass through on true", func():
var filter := PeerVisibilityFilter.new()
filter.default_visibility = true
filter.add_visibility_filter(func(_peer: int): return true)
filter.update_visibility(peers_packed_array)
assert_that(filter.get_visible_peers()).is_equal_to(peers)
)
test("should exclude on false", func():
var filter := PeerVisibilityFilter.new()
filter.default_visibility = true
filter.add_visibility_filter(func(_peer: int): return false)
filter.update_visibility(peers_packed_array)
assert_that(filter.get_visible_peers()).is_empty()
)
test("should exclude if any returns false", func():
var filter := PeerVisibilityFilter.new()
filter.default_visibility = true
filter.add_visibility_filter(func(_peer: int): return true)
filter.add_visibility_filter(func(_peer: int): return false)
filter.update_visibility(peers_packed_array)
assert_that(filter.get_visible_peers()).is_empty()
)
)
test("filter should have precendence over override", func():
var excluded_peer := 2
var filter := PeerVisibilityFilter.new()
filter.default_visibility = false
filter.set_visibility_for(excluded_peer, true)
filter.add_visibility_filter(func(peer: int):
if peer == excluded_peer: return false
else: return true
)
filter.update_visibility(peers_packed_array)
assert_that(filter.get_visible_peers()).is_empty()
)
@@ -0,0 +1 @@
uid://bxy7dx4ktuwm7
@@ -0,0 +1,57 @@
extends VestTest
func get_suite_name() -> String:
return "PropertyConfig"
const LOCAL_PEER := 1
const REMOTE_PEER := 2
func suite():
var local_node := SnapshotFixtures.state_node()
var remote_node := SnapshotFixtures.state_node()
remote_node.set_multiplayer_authority(REMOTE_PEER)
test("should get locally owned properties", func():
var properties := SnapshotFixtures.state_propery_entries(local_node)
var config := _PropertyConfig.new()
config.set_properties(properties)
config.local_peer_id = LOCAL_PEER
expect_equal(config.get_owned_properties(), properties)
expect_equal(config.get_properties_owned_by(LOCAL_PEER), properties)
expect_empty(config.get_properties_owned_by(REMOTE_PEER))
)
test("should get remote owned properties", func():
var properties := SnapshotFixtures.state_propery_entries(remote_node)
var config := _PropertyConfig.new()
config.set_properties(properties)
config.local_peer_id = REMOTE_PEER
expect_equal(config.get_properties_owned_by(REMOTE_PEER), properties)
expect_empty(config.get_properties_owned_by(LOCAL_PEER))
)
test("should get remote owned properties after ownership change", func():
var properties := SnapshotFixtures.state_properties()
var cache := PropertyCache.new(remote_node)
var config := _PropertyConfig.new()
config.set_properties_from_paths(SnapshotFixtures.state_properties(), cache)
config.local_peer_id = LOCAL_PEER
expect_empty(config.get_owned_properties())
# Update ownership
remote_node.set_multiplayer_authority(1)
config.set_properties_from_paths(SnapshotFixtures.state_properties(), cache)
# Convert to a list of property paths for easier comparison
var owned_properties := config.get_owned_properties()\
.map(func(it): return str(it))
expect_equal(owned_properties, properties)
)
on_finish.connect(func():
local_node.queue_free()
remote_node.queue_free()
)
@@ -0,0 +1 @@
uid://wucpfwpo6w3n
@@ -0,0 +1,45 @@
extends VestTest
func get_suite_name() -> String:
return "PropertyEntry"
func test_should_be_valid_on_known_property():
var property_entry := PropertyEntry.new()
property_entry.node = TestNode.new()
property_entry.property = "known_property"
expect(property_entry.is_valid(), "Property entry is invalid!")
func test_should_be_valid_on_null_property():
var property_entry := PropertyEntry.new()
property_entry.node = TestNode.new()
property_entry.property = "null_property"
expect(property_entry.is_valid(), "Property entry is invalid!")
func test_should_be_invalid_on_unknown_property():
var property_entry := PropertyEntry.new()
property_entry.node = TestNode.new()
property_entry.property = "unknown_property"
expect_not(property_entry.is_valid(), "Property entry is valid!")
func test_should_be_invalid_on_unknown_node():
var property_entry := PropertyEntry.new()
property_entry.node = null
property_entry.property = "known_property"
expect_not(property_entry.is_valid(), "Property entry is valid!")
func test_should_be_invalid_on_invalid_node():
var property_entry := PropertyEntry.new()
property_entry.node = TestNode.new()
property_entry.property = "known_property"
property_entry.node.free()
expect_not(property_entry.is_valid(), "Property entry is valid!")
class TestNode extends Node:
var known_property := ""
var null_property = null
@@ -0,0 +1 @@
uid://ci3ql26p3vwsi
@@ -0,0 +1,86 @@
extends VestTest
func get_suite_name() -> String:
return "PropertySnapshot"
func test_diff_should_be_empty():
# Given
var from := _PropertySnapshot.from_dictionary({
"foo": 15,
"bar": 18,
})
var to := _PropertySnapshot.from_dictionary({
"foo": 15,
"bar": 18,
})
var expected := _PropertySnapshot.from_dictionary({})
# When
var actual := from.make_patch(to)
# Then
expect_equal(actual.as_dictionary(), expected.as_dictionary())
func test_diff_should_add_unknown():
# Given
var from := _PropertySnapshot.from_dictionary({
"foo": 15,
})
var to := _PropertySnapshot.from_dictionary({
"foo": 15,
"bar": 18,
})
var expected := _PropertySnapshot.from_dictionary({
"bar": 18,
})
# When
var actual := from.make_patch(to)
# Then
expect_equal(actual.as_dictionary(), expected.as_dictionary())
func test_diff_should_add_differing():
# Given
var from := _PropertySnapshot.from_dictionary({
"foo": 15,
"bar": 18,
})
var to := _PropertySnapshot.from_dictionary({
"foo": 35,
"bar": 18,
})
var expected := _PropertySnapshot.from_dictionary({
"foo": 35
})
# When
var actual := from.make_patch(to)
# Then
expect_equal(actual.as_dictionary(), expected.as_dictionary())
func test_diff_should_exclude_removed():
# Given
var from := _PropertySnapshot.from_dictionary({
"foo": 15,
"bar": 18,
})
var to := _PropertySnapshot.from_dictionary({
"foo": 15,
})
var expected := _PropertySnapshot.from_dictionary({})
# When
var actual := from.make_patch(to)
# Then
expect_equal(actual.as_dictionary(), expected.as_dictionary())
@@ -0,0 +1 @@
uid://cmu1no787kjyt
+34
View File
@@ -0,0 +1,34 @@
extends VestTest
func get_suite_name():
return "RewindableAction"
var rewindable_action: RewindableAction
func before_case(__) -> void:
rewindable_action = RewindableAction.new()
# TODO(vest): Some good way to add nodes to the tree
await NetworkTime.get_tree().process_frame
NetworkTime.get_tree().root.add_child(rewindable_action)
func after_case(__) -> void:
rewindable_action.free()
func test_should_empty_queue_after_loop():
# Run as client
rewindable_action.set_multiplayer_authority(2)
# Prepare changes
var tickset := _Set.of([0])
var data := _TicksetSerializer.serialize(0, 4, tickset)
NetworkMocks.set_tick(4, 4)
# Run first loop
rewindable_action._submit_state(data) # Server sends data
NetworkMocks.run_network_tick_loop()
expect(rewindable_action.has_confirmed(), "RewindableAction was not confirmed!")
# Run second loop
rewindable_action._submit_state(data) # Server sends data again
NetworkMocks.run_network_tick_loop()
expect_not(rewindable_action.has_confirmed(), "RewindableAction was redundantly confirmed!")
@@ -0,0 +1 @@
uid://bjj3hqe3obauq
+22
View File
@@ -0,0 +1,22 @@
extends VestTest
func get_suite_name() -> String:
return "RollbackSynchronizer"
func suite():
# Messy to set up, keeping cases for later
define("Input age and predicting", func():
test("should return -1 on no input", func(): todo())
test("should return 0 on recent input", func(): todo())
test("should return positive on old input", func(): todo())
)
define("get_last_known_input()", func():
test("should return -1 for no input", func(): todo())
test("should return latest", func(): todo())
)
define("get_last_known_state()", func():
test("should return -1 for no state", func(): todo())
test("should return latest", func(): todo())
)
@@ -0,0 +1 @@
uid://5ufnoe5bk8kt
@@ -0,0 +1,53 @@
extends VestTest
func get_suite_name() -> String:
return "NetworkRollback"
var network_rollback: _NetworkRollback
var mutated_node: Node
func before_case(__):
network_rollback = _NetworkRollback.new()
mutated_node = Node.new()
func after_case(__):
network_rollback.queue_free()
mutated_node.queue_free()
func suite() -> void:
define("mutate()", func():
test("should be mutated after", func():
# Given
network_rollback.mutate(mutated_node, 8)
# When + Then
expect(network_rollback.is_mutated(mutated_node, 10))
expect_not(network_rollback.is_just_mutated(mutated_node, 10))
)
test("should just be mutated", func():
# Given
network_rollback.mutate(mutated_node, 8)
# When + Then
expect(network_rollback.is_mutated(mutated_node, 8))
expect(network_rollback.is_just_mutated(mutated_node, 8))
)
test("should not be mutated after", func():
# Given
network_rollback.mutate(mutated_node, 8)
# When + Then
expect_not(network_rollback.is_mutated(mutated_node, 4))
expect_not(network_rollback.is_just_mutated(mutated_node, 4))
)
test("unknown should not be mutated", func():
# Given nothing
# Then
expect_not(network_rollback.is_mutated(mutated_node, 8))
expect_not(network_rollback.is_just_mutated(mutated_node, 8))
)
)
@@ -0,0 +1 @@
uid://bvy32kc3ngile
+138
View File
@@ -0,0 +1,138 @@
extends VestTest
func get_suite_name() -> String:
return "NetworkSchemas"
func suite() -> void:
var has_half := (Engine.get_version_info().hex >= 0x040400) as bool
var cases := [
["variant", NetworkSchemas.variant(), 32, 12],
["string", NetworkSchemas.string(), "hi!!", 8],
["bool8", NetworkSchemas.bool8(), true, 1],
["int8", NetworkSchemas.int8(), 107, 1],
["int16", NetworkSchemas.int16(), 107, 2],
["int32", NetworkSchemas.int32(), 107, 4],
["int64", NetworkSchemas.int64(), 107, 8],
["uint8", NetworkSchemas.uint8(), 107, 1],
["uint16", NetworkSchemas.uint16(), 107, 2],
["uint32", NetworkSchemas.uint32(), 107, 4],
["uint64", NetworkSchemas.uint64(), 107, 8],
# Test values are max representable values in `n` bytes
["varuint 8", NetworkSchemas.varuint(), 127, 1],
["varuint 16", NetworkSchemas.varuint(), 16383, 2],
["varuint 24", NetworkSchemas.varuint(), 2097151, 3],
["varuint 32", NetworkSchemas.varuint(), 268435455, 4],
["varuint 40", NetworkSchemas.varuint(), 34359738367, 5],
["varuint 48", NetworkSchemas.varuint(), 4398046511103, 6],
["varuint 56", NetworkSchemas.varuint(), 562949953421311, 7],
["varuint 64", NetworkSchemas.varuint(), 72057594037927935, 8],
["sfrac8", NetworkSchemas.sfrac8(), -63. / 255., 1],
["sfrac16", NetworkSchemas.sfrac16(), -63. / 255., 2],
["sfrac32", NetworkSchemas.sfrac32(), -63. / 255., 4],
["ufrac8", NetworkSchemas.ufrac8(), 63. / 255., 1],
["ufrac16", NetworkSchemas.ufrac16(), 63. / 255., 2],
["ufrac32", NetworkSchemas.ufrac32(), 63. / 255., 4],
["degrees8", NetworkSchemas.degrees8(), 127. / 255. * 360., 1],
["degrees16", NetworkSchemas.degrees16(), 127. / 255. * 360., 2],
["degrees32", NetworkSchemas.degrees32(), 127. / 255. * 360., 4],
["radians8", NetworkSchemas.radians8(), 127. / 255. * TAU, 1],
["radians16", NetworkSchemas.radians16(), 127. / 255. * TAU, 2],
["radians32", NetworkSchemas.radians32(), 127. / 255. * TAU, 4],
["float16", NetworkSchemas.float16(), 2.0, 2 if has_half else 4],
["float32", NetworkSchemas.float32(), 2.0, 4],
["float64", NetworkSchemas.float64(), 2.0, 8],
["vec2f16", NetworkSchemas.vec2f32(), Vector2(+1, -1), 4 if has_half else 8],
["vec2f32", NetworkSchemas.vec2f32(), Vector2(+1, -1), 8],
["vec2f64", NetworkSchemas.vec2f64(), Vector2(+1, -1), 16],
["vec3f32", NetworkSchemas.vec3f32(), Vector3(+1, -1, .5), 12],
["vec3f64", NetworkSchemas.vec3f64(), Vector3(+1, -1, .5), 24],
["vec4f32", NetworkSchemas.vec4f32(), Vector4(+1, -1, .5, -5), 16],
["vec4f64", NetworkSchemas.vec4f64(), Vector4(+1, -1, .5, -5), 32],
["normal2f16", NetworkSchemas.normal2f16(), Vector2.RIGHT.rotated(PI / 6.), 2 if has_half else 4],
["normal2f32", NetworkSchemas.normal2f32(), Vector2.RIGHT.rotated(PI / 6.), 4],
["normal2f64", NetworkSchemas.normal2f64(), Vector2.RIGHT.rotated(PI / 6.), 8],
["normal3f16", NetworkSchemas.normal3f16(), Vector3.UP, 4 if has_half else 8],
["normal3f32", NetworkSchemas.normal3f32(), Vector3.UP, 8],
["normal3f64", NetworkSchemas.normal3f64(), Vector3.UP, 16],
["quat16f", NetworkSchemas.quatf16(), Quaternion.from_euler(Vector3.ONE), 8 if has_half else 16],
["quat32f", NetworkSchemas.quatf32(), Quaternion.from_euler(Vector3.ONE), 16],
["quat64f", NetworkSchemas.quatf64(), Quaternion.from_euler(Vector3.ONE), 32],
["transform2f16", NetworkSchemas.transform2f16(), Transform2D.IDENTITY.rotated(37.), 12 if has_half else 24],
["transform2f32", NetworkSchemas.transform2f32(), Transform2D.IDENTITY.rotated(37.), 24],
["transform2f64", NetworkSchemas.transform2f64(), Transform2D.IDENTITY.rotated(37.), 48],
["transform3f16", NetworkSchemas.transform3f16(), Transform3D.IDENTITY.rotated(Vector3.ONE, 37.), 24 if has_half else 48],
["transform3f32", NetworkSchemas.transform3f32(), Transform3D.IDENTITY.rotated(Vector3.ONE, 37.), 48],
["transform3f64", NetworkSchemas.transform3f64(), Transform3D.IDENTITY.rotated(Vector3.ONE, 37.), 96],
["array", NetworkSchemas.array_of(NetworkSchemas.uint16()), [1, 2, 3], 8],
["dictionary", NetworkSchemas.dictionary(NetworkSchemas.uint16(), NetworkSchemas.uint16()), { 1: 32, 2: 48 }, 10],
["netref id8", NetworkSchemas._netref(), _NetworkIdentityReference.of_id(12), 1],
["netref id16", NetworkSchemas._netref(), _NetworkIdentityReference.of_id(138), 2],
["netref name", NetworkSchemas._netref(), _NetworkIdentityReference.of_full_name("path"), 9],
# Private
["varbits", NetworkSchemas._varbits(), _Bitset.of_bools([0, 1, 0, 1, 1, 0, 1]), 1]
]
for case in cases:
var name := case[0] as String
var serializer := case[1] as NetworkSchemaSerializer
var value = case[2]
var expected_size := case[3] as int
test(name, func():
var buffer := StreamPeerBuffer.new()
serializer.encode(value, buffer)
buffer.seek(0)
var decoded = serializer.decode(buffer)
expect_equal(decoded, value, "Value mismatch! Expected %s, got %s" % [value, decoded])
expect_equal(buffer.data_array.size(), expected_size, "Size mismatch! Expected %d, got %d" % [expected_size, buffer.data_array.size(), expected_size])
)
test("should handle negative degrees", func():
var input_value := -60.
var expected_value := 300.
var threshold := 1.
var buffer := StreamPeerBuffer.new()
var serializer := NetworkSchemas.degrees16()
serializer.encode(input_value, buffer)
buffer.seek(0)
var decoded := serializer.decode(buffer) as float
expect(abs(decoded - expected_value) < threshold, "Decoded %s while expected %s!" % [decoded, expected_value])
Vest.message("Difference: %s" % [abs(decoded - expected_value)])
)
test("should handle negative radians", func():
var input_value := -TAU / 6.
var expected_value := 5. / 6. * TAU
var threshold := 1. / TAU
var buffer := StreamPeerBuffer.new()
var serializer := NetworkSchemas.radians16()
serializer.encode(input_value, buffer)
buffer.seek(0)
var decoded := serializer.decode(buffer) as float
expect(abs(decoded - expected_value) < threshold, "Decoded %s while expected %s!" % [decoded, expected_value])
Vest.message("Difference: %s" % [abs(decoded - expected_value)])
)
@@ -0,0 +1 @@
uid://bk7cc3vjakruu
@@ -0,0 +1,102 @@
extends SnapshotSerializerTest
func get_suite_name() -> String:
return "DenseSnapshotSerializer"
func suite() -> void:
test("should deserialize to same", func():
var schema := _NetworkSchema.new()
var serializer := _DenseSnapshotSerializer.new(schema)
var subject := await get_subject()
NetworkIdentityServer.register_node(subject)
var snapshot := _Snapshot.of(0, [
[subject, "position", Vector3.ZERO],
[subject, "quaternion", Quaternion.from_euler(Vector3.ONE)],
[subject, "scale", Vector3.ONE]
], [subject])
var props := _PropertyPool.of([
[subject, "position"],
[subject, "quaternion"],
[subject, "scale"]
])
var serialized := serializer.write_for(1, snapshot, props)
var deserialized := serializer.read_from(1, props, to_buffer(serialized))
expect_equal(deserialized, snapshot)
Vest.message("Serialized %d props to %d bytes" % [snapshot.size(), serialized.size()])
)
test("should ignore unknown identifiers", func():
var schema := _NetworkSchema.new()
var writer_identity_server := _NetworkIdentityServer.new(TestingCommandServer.new())
var reader_identity_server := _NetworkIdentityServer.new(TestingCommandServer.new())
Vest.get_tree().root.add_child.call_deferred(writer_identity_server)
Vest.get_tree().root.add_child.call_deferred(reader_identity_server)
await writer_identity_server.ready; await reader_identity_server.ready
var writer_serializer := _DenseSnapshotSerializer.new(schema, writer_identity_server)
var reader_serializer := _DenseSnapshotSerializer.new(schema, reader_identity_server)
var known_subject := await get_subject()
var unknown_subject := await get_subject()
var writer_props := _PropertyPool.of([[known_subject, "position"], [unknown_subject, "position"]])
var reader_props := _PropertyPool.of([[known_subject, "position"]])
writer_identity_server.register_node(known_subject)
writer_identity_server.register_node(unknown_subject)
reader_identity_server.register_node(known_subject)
var snapshot := _Snapshot.of(0, [
[unknown_subject, "position", Vector3.ZERO],
[known_subject, "position", Vector3.ZERO]
], [known_subject, unknown_subject])
var expected := _Snapshot.of(0, [
[known_subject, "position", Vector3.ZERO]
], [known_subject])
var serialized := writer_serializer.write_for(1, snapshot, writer_props)
var deserialized := reader_serializer.read_from(1, reader_props, to_buffer(serialized))
expect_equal(deserialized, expected)
)
test("should handle missing data", func():
var schema := _NetworkSchema.new()
var serializer := _DenseSnapshotSerializer.new(schema)
var subject := await get_subject()
NetworkIdentityServer.register_node(subject)
var snapshot := _Snapshot.of(0, [
[subject, "position", Vector3.ZERO],
[subject, "quaternion", Quaternion.from_euler(Vector3.ONE)],
[subject, "scale", Vector3.ONE]
], [subject])
var props := _PropertyPool.of([
[subject, "position"],
[subject, "quaternion"],
[subject, "scale"]
])
var expected := _Snapshot.of(0, [
[subject, "position", Vector3.ZERO],
[subject, "quaternion", Quaternion.from_euler(Vector3.ONE)],
[subject, "scale", null]
], [subject])
var serialized := serializer.write_for(1, snapshot, props)
serialized = serialized.slice(0, serialized.size() - 4)
var deserialized := serializer.read_from(1, props, to_buffer(serialized))
expect_equal(deserialized, expected)
)
@@ -0,0 +1 @@
uid://cluy2t885ych6
@@ -0,0 +1,43 @@
extends SnapshotSerializerTest
func get_suite_name() -> String:
return "RedundantSnapshotSerializer"
func suite() -> void:
test("should deserialize to same", func():
var schema := _NetworkSchema.new()
var serializer := _RedundantSnapshotSerializer.new(schema)
var subject := Node3D.new()
Vest.get_tree().root.add_child.call_deferred(subject)
await subject.ready
NetworkIdentityServer.register_node(subject)
var snapshots := [
_Snapshot.of(0, [
[subject, "position", Vector3(0., 0., 0.)],
[subject, "quaternion", Quaternion.from_euler(Vector3.ONE)],
[subject, "scale", Vector3(1., 1., 1.)]
], [subject]),
_Snapshot.of(1, [
[subject, "position", Vector3(1., 0., 0.)],
[subject, "quaternion", Quaternion.from_euler(Vector3.ZERO)],
[subject, "scale", Vector3(1., .5, 1.)]
], [subject])
] as Array[_Snapshot]
var props := _PropertyPool.of([
[subject, "position"],
[subject, "quaternion"],
[subject, "scale"]
])
var serialized := serializer.write_for(1, snapshots, props)
var deserialized := serializer.read_from(1, props, to_buffer(serialized))
expect_equal(deserialized.size(), snapshots.size(), "Not all snapshots were deserialized!")
for i in snapshots.size():
expect_equal(deserialized[i], snapshots[i])
Vest.message("Serialized %d snapshots to %d bytes" % [snapshots.size(), serialized.size()])
)
@@ -0,0 +1 @@
uid://b8k4gyko3nrqj
@@ -0,0 +1,19 @@
extends VestTest
class_name SnapshotSerializerTest
func before_case(__):
# Makes sure local peer is 1, otherwise identifiers get random local IDs
Vest.get_tree().root.multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()
func to_buffer(data: PackedByteArray) -> StreamPeerBuffer:
var buffer := StreamPeerBuffer.new()
buffer.data_array = data
return buffer
func get_subject() -> Node3D:
var subject := Node3D.new()
Vest.get_tree().root.add_child.call_deferred(subject)
await subject.ready
return subject
@@ -0,0 +1 @@
uid://drf3txqsw1vlv
@@ -0,0 +1,72 @@
extends SnapshotSerializerTest
func get_suite_name() -> String:
return "SparseSnapshotSerializer"
func suite() -> void:
test("should deserialize to same", func():
var schema := _NetworkSchema.new()
var serializer := _SparseSnapshotSerializer.new(schema)
var subject := Node3D.new()
Vest.get_tree().root.add_child.call_deferred(subject)
await subject.ready
NetworkIdentityServer.register_node(subject)
var snapshot := _Snapshot.of(0, [
[subject, "position", Vector3.ZERO],
[subject, "quaternion", Quaternion.from_euler(Vector3.ONE)],
[subject, "scale", Vector3.ONE]
], [subject])
var props := _PropertyPool.of([
[subject, "position"],
[subject, "quaternion"],
[subject, "scale"]
])
var serialized := serializer.write_for(1, snapshot, props)
var deserialized := serializer.read_from(1, props, to_buffer(serialized))
expect_equal(deserialized, snapshot)
Vest.message("Serialized %d props to %d bytes" % [snapshot.size(), serialized.size()])
)
test("should ignore unknown identifiers", func():
var schema := _NetworkSchema.new()
var writer_identity_server := _NetworkIdentityServer.new(TestingCommandServer.new())
var reader_identity_server := _NetworkIdentityServer.new(TestingCommandServer.new())
Vest.get_tree().root.add_child.call_deferred(writer_identity_server)
Vest.get_tree().root.add_child.call_deferred(reader_identity_server)
await writer_identity_server.ready; await reader_identity_server.ready
var writer_serializer := _SparseSnapshotSerializer.new(schema, writer_identity_server)
var reader_serializer := _SparseSnapshotSerializer.new(schema, reader_identity_server)
var known_subject := await get_subject()
var unknown_subject := await get_subject()
var writer_props := _PropertyPool.of([[known_subject, "position"], [unknown_subject, "position"]])
var reader_props := _PropertyPool.of([[known_subject, "position"]])
writer_identity_server.register_node(known_subject)
writer_identity_server.register_node(unknown_subject)
reader_identity_server.register_node(known_subject)
var snapshot := _Snapshot.of(0, [
[unknown_subject, "position", Vector3.ZERO],
[known_subject, "position", Vector3.ZERO]
], [known_subject, unknown_subject])
var expected := _Snapshot.of(0, [
[known_subject, "position", Vector3.ZERO]
], [known_subject])
var serialized := writer_serializer.write_for(1, snapshot, writer_props)
var deserialized := reader_serializer.read_from(1, reader_props, to_buffer(serialized))
expect_equal(deserialized, expected)
)
@@ -0,0 +1 @@
uid://2l21sx6aw4ek
@@ -0,0 +1,72 @@
extends VestTest
func get_suite_name() -> String:
return "TicksetSerializer"
func test_should_serialize_empty():
# Given
var earliest_tick = 15
var latest_tick = 35
var active_ticks = _Set.new()
# When
var bytes = _TicksetSerializer.serialize(earliest_tick, latest_tick, active_ticks)
var deserialized = _TicksetSerializer.deserialize(bytes)
# Then
expect_equal(deserialized[0], earliest_tick)
expect_equal(deserialized[1], latest_tick)
expect_equal(deserialized[2], active_ticks)
func test_should_serialize_ticks():
# Given
var earliest_tick = 15
var latest_tick = 35
var active_ticks = _Set.of([15, 16, 20, 24])
# When
var bytes = _TicksetSerializer.serialize(earliest_tick, latest_tick, active_ticks)
var deserialized = _TicksetSerializer.deserialize(bytes)
# Then
expect_equal(deserialized[0], earliest_tick)
expect_equal(deserialized[1], latest_tick)
expect_equal(deserialized[2], active_ticks)
func test_should_serialize_only_range():
# Given
var earliest_tick = 16
var latest_tick = 20
var active_ticks = _Set.of([15, 16, 20, 24])
# When
var bytes = _TicksetSerializer.serialize(earliest_tick, latest_tick, active_ticks)
var deserialized = _TicksetSerializer.deserialize(bytes)
# Then
expect_equal(deserialized[0], earliest_tick)
expect_equal(deserialized[1], latest_tick)
expect_equal(deserialized[2], _Set.of([16, 20]))
func test_should_fail_on_earliest_after_latest():
# Given
var earliest_tick = 42
var latest_tick = 12
var active_ticks = _Set.new()
# When
var bytes = _TicksetSerializer.serialize(earliest_tick, latest_tick, active_ticks)
expect(bytes.is_empty())
func test_bandwidth():
var earliest_tick = 15
var latest_tick = 35
var active_ticks = _Set.of([15, 16, 20, 24])
var empty_buffer := _TicksetSerializer.serialize(earliest_tick, latest_tick, _Set.of([]))
var buffer := _TicksetSerializer.serialize(earliest_tick, latest_tick, active_ticks)
Vest.message("Data usage with empty tickset: %d bytes" % [empty_buffer.size()])
Vest.message("Data usage with %d active ticks: %d bytes" % [active_ticks.size(), buffer.size()])
ok()
@@ -0,0 +1 @@
uid://bd0su60kc3cyu
+77
View File
@@ -0,0 +1,77 @@
extends VestTest
func get_suite_name() -> String:
return "Snapshot"
func suite() -> void:
test("apply()", func():
# 22.46us, 367.25us, 1.44ms, 2.90ms, 54.68ms
# 23.92us, 331.23us, 1.35ms, 2.73ms, 45.37ms
for case in [[16, 2, 64], [128, 4, 8], [512, 4, 4], [1024, 4, 1], [16384, 4, 1]]:
var node_count := case[0] as int
var prop_count := case[1] as int
var batch_size := case[2] as int
var nodes := get_nodes(node_count)
var snapshot := _Snapshot.new(0)
for node in nodes:
for i in prop_count:
var prop := "property%d" % (i + 1)
snapshot.record_property(node, prop)
benchmark("apply() - %dn/%dp" % [node_count, prop_count], func(__):
snapshot.apply()
).with_duration(1.).with_batch_size(batch_size).run()
free_nodes(nodes)
)
test("merge()", func():
# 30.35us, 502.14us, 2.09ms, 4.85ms, 114.49ms
# 20.75us, 178.53us, 704.67us, 1.46ms, 31.61ms
for case in [[16, 2, 64], [128, 4, 8], [512, 4, 4], [1024, 4, 1], [16384, 4, 1]]:
var node_count := case[0] as int
var prop_count := case[1] as int
var batch_size := case[2] as int
var nodes := get_nodes(node_count)
var base_snapshot := _Snapshot.new(0)
var patch_snapshot := _Snapshot.new(0)
for node in nodes:
for i in prop_count:
var prop := "property%d" % (i + 1)
base_snapshot.set_property(node, prop, randi())
patch_snapshot.set_property(node, prop, randi())
benchmark("merge() - %dn/%dp" % [node_count, prop_count], func(__):
base_snapshot.merge(patch_snapshot)
).with_duration(1.).with_batch_size(batch_size).run()
free_nodes(nodes)
)
func get_nodes(count: int) -> Array[TestNode]:
var nodes := [] as Array[TestNode]
for i in count:
var node := TestNode.new()
node.name += " %d" % i
nodes.append(node)
return nodes
func free_nodes(nodes: Array[TestNode]) -> void:
for node in nodes:
node.queue_free()
class TestNode extends Node:
var property1 := randi()
var property2 := randi()
var property3 := randi()
var property4 := randi()
var property5 := randi()
var property6 := randi()
var property7 := randi()
var property8 := randi()
@@ -0,0 +1 @@
uid://do3iolh8ysmcb
+147
View File
@@ -0,0 +1,147 @@
extends VestTest
func get_suite_name() -> String:
return "Snapshot"
func suite() -> void:
var node := Node3D.new()
var other_node := Node3D.new()
define("make_patch()", func():
test("should return empty on same", func():
var snapshot := _Snapshot.of(0, [
[node, "position", Vector3.ZERO],
[node, "scale", Vector3.ONE]
], [node])
expect_empty(_Snapshot.make_patch(snapshot, snapshot))
)
test("should include differing property", func():
var from := _Snapshot.of(0, [
[node, "position", Vector3.ZERO],
[node, "scale", Vector3.ONE]
], [node])
var to := _Snapshot.of(0, [
[node, "position", Vector3.ONE],
[node, "scale", Vector3.ONE]
], [node])
var expected := _Snapshot.of(0, [
[node, "position", Vector3.ONE]
], [node])
expect_equal(_Snapshot.make_patch(from, to), expected)
)
test("should include new property", func():
var from := _Snapshot.of(0, [
[node, "position", Vector3.ZERO]
], [node])
var to := _Snapshot.of(0, [
[node, "position", Vector3.ZERO],
[node, "scale", Vector3.ONE]
], [node])
var expected := _Snapshot.of(0, [
[node, "scale", Vector3.ONE]
], [node])
expect_equal(_Snapshot.make_patch(from, to), expected)
)
test("patch should yield `to` on merge", func():
var from := _Snapshot.of(0, [
[node, "position", Vector3.ZERO],
[node, "scale", Vector3.ONE]
], [node])
var to := _Snapshot.of(0, [
[node, "position", Vector3.ONE],
[node, "scale", Vector3.ONE]
], [node])
var patch := _Snapshot.make_patch(from, to)
var applied := from.duplicate()
applied.tick = to.tick
applied.merge(patch)
expect_equal(applied, to)
)
)
define("merge()", func():
test("auth should override non-auth", func():
var snapshot := _Snapshot.of(0, [
[node, "position", Vector3.ZERO],
[other_node, "position", Vector3.ZERO]
], [])
var patch := _Snapshot.of(0, [
[node, "position", Vector3.ONE],
[other_node, "position", Vector3.ONE]
], [node])
expect_true(snapshot.merge(patch))
expect_equal(snapshot, _Snapshot.of(0, [
[node, "position", Vector3.ONE],
[other_node, "position", Vector3.ONE]
], [node]))
)
test("auth should update auth", func():
var snapshot := _Snapshot.of(0, [
[node, "position", Vector3.ZERO],
[other_node, "position", Vector3.ZERO]
], [node])
var patch := _Snapshot.of(0, [
[node, "position", Vector3.ONE],
[other_node, "position", Vector3.ONE]
], [node])
expect_true(snapshot.merge(patch))
expect_equal(snapshot, _Snapshot.of(0, [
[node, "position", Vector3.ONE],
[other_node, "position", Vector3.ONE]
], [node]))
)
test("non-auth should not update auth", func():
var snapshot := _Snapshot.of(0, [
[node, "position", Vector3.ZERO],
[other_node, "position", Vector3.ZERO]
], [node])
var patch := _Snapshot.of(0, [
[node, "position", Vector3.ONE],
[other_node, "position", Vector3.ONE]
], [])
expect_true(snapshot.merge(patch))
expect_equal(snapshot, _Snapshot.of(0, [
[node, "position", Vector3.ZERO],
[other_node, "position", Vector3.ONE]
], [node]))
)
test("non-auth should update non-auth", func():
var snapshot := _Snapshot.of(0, [
[node, "position", Vector3.ZERO],
[other_node, "position", Vector3.ZERO]
], [])
var patch := _Snapshot.of(0, [
[node, "position", Vector3.ONE],
[other_node, "position", Vector3.ONE]
], [])
expect_true(snapshot.merge(patch))
expect_equal(snapshot, _Snapshot.of(0, [
[node, "position", Vector3.ONE],
[other_node, "position", Vector3.ONE]
], []))
)
)
@@ -0,0 +1 @@
uid://csptcun1bgc5d
@@ -0,0 +1,61 @@
extends VestTest
func get_suite_name() -> String:
return "NetworkHistoryServer"
var idx := 0
func suite():
for count in [16, 1024, 16384]:
# register / record / restore / deregister
# *2.06us/235.37us/*46.01us/*1.37us, 14.05us/14.73ms/1.08ms/6.56us, 207.84us/234.73ms/23.18ms/88.84us
# *2.19us/77.97us/*23.46us/*3.07us, 1.73us/ 4.55ms/1.83ms/1.48us, 2.15us/ 81.64ms/45.45ms/ 1.57us
test("%d nodes" % count, func():
# Create nodes
var nodes := await get_nodes(count)
# Register nodes
idx = 0
benchmark("register()", func(__):
NetworkHistoryServer.register_rollback_state(nodes[idx], "name")
idx += 1
).with_iterations(count).with_batch_size(count).run()
# Record
benchmark("record()", func(__):
NetworkHistoryServer._record_rollback_state(0)
).with_duration(1.).with_batch_size(16).run()
# Restore
benchmark("restore()", func(__):
NetworkHistoryServer._restore_rollback_state(0)
).with_duration(1.).with_batch_size(16).run()
# Deregister nodes
idx = 0
benchmark("deregister()", func(__):
NetworkHistoryServer.deregister_rollback_state(nodes[idx], "name")
idx += 1
).with_iterations(count).with_batch_size(count).run()
# Free nodes
free_nodes(nodes)
)
func get_nodes(count: int) -> Array[Node]:
var nodes := [] as Array[Node]
for i in count:
var node := Node.new()
node.name = "Node %d" % i
nodes.append(node)
Vest.get_tree().root.add_child.call_deferred(node)
for node in nodes:
await node.ready
return nodes
func free_nodes(nodes: Array[Node]) -> void:
for node in nodes:
node.queue_free()
@@ -0,0 +1 @@
uid://bygji0wruckul
@@ -0,0 +1,66 @@
extends VestTest
func get_suite_name() -> String:
return "NetworkIdentityServer"
var command_server: _NetworkCommandServer
var identity_server: _NetworkIdentityServer
func before_case(__):
# Makes sure local peer is 1, otherwise identifiers get random local IDs
Vest.get_tree().root.multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()
command_server = _NetworkCommandServer.new()
identity_server = _NetworkIdentityServer.new(command_server)
Vest.get_tree().root.add_child.call_deferred(command_server)
Vest.get_tree().root.add_child.call_deferred(identity_server)
await command_server.ready
await identity_server.ready
func after_case(__):
identity_server.queue_free()
command_server.queue_free()
func suite() -> void:
define("resolve_reference()", func():
# 6.04us/7.08us, 26.75us/32.29us, 211.05us/240.66us, 4.55ms/4.35ms
# ??/3.69us, ??/3.74us, ??/3.81us, ??/3.99us
# 2.73us/3.63us, 2.73us/3.72us, 2.68us/3.68us, 2.91us/3.95us
for case in [[16, 512], [128, 512], [1024, 512], [16384, 512]]:
var count := case[0] as int
var batch := case[1] as int
# Run benchmarks
test("resolve %d nodes" % count, func():
# Setup nodes
var nodes := [] as Array[Node]
for i in count:
var node := Node.new()
node.name = "Node %d" % i
nodes.append(node)
Vest.get_tree().root.add_child.call_deferred(node)
# Wait for nodes to be in tree and register
for node in nodes:
await node.ready
identity_server.register_node(node)
benchmark("resolve by ID", func(__):
var id := (randi() % count) as int
var reference := _NetworkIdentityReference.of_id(id)
identity_server._resolve_reference(1, reference, false)
).with_batch_size(batch).with_duration(1.).run()
benchmark("resolve by name", func(__):
var id := (randi() % count) as int
var reference := _NetworkIdentityReference.of_full_name("/root/Node %d" % id)
identity_server._resolve_reference(1, reference, false)
).with_batch_size(batch).with_duration(1.).run()
# Free nodes
for node in nodes:
node.queue_free()
)
)
@@ -0,0 +1 @@
uid://cs0iapbvn2jta
@@ -0,0 +1,128 @@
extends VestTest
func get_suite_name() -> String:
return "NetworkIdentityServer"
var command_server: TestingCommandServer
var identity_server: _NetworkIdentityServer
var node: Node
var orphan_node: Node
func before_case(__):
# Makes sure local peer is 1, otherwise identifiers get random local IDs
Vest.get_tree().root.multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()
command_server = TestingCommandServer.new()
identity_server = _NetworkIdentityServer.new(command_server)
node = Node.new()
node.name = "Identified Node"
orphan_node = Node.new()
Vest.get_tree().root.add_child.call_deferred(node)
Vest.get_tree().root.add_child.call_deferred(command_server)
Vest.get_tree().root.add_child.call_deferred(identity_server)
await node.ready
await command_server.ready
await identity_server.ready
func after_case(__):
identity_server.queue_free()
command_server.queue_free()
node.queue_free()
orphan_node.queue_free()
func suite() -> void:
define("register_node()", func():
test("should register", func():
# Register node
identity_server.register_node(node)
# Assert for identifier
var identifier := identity_server._get_identifier_of(node)
expect_not_null(identifier)
expect_equal(identifier.get_full_name(), "/root/Identified Node")
)
test("should fail on node node in tree", func():
# Register node
identity_server.register_node(orphan_node)
# Assert for identifier
var identifier := identity_server._get_identifier_of(orphan_node)
expect_null(identifier)
)
)
define("deregister_node()", func():
test("should remove known", func():
# Register node
identity_server.register_node(node)
expect_not_null(identity_server._get_identifier_of(node))
# Deregister
identity_server.deregister_node(node)
expect_null(identity_server._get_identifier_of(node))
)
test("should do nothing on unknown", func():
identity_server.deregister_node(orphan_node)
expect_null(identity_server._get_identifier_of(orphan_node))
)
)
define("flush_queue()", func():
test("should send ids", func():
# Register node
identity_server.register_node(node)
var identifier := identity_server._get_identifier_of(node)
var full_name := identifier.get_full_name()
# Try and resolve some unknown identities
identity_server._resolve_reference(2, identifier.reference_for(2))
identity_server._resolve_reference(3, identifier.reference_for(3))
# Flush queue
identity_server.flush_queue()
# Check commands sent
expect_equal(command_server.commands_sent.size(), 2)
for i in command_server.commands_sent.size():
var command := command_server.commands_sent[i]
expect_equal(command[0], 0) # Command id
expect_equal(command[2], 2 + i) # Peer
expect_equal(command[3], MultiplayerPeer.TRANSFER_MODE_RELIABLE)
)
)
define("_resolve_reference()", func():
test("should return by id", func():
# Register node
identity_server.register_node(node)
var identifier := identity_server._get_identifier_of(node)
# Resolve
var reference := _NetworkIdentityReference.of_id(identifier.get_local_id())
expect_equal(identity_server._resolve_reference(1, reference), identifier)
)
test("should return by name", func():
# Register node
identity_server.register_node(node)
var identifier := identity_server._get_identifier_of(node)
# Resolve
var reference := _NetworkIdentityReference.of_full_name(identifier.get_full_name())
expect_equal(identity_server._resolve_reference(1, reference), identifier)
)
test("should return null on unknown", func():
var reference := _NetworkIdentityReference.of_full_name("Unknown Node")
expect_null(identity_server._resolve_reference(1, reference))
)
)
@@ -0,0 +1 @@
uid://si105rtl84q6
@@ -0,0 +1,58 @@
extends VestTest
func get_suite_name() -> String:
return "NetworkSynchronizationServer"
var servers: TestingServers
func before_case(__):
# Makes sure local peer is 1, otherwise identifiers get random local IDs
Vest.get_tree().root.multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()
servers = await TestingServers.create()
servers.synchronization_server()._rb_enable_input_broadcast = true # Force input broadcast
func after_case(__):
servers.queue_free()
func suite() -> void:
define("synchronize_input()", func():
test("should submit owned", func():
var owned_node := await get_node()
var other_node := await get_node()
other_node.set_multiplayer_authority(2)
servers.history_server().register_rollback_input(owned_node, "position")
servers.history_server().register_rollback_input(other_node, "position")
servers.synchronization_server().register_rollback_input(owned_node, "position")
servers.synchronization_server().register_rollback_input(other_node, "position")
servers.history_server()._record_rollback_input(0)
servers.synchronization_server()._synchronize_input(0)
skip() # Somehow setup a live client-server connection
)
)
define("synchronize_state()", func():
test("should submit owned", func():
skip()
)
)
define("synchronize_sync_state()", func():
test("should submit owned", func():
skip()
)
)
func get_node(name: String = "") -> Node3D:
var node := Node3D.new()
Vest.get_tree().root.add_child.call_deferred(node)
await node.ready
if name: node.name = name
return node
@@ -0,0 +1 @@
uid://csdkme0ex4dxl
@@ -0,0 +1,114 @@
extends VestTest
func get_suite_name() -> String:
return "RollbackSimulationServer"
func before_case(__):
# Makes sure local peer is 1, otherwise identifiers get random local IDs
Vest.get_tree().root.multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()
func suite() -> void:
define("is_predicting()", func():
test("should predict non-owned node", func():
var state_node := await get_node()
var input_node := await get_node()
var input_snapshot := _Snapshot.of(1, [[input_node, "name", "Input"]], [])
state_node.set_multiplayer_authority(2)
RollbackSimulationServer.register_rollback_input_for(state_node, input_node)
expect(RollbackSimulationServer._is_predicting(input_snapshot, state_node))
)
test("should predict owned node without input", func():
var input_snapshot := _Snapshot.new(0)
var state_node := await get_node()
var input_node := await get_node()
RollbackSimulationServer.register_rollback_input_for(state_node, input_node)
expect(RollbackSimulationServer._is_predicting(input_snapshot, state_node))
)
test("should predict non-owned inputless", func():
var input_snapshot := _Snapshot.new(0)
var state_node := await get_node()
state_node.set_multiplayer_authority(2)
expect(RollbackSimulationServer._is_predicting(input_snapshot, state_node))
)
test("should not predict owned inputless", func():
var input_snapshot := _Snapshot.new(0)
var state_node := await get_node()
expect_not(RollbackSimulationServer._is_predicting(input_snapshot, state_node))
)
test("should not predict owned with input", func():
var state_node := await get_node()
var input_node := await get_node()
var input_snapshot := _Snapshot.of(1, [[input_node, "name", "Input"]], [input_node])
RollbackSimulationServer.register_rollback_input_for(state_node, input_node)
expect_not(RollbackSimulationServer._is_predicting(input_snapshot, state_node))
)
)
define("get_nodes_to_simulate()", func():
test("should not simulate without input", func():
var node := RewindableNode.new()
var input_node := Node.new()
var server := _RollbackSimulationServer.new()
server.register(node._rollback_tick)
server.register_rollback_input_for(node, input_node)
var snapshot := _Snapshot.new(1)
expect_empty(server._get_nodes_to_simulate(snapshot))
)
test("should simulate with input", func():
var node := RewindableNode.new()
var input_node := Node.new()
var server := _RollbackSimulationServer.new()
server.register(node._rollback_tick)
server.register_rollback_input_for(node, input_node)
var snapshot := _Snapshot.new(1)
snapshot.set_property(input_node, "editor_description", "Test input node")
snapshot.set_auth(input_node, true)
expect_equal(server._get_nodes_to_simulate(snapshot), [node])
)
test("should simulate mutated", func():
var node := RewindableNode.new()
var input_node := Node.new()
var server := _RollbackSimulationServer.new()
server.register(node._rollback_tick)
server.register_rollback_input_for(node, input_node)
var snapshot := _Snapshot.new(1)
NetworkRollback.mutate(node)
expect_equal(server._get_nodes_to_simulate(snapshot), [node])
)
)
class RewindableNode extends Node:
func _rollback_tick(_dt, _t, _if) -> void:
pass
func get_node() -> Node:
var node := Node.new()
Vest.get_tree().root.add_child.call_deferred(node)
await node.ready
return node
@@ -0,0 +1 @@
uid://bxv65630oy2jj
@@ -0,0 +1,7 @@
extends _NetworkCommandServer
class_name TestingCommandServer
var commands_sent := [] as Array[Array]
func send_command(idx: int, data: PackedByteArray, target_peer: int = 0, mode: MultiplayerPeer.TransferMode = MultiplayerPeer.TRANSFER_MODE_RELIABLE, channel: int = 0) -> void:
commands_sent.append([idx, data, target_peer, mode, channel])
@@ -0,0 +1 @@
uid://dch0o2dsb4bv1
+52
View File
@@ -0,0 +1,52 @@
extends Node
class_name TestingServers
var _command_server: CommandServer
var _identity_server: _NetworkIdentityServer
var _history_server: _NetworkHistoryServer
var _synchronization_server: _NetworkSynchronizationServer
var _simulation_server: _RollbackSimulationServer
static func create() -> TestingServers:
var servers := TestingServers.new()
Vest.get_tree().root.add_child.call_deferred(servers)
await servers.ready
return servers
func _ready():
_command_server = CommandServer.new()
_history_server = _NetworkHistoryServer.new()
_identity_server = _NetworkIdentityServer.new(_command_server)
_synchronization_server = _NetworkSynchronizationServer.new(_command_server, _history_server, _identity_server, _simulation_server)
_simulation_server = _RollbackSimulationServer.new(_history_server)
var servers := [_command_server, _history_server, _identity_server, _synchronization_server, _simulation_server]
for server in servers:
add_child.call_deferred(server)
for server in servers:
await server.ready
func command_server() -> CommandServer:
return _command_server
func identity_server() -> _NetworkIdentityServer:
return _identity_server
func history_server() -> _NetworkHistoryServer:
return _history_server
func synchronization_server() -> _NetworkSynchronizationServer:
return _synchronization_server
func simulation_server() -> _RollbackSimulationServer:
return _simulation_server
class CommandServer extends _NetworkCommandServer:
var commands_sent := [] as Array[Array]
func send_command(idx: int, data: PackedByteArray, target_peer: int = 0, mode: MultiplayerPeer.TransferMode = MultiplayerPeer.TRANSFER_MODE_RELIABLE, channel: int = 0) -> void:
commands_sent.append([idx, data, target_peer, mode, channel])
@@ -0,0 +1 @@
uid://b0xnh4u80f7ux
@@ -0,0 +1,69 @@
extends VestTest
func get_suite_name() -> String:
return "NetworkTickrateHandshake"
func suite() -> void:
var base_tickrate := 60
var mismatching_tickrate := 48
on_case_begin.connect(func(__):
# Reset tickrate
NetworkMocks.set_tickrate(base_tickrate)
)
test("client should disconnect on mismatch", func():
# Difficult to pretend we're a client AND detect a disconnect
todo()
)
test("client should adjust on mismatch", func():
var handshake := await get_handshake(false)
handshake.mismatch_action = NetworkTickrateHandshake.ADJUST
handshake.submit_tickrate(mismatching_tickrate)
expect_equal(NetworkTime.tickrate, mismatching_tickrate, "Tickrate was not adjusted!")
)
test("server should not adjust on mismatch", func():
var handshake := await get_handshake(true)
handshake.mismatch_action = NetworkTickrateHandshake.ADJUST
handshake.submit_tickrate(mismatching_tickrate)
expect_equal(NetworkTime.tickrate, base_tickrate, "Tickrate was adjusted!")
)
test("client should emit signal on mismatch", func():
var handshake := await get_handshake(false)
handshake.mismatch_action = NetworkTickrateHandshake.SIGNAL
capture_signal(handshake.on_tickrate_mismatch, 2)
handshake.submit_tickrate(mismatching_tickrate)
expect_equal(
get_signal_emissions(handshake.on_tickrate_mismatch),
[[0, mismatching_tickrate]]
)
)
func get_handshake(is_authority: bool) -> TestingTickrateHandshake:
var handshake := TestingTickrateHandshake.new(is_authority)
# Add to tree
# TODO(vest): Use Vest.get_tree() after upgrade
Vest._scene_tree.root.add_child.call_deferred(handshake)
await handshake.ready
return handshake
class TestingTickrateHandshake extends NetworkTickrateHandshake:
var _is_tickrate_auth: bool
func _init(p_is_tickrate_auth: bool):
_is_tickrate_auth = p_is_tickrate_auth
func submit_tickrate(tickrate: int):
_submit_tickrate(tickrate)
func _is_authority() -> bool:
return _is_tickrate_auth
@@ -0,0 +1 @@
uid://sjw7blpv81fr