Add collaborative surface drawing

This commit is contained in:
Alexander Sellite 2026-08-01 22:55:39 -04:00
parent 6b7ef4c11b
commit b92c55562d
28 changed files with 3504 additions and 2 deletions

View file

@ -11,6 +11,7 @@ var _known: KnownPlayerStore
var _spawn: PlayerSpawnService
var _chat: NetworkChatService
var _mail: NetworkMailService
var _surface_drawing: NetworkSurfaceDrawingService
var _revision := 0
var _host_block_pairs: Dictionary[String, bool] = {}
var _peer_fingerprints: Dictionary[int, String] = {}
@ -96,6 +97,42 @@ func get_bans() -> Array[Dictionary]:
return _bans.get_bans(_session.get_host_identity_fingerprint()) if _session.is_host() else []
func set_surface_drawing_service(
service: NetworkSurfaceDrawingService,
) -> void:
_surface_drawing = service
if (
_surface_drawing != null
and not _surface_drawing.session_artwork_changed.is_connected(
_on_session_artwork_changed
)
):
_surface_drawing.session_artwork_changed.connect(
_on_session_artwork_changed
)
_changed()
func get_session_artwork_counts() -> Vector2i:
if _surface_drawing == null:
return Vector2i.ZERO
return Vector2i(
_surface_drawing.get_canvas_count(),
_surface_drawing.get_painted_cell_count(),
)
func reset_session_artwork() -> bool:
if not _session.is_host() or _surface_drawing == null:
return false
var ok: bool = _surface_drawing.clear_session_artwork()
moderation_finished.emit(
ok,
"Session artwork cleared." if ok else "Session artwork could not be cleared.",
)
return ok
func set_muted(fingerprint: String, display_name: String, value: bool) -> bool:
if fingerprint == _session.get_local_identity_fingerprint():
return false
@ -269,3 +306,10 @@ func _entry_before(a: PlayerListEntry, b: PlayerListEntry) -> bool:
func _changed() -> void:
_revision += 1
entries_changed.emit()
func _on_session_artwork_changed(
_canvas_count: int,
_painted_cell_count: int,
) -> void:
_changed()

View file

