3 Commits

Author SHA1 Message Date
shawn 597d6dde2d Fix server clock reset in NetworkTimeSynchronizer and client-side peer 1 avatar duplicate
- Move _clock.set_time(0.) inside the 'if not server' block in
  NetworkTimeSynchronizer.start() so the server's SystemClock isn't
  reset to zero — fixes -1.78 billion second offset panic on client
- Skip spawning peer 1's avatar on clients (the server replicates
  all avatars; spawning peer 1 locally creates a duplicate that
  the dedicated server doesn't have)
2026-07-03 16:21:35 -04:00
shawn aad186552c Bake dev server IP (192.168.0.127:34197) as default in network popup 2026-07-03 16:11:23 -04:00
shawn 5a1695e2ab Fix Godot 4.7 typed array crash: add freed-object filtering in PropertyPool and PerObjectHistory 2026-07-03 16:09:59 -04:00
6 changed files with 33 additions and 8 deletions
+1 -2
View File
@@ -142,10 +142,9 @@ signal on_panic(offset: float)
func start() -> void: func start() -> void:
if _active: if _active:
return return
_clock.set_time(0.)
if not multiplayer.is_server(): if not multiplayer.is_server():
_clock.set_time(0.)
_active = true _active = true
_sample_idx = 0 _sample_idx = 0
_sample_buffer = _RingBuffer.new(sync_samples) _sample_buffer = _RingBuffer.new(sync_samples)
@@ -11,8 +11,14 @@ func _init(p_history_size: int):
_history_size = p_history_size _history_size = p_history_size
func subjects() -> Array[Object]: func subjects() -> Array[Object]:
# Filter out freed objects — Godot 4.7 stricter typed arrays
# reject invalid references and crash the engine.
var valid := []
for o in _data.keys():
if is_instance_valid(o):
valid.append(o)
var result := [] as Array[Object] var result := [] as Array[Object]
result.assign(_data.keys()) result.assign(valid)
return result return result
func is_auth(tick: int, subject: Object) -> bool: func is_auth(tick: int, subject: Object) -> bool:
+7 -1
View File
@@ -49,8 +49,14 @@ func get_properties_of(subject: Object) -> Array[NodePath]:
return properties return properties
func get_subjects() -> Array[Object]: func get_subjects() -> Array[Object]:
# Filter out freed objects — Godot 4.7 stricter typed arrays
# reject invalid references and crash the engine.
var valid := []
for s in _properties_by_subject.keys():
if is_instance_valid(s):
valid.append(s)
var subjects := [] as Array[Object] var subjects := [] as Array[Object]
subjects.assign(_properties_by_subject.keys()) subjects.assign(valid)
return subjects return subjects
func is_empty() -> bool: func is_empty() -> bool:
@@ -73,8 +73,15 @@ func _handle_host():
round_manager.start_match() round_manager.start_match()
func _handle_new_peer(id: int): func _handle_new_peer(id: int):
# On a dedicated server, peer 1 (the server process) has no avatar.
# When a client connects, it receives on_peer_join for peer 1, but
# should skip spawning because the server doesn't have that avatar.
# This also applies to listen-server: the server replicates peer 1's
# avatar to clients; spawning a local copy would duplicate it.
if id == 1 and not multiplayer.is_server():
print("[PlayerSpawner] Client skipping spawn for peer 1 (server has no dedicated avatar)")
return
if _dedicated_server and id == 1: if _dedicated_server and id == 1:
# Peer 1 is the server itself — no avatar needed
return return
if id == 1 and not spawn_host_avatar: if id == 1 and not spawn_host_avatar:
print("[PlayerSpawner] Skipping spawn for peer 1 (spawn_host_avatar=false)") print("[PlayerSpawner] Skipping spawn for peer 1 (spawn_host_avatar=false)")
+2 -2
View File
@@ -34,7 +34,7 @@ text = "Address:"
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
size_flags_vertical = 0 size_flags_vertical = 0
text = "localhost" text = "192.168.0.127"
[node name="Port Label" type="Label" parent="LAN/Address Row"] [node name="Port Label" type="Label" parent="LAN/Address Row"]
layout_mode = 2 layout_mode = 2
@@ -44,7 +44,7 @@ text = "Port:"
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
size_flags_vertical = 0 size_flags_vertical = 0
text = "16384" text = "34197"
[node name="Actions Row" type="HBoxContainer" parent="LAN"] [node name="Actions Row" type="HBoxContainer" parent="LAN"]
layout_mode = 2 layout_mode = 2
+8 -1
View File
@@ -110,7 +110,14 @@ func _on_first_peer_join(_peer_id: int) -> void:
return return
_first_client_connected = true _first_client_connected = true
print("[HeadlessServer] First client connected — starting netfox tick loop...") print("[HeadlessServer] First client connected — letting avatar spawn...")
# Wait TWO frames so PlayerSpawner._handle_new_peer() creates the avatar
# BEFORE NetworkTime starts. The tick loop needs at least one subject.
await get_tree().process_frame
await get_tree().process_frame
print("[HeadlessServer] Starting netfox tick loop with player subjects...")
# Start netfox tick loop now that we have at least one player # Start netfox tick loop now that we have at least one player
if NetworkTime and NetworkTime.has_method("start"): if NetworkTime and NetworkTime.has_method("start"):