Fix multiplayer presentation and Fishnet synchronization

This commit is contained in:
Alexander Sellite 2026-08-13 21:58:43 -04:00
parent 31d459b3a7
commit 2045d8e288
19 changed files with 748 additions and 41 deletions

View file

@ -2,11 +2,13 @@ class_name NetworkJobService
extends Node
const MAX_SESSION_ID_LENGTH: int = 96
const BOARD_REQUEST_INTERVAL_SECONDS: float = 1.5
var _session: NetworkSession
var _jobs: PlayerJobService
var _sequence: int = 0
var _last_received_sequence: int = -1
var _board_request_accumulator: float = 0.0
func setup(session: NetworkSession, jobs: PlayerJobService) -> void:
@ -15,13 +17,36 @@ func setup(session: NetworkSession, jobs: PlayerJobService) -> void:
_session.state_changed.connect(_on_session_state_changed)
_session.peer_authenticated.connect(_on_peer_authenticated)
_jobs.board_changed.connect(_on_board_changed)
set_process(true)
func _process(delta: float) -> void:
if (
_session == null
or _jobs == null
or not _session.is_joined_client()
or not _session.supports_server_capability(
NetworkProtocol.JOBS_CAPABILITY
)
or _jobs.has_active_board()
):
_board_request_accumulator = 0.0
return
_board_request_accumulator += delta
if _board_request_accumulator < BOARD_REQUEST_INTERVAL_SECONDS:
return
_board_request_accumulator = 0.0
_request_remote_board()
func _on_session_state_changed(state: NetworkSession.State) -> void:
_sequence = 0
_last_received_sequence = -1
_board_request_accumulator = 0.0
if state == NetworkSession.State.JOINED_CLIENT:
if not _session.supports_server_capability(NetworkProtocol.JOBS_CAPABILITY):
if _session.supports_server_capability(NetworkProtocol.JOBS_CAPABILITY):
_request_remote_board()
else:
_jobs.clear_remote_board()
elif state in [
NetworkSession.State.INACTIVE,
@ -69,6 +94,33 @@ func _send_board(peer_id: int) -> void:
})
func _request_remote_board() -> void:
if (
_session != null
and _session.is_joined_client()
and _session.supports_server_capability(
NetworkProtocol.JOBS_CAPABILITY
)
):
request_job_board.rpc_id(1)
@rpc("any_peer", "call_remote", "reliable", 0)
func request_job_board() -> void:
var sender_id: int = multiplayer.get_remote_sender_id()
if (
_session == null
or not _session.is_host()
or sender_id <= 1
or not _session.is_authenticated_peer(sender_id)
or not _session.peer_supports_capability(
sender_id, NetworkProtocol.JOBS_CAPABILITY
)
):
return
_send_board(sender_id)
@rpc("authority", "call_remote", "reliable", 0)
func receive_job_board(data: Dictionary) -> void:
if (
@ -86,6 +138,7 @@ func receive_job_board(data: Dictionary) -> void:
var board: Dictionary = data.get("board", {})
if _jobs.apply_remote_board(board):
_last_received_sequence = sequence
_board_request_accumulator = 0.0
static func validate_snapshot(value: Variant) -> bool:

View file

@ -0,0 +1,94 @@
class_name NetworkPlayerAnimationProtocol
extends RefCounted
const FORMAT_VERSION: int = 1
const MAX_STATE_ID_LENGTH: int = 64
const MAX_ACTION_SEQUENCE: int = 2147483647
const MAX_ACTION_ELAPSED_SECONDS: float = 86400.0
const LOCOMOTION_IDLE: StringName = &"idle"
const LOCOMOTION_WALKING: StringName = &"walking"
const LOCOMOTION_RUNNING: StringName = &"running"
static func make_state(
locomotion_id: StringName,
grounded: bool,
action_id: StringName = &"",
action_sequence: int = 0,
action_elapsed: float = 0.0,
) -> Dictionary:
return {
"format_version": FORMAT_VERSION,
"locomotion_id": String(locomotion_id),
"grounded": grounded,
"action": make_action_state(
action_id, action_sequence, action_elapsed
),
}
static func make_action_state(
action_id: StringName = &"",
action_sequence: int = 0,
action_elapsed: float = 0.0,
) -> Dictionary:
return {
"id": String(action_id),
"sequence": action_sequence,
"elapsed": action_elapsed,
}
static func validate_state(value: Variant) -> bool:
if typeof(value) != TYPE_DICTIONARY:
return false
var state: Dictionary = value
return (
typeof(state.get("format_version")) == TYPE_INT
and int(state["format_version"]) == FORMAT_VERSION
and valid_state_id(state.get("locomotion_id"), false)
and typeof(state.get("grounded")) == TYPE_BOOL
and validate_action_state(state.get("action"))
)
static func validate_action_state(value: Variant) -> bool:
if typeof(value) != TYPE_DICTIONARY:
return false
var action: Dictionary = value
if (
not valid_state_id(action.get("id"), true)
or typeof(action.get("sequence")) != TYPE_INT
or typeof(action.get("elapsed")) not in [TYPE_FLOAT, TYPE_INT]
):
return false
var sequence: int = int(action["sequence"])
var elapsed: float = float(action["elapsed"])
return (
sequence >= 0
and sequence <= MAX_ACTION_SEQUENCE
and is_finite(elapsed)
and elapsed >= 0.0
and elapsed <= MAX_ACTION_ELAPSED_SECONDS
)
static func valid_state_id(value: Variant, allow_empty: bool) -> bool:
if typeof(value) not in [TYPE_STRING, TYPE_STRING_NAME]:
return false
var state_id: String = str(value)
if state_id.is_empty():
return allow_empty
if state_id.length() > MAX_STATE_ID_LENGTH:
return false
for index: int in state_id.length():
var codepoint: int = state_id.unicode_at(index)
var valid_character: bool = (
codepoint >= 97 and codepoint <= 122
or codepoint >= 48 and codepoint <= 57
or codepoint == 95
)
if not valid_character:
return false
return true

View file

@ -0,0 +1 @@
uid://cmaaep4n3p2tq

View file

@ -1,7 +1,7 @@
class_name NetworkProtocol
extends RefCounted
const PROTOCOL_VERSION: int = 3
const PROTOCOL_VERSION: int = 4
const GAME_BUILD: String = "prealpha"
const MAX_GAME_VERSION_LENGTH: int = 64
const MAX_DISPLAY_NAME_LENGTH: int = 24

View file

@ -9,6 +9,7 @@ const CONNECTION_TIMEOUT_SECONDS: float = 10.0
const AUTHENTICATION_TIMEOUT_SECONDS: float = 60.0
const INPUT_INTERVAL: float = 1.0 / 30.0
const SNAPSHOT_INTERVAL: float = 1.0 / 30.0
const MAX_MOVEMENT_INPUT_SEQUENCE: int = 2147483647
signal state_changed(state: State)
signal status_message_changed(message: String)
@ -1741,9 +1742,10 @@ func _is_valid_movement_input(data: Dictionary) -> bool:
or typeof(data.get("sprint")) != TYPE_BOOL
or typeof(data.get("sneak")) != TYPE_BOOL
or typeof(data.get("slow_walk")) != TYPE_BOOL
or (
data.has("sitting")
and typeof(data.get("sitting")) != TYPE_BOOL
or typeof(data.get("sitting")) != TYPE_BOOL
or typeof(data.get("casting")) != TYPE_BOOL
or not NetworkPlayerAnimationProtocol.validate_action_state(
data.get("animation_action")
)
or typeof(data.get("camera_yaw")) not in [TYPE_FLOAT, TYPE_INT]
):
@ -1751,11 +1753,18 @@ func _is_valid_movement_input(data: Dictionary) -> bool:
var axis: Array = data["axis"]
if axis.size() != 2:
return false
if (
typeof(axis[0]) not in [TYPE_FLOAT, TYPE_INT]
or typeof(axis[1]) not in [TYPE_FLOAT, TYPE_INT]
):
return false
var x: float = float(axis[0])
var y: float = float(axis[1])
var camera_yaw: float = float(data["camera_yaw"])
return (
is_finite(x)
int(data["sequence"]) > 0
and int(data["sequence"]) <= MAX_MOVEMENT_INPUT_SEQUENCE
and is_finite(x)
and is_finite(y)
and is_finite(camera_yaw)
and absf(x) <= 1.01