@ -10,7 +10,7 @@ const MAX_PUBLIC_KEY_LENGTH: int = 8192
const MAX_SIGNATURE_LENGTH: int = 2048
# ENet channels: 0 reliable lifecycle, 1 movement input, 2 movement
# snapshots, 3 fishing input, 4 fishing snapshots, 5 reliable sales,
# 6 reliable shop transactions, 7 reliable item/equipment/showcase lifecycle,
# 6 reliable shop transactions, 7 reliable item/equipment/showcase/drawing,
# 8 reliable ordered session chat, 9 reliable private session mail.
const SALE_RELIABLE_CHANNEL: int = 5
const SHOP_RELIABLE_CHANNEL: int = 6
@ -18,6 +18,7 @@ const ITEM_RELIABLE_CHANNEL: int = 7
const CHAT_RELIABLE_CHANNEL: int = 8
const MAIL_RELIABLE_CHANNEL: int = 9
const ENET_CHANNEL_COUNT: int = 10
const SURFACE_DRAWING_CAPABILITY: String = "surface_drawing_v1"
enum RejectionCode {
NONE,
@ -83,7 +84,9 @@ static func make_client_hello(
"local_profile_id": profile_id,
"display_name": display_name,
"client_nonce": client_nonce,
"capability_flags": PackedStringArray(),
"capability_flags": PackedStringArray([
SURFACE_DRAWING_CAPABILITY,
]),
"cosmetic_snapshot": cosmetic_snapshot,
"identity_fingerprint": identity_fingerprint,
"identity_signature": identity_signature,
@ -122,6 +125,16 @@ static func validate_client_hello(data: Variant) -> String:
TYPE_ARRAY,
]:
return "Capabilities are invalid."
var capabilities: Variant = payload["capability_flags"]
if capabilities.size() > 32:
return "Capabilities are invalid."
for capability: Variant in capabilities:
if (
typeof(capability) not in [TYPE_STRING, TYPE_STRING_NAME]
or str(capability).is_empty()
or str(capability).length() > 64
):
return "Capabilities are invalid."
if typeof(payload["cosmetic_snapshot"]) != TYPE_DICTIONARY:
return "Cosmetic snapshot is invalid."
if (
@ -188,6 +201,7 @@ static func make_server_hello(
"item_use_v1",
"equipment_v1",
"fish_showcase_v1",
SURFACE_DRAWING_CAPABILITY,
"chat_v1",
"mail_v1",
"profile_v1",

View file

@ -168,6 +168,7 @@ func start_private_host(port: int = DEFAULT_PORT) -> bool:
NetworkProtocol.PROTOCOL_VERSION,
_player_identity.fingerprint,
_player_identity.public_pem,
PackedStringArray([NetworkProtocol.SURFACE_DRAWING_CAPABILITY]),
)
_registry.update_appearance(1, _local_appearance_snapshot)
var host_profile_hello := NetworkProtocol.make_client_hello(
@ -361,6 +362,7 @@ func supports_server_capability(capability: StringName) -> bool:
return str(capability) in PackedStringArray([
"movement_v1", "fishing_v1", "sale_v1", "shop_v1",
"item_use_v1", "equipment_v1", "fish_showcase_v1",
NetworkProtocol.SURFACE_DRAWING_CAPABILITY,
"chat_v1",
"mail_v1",
"profile_v1",
@ -373,6 +375,14 @@ func get_peer_record(peer_id: int) -> PeerRegistry.PeerRecord:
return _registry.get_peer(peer_id)
func peer_supports_capability(
peer_id: int,
capability: StringName,
) -> bool:
var record: PeerRegistry.PeerRecord = _registry.get_peer(peer_id)
return record != null and str(capability) in record.capability_flags
func get_authenticated_peer_ids() -> Array[int]:
return _registry.get_peer_ids()
@ -931,6 +941,7 @@ func submit_client_hello(data: Dictionary) -> void:
NetworkProtocol.PROTOCOL_VERSION,
identity["fingerprint"],
identity["public_key"],
_sanitized_capabilities(data.get("capability_flags", [])),
):
_reject_peer(
sender_id,
@ -1062,6 +1073,7 @@ func receive_server_hello(data: Dictionary) -> void:
NetworkProtocol.PROTOCOL_VERSION,
_player_identity.fingerprint,
_player_identity.public_pem,
PackedStringArray([NetworkProtocol.SURFACE_DRAWING_CAPABILITY]),
)
_registry.update_appearance(local_peer_id, _local_appearance_snapshot)
var local_record := _registry.get_peer(local_peer_id)
@ -1143,6 +1155,7 @@ func _apply_spawn_entry(entry: Dictionary) -> void:
NetworkProtocol.PROTOCOL_VERSION,
str(entry.get("identity_fingerprint", "")),
str(entry.get("identity_public_key", "")),
_sanitized_capabilities(entry.get("capability_flags", [])),
)
var added_record := _registry.get_peer(peer_id)
if added_record != null and added_record.identity_authenticated:
@ -1218,9 +1231,28 @@ func _make_spawn_entry(
record.profile_authorization.duplicate(true)
if record != null else {}
),
"capability_flags": (
record.capability_flags.duplicate()
if record != null else PackedStringArray()
),
}
func _sanitized_capabilities(value: Variant) -> PackedStringArray:
var result := PackedStringArray()
if typeof(value) not in [TYPE_ARRAY, TYPE_PACKED_STRING_ARRAY]:
return result
for capability: Variant in value:
if (
typeof(capability) in [TYPE_STRING, TYPE_STRING_NAME]
and not str(capability).is_empty()
and str(capability).length() <= 64
and str(capability) not in result
):
result.append(str(capability))
return result
func _verify_spawn_identity(entry: Dictionary) -> bool:
var fingerprint := str(entry.get("identity_fingerprint", ""))
var public_pem := NetworkIdentityCrypto.normalize_public_pem(

File diff suppressed because it is too large Load diff

View file

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

View file

@ -16,6 +16,7 @@ class PeerRecord:
var identity_public_key: String = ""
var identity_authenticated: bool = false
var profile_authorization: Dictionary = {}
var capability_flags: PackedStringArray = PackedStringArray()
var _records: Dictionary[int, PeerRecord] = {}
@ -28,6 +29,7 @@ func add_peer(
protocol_version: int,
identity_fingerprint: String = "",
identity_public_key: String = "",
capability_flags: PackedStringArray = PackedStringArray(),
) -> bool:
if (
peer_id <= 0
@ -49,6 +51,7 @@ func add_peer(
record.identity_authenticated = NetworkIdentityCrypto.valid_fingerprint(
identity_fingerprint
)
record.capability_flags = capability_flags.duplicate()
_records[peer_id] = record
return true