Multiplayer clients spawn at 0,0,0. Host spawns at indicated position

Godot Version

v4.2.1

Question

I have some code that allows a host to create a server and spawn next to a defined spawn point. Clients can join and spawn, however they are positioning at 0,0,0 instead of the spawn point. They can otherwise move and see/be seen by the host correctly.

The players ServerSynchronizer is set to ‘spawn’ and ‘always’. I’ve checked that the player is being added to the correct node.

The add_player function instantiates the clients character, sets the id/name, positions it and adds it to a dedicated, empty node.

func add_player(id: int):
	# Instantiate a player and spawn it
	var character = preload("res://player.tscn").instantiate()

	# Set player id.
	character.player = id
	
	# Set player name
	character.name = str(id)
	
	# Get the position of the appropriate spawn point
	var spawnPoint = get_node("mship1").position
	print("Ship position: ", spawnPoint)

	# Move the player to that position
	character.position = Vector3(spawnPoint.x, spawnPoint.y + 20, spawnPoint.z - 20)
	print("Character position: ", character.position)
	$Players.add_child(character, true)

I’ve then added a print to the players ready function to confirm its position

func _ready():
	# Give authority over the player input to the appropriate peer.
	if not is_multiplayer_authority(): return
	# Set the camera as current if we are this player.
	if str(name).to_int() == multiplayer.get_unique_id():
		$Camera3D.current = true
		#var camera = $Camera3D.current
	capture_mouse()
	print("Players 'on ready' position: ", position)

When run, the host prints:

Ship position: (292.334, 22.0841, 724.584)
Character position: (292.334, 42.0841, 704.584)
Players 'on ready' position: (292.334, 42.0841, 704.584)

The client prints

Ship position: (292.334, 22.0841, 724.584)
Character position: (292.334, 42.0841, 704.584)
Players 'on ready' position: (0, 0, 0)

My best guess is this is some kind of client/server permission issue, but tbh its got me stumped.

After some poking, I’ve made a few observations.
1 - The client spawn is relevant to the ‘Players’ node that is acting as parent to the player nodes (This is an otherwise empty node, its just for organisation
2 - The host is firing the ‘on ready’ prints BEFORE the line that sets it position. Meanwhile, the client is doing the reverse.
3 - Even using global positioning, things don’t line up. In the below output, the ship global position is the actual spawn point, but the ‘on ready’ global-position is returning something else.

<HOST OUTPUT>
Ship global_position: (292.334, 22.0841, 724.584)
Players 'on ready' global-position: (82.0633, 23.4845, 762.729)
Players 'on ready' position: (0, 0, 0)
Character global_position: (292.334, 42.0841, 704.584)
<CLIENT OUTPUT>
Ship global_position: (292.334, 22.0841, 724.584)
Character global_position: (292.334, 42.0841, 704.584)
Players 'on ready' global-position: (82.0633, 23.4845, 762.729)
Players 'on ready' position: (0, 0, 0)

After some further testing, it seems that if I add the child and immediately set the position, the host spawns correctly, the client spawns on ‘Players’ node.

If I add the child and then await ‘character.ready’ before moving, both the host and the client spawn on ‘Players’ node.

Current iteration of code:

func _ready():
	# Give authority over the player input to the appropriate peer.
	if not is_multiplayer_authority(): return
	# Set the camera as current if we are this player.
	if str(name).to_int() == multiplayer.get_unique_id():
		$Camera3D.current = true
		#var camera = $Camera3D.current
	capture_mouse()
	print("Players 'on ready' global-position: ", global_position)
	print("Players 'on ready' position: ", position)
	return
func add_player(id: int):
	# Instantiate a player and spawn it
	var character = preload("res://player.tscn").instantiate()

	# Set player id.
	character.player = id
	print(character.player)

	# Set player name
	character.name = str(id)
	
	# Get the position of the appropriate spawn point
	var spawnPoint = get_node("mship1").global_position
	print("Ship global_position: ", spawnPoint)
	$Players.add_child(character, true)
	# Wait untill the 
	if not character.is_node_ready():
		print("Node not yet ready, waiting")
		await character.ready
		# Move the player to that position
		character.global_position = Vector3(spawnPoint.x, spawnPoint.y + 20, spawnPoint.z - 20)
		print("Character global_position: ", character.global_position)
	else:
		character.global_position = Vector3(spawnPoint.x, spawnPoint.y + 20, spawnPoint.z - 20)
		print("Character global_position: ", character.global_position)
	return