class_name NetworkSurfaceDrawingService extends Node const BrushHighlightShader: Shader = preload( "res://drawing/surface_drawing_highlight.gdshader" ) const ArtShopStockType = preload("res://economy/art_shop_stock.gd") signal hud_state_changed( is_active: bool, mode_name: String, color_name: String, color_value: Color, brush_size: int, grid_size: int, status: String, ) signal session_artwork_changed(canvas_count: int, painted_cell_count: int) const SOLID_SURFACE_MASK: int = 1 const MAX_DRAW_DISTANCE: float = 16.0 const SURFACE_VALIDATION_DEPTH: float = 0.5 const MIN_SURFACE_NORMAL_DOT: float = 0.35 const EDIT_INTERVAL_MSEC: int = 45 const REQUEST_WINDOW_MSEC: int = 1000 const MAX_REQUESTS_PER_WINDOW: int = 32 const MAX_RECENT_REQUEST_IDS: int = 64 const GRID_PREVIEW_SURFACE_OFFSET: float = 0.03 const POINTER_EDGE_MARGIN: float = 2.0 const INVALID_POINTER_SCREEN_POSITION := Vector2(-1.0, -1.0) enum GuideAction { NONE, HIDE, RESTORE, FINALIZE, } var _session: NetworkSession var _spawn_service: PlayerSpawnService var _relationships: PlayerRelationshipStore var _local_player: Player var _bag: PlayerBag var _hotbar: PlayerHotbar var _art_unlocks: PlayerArtUnlocks var _drawing_root: Node3D var _canvas_states: Dictionary[String, Dictionary] = {} var _canvas_nodes: Dictionary[String, SurfaceDrawingCanvas] = {} var _selected_canvas_id: String = "" var _brush_preview: MultiMeshInstance3D var _brush_preview_multimesh: MultiMesh var _brush_preview_material: ShaderMaterial var _placement_preview: MeshInstance3D var _placement_preview_material: StandardMaterial3D var _active: bool = false var _placing_grid: bool = false var _brush_size: int = 1 var _grid_size: int = SurfaceDrawingProtocol.DEFAULT_GRID_SIZE var _color_ids: Array[StringName] = [] var _color_index: int = 0 var _painting: bool = false var _erasing: bool = false var _eraser_mode: bool = false var _armed_guide_action: int = GuideAction.NONE var _armed_return_placement_mode: bool = false var _armed_return_eraser_mode: bool = false var _camera_look_active: bool = false var _prior_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_VISIBLE var _last_edited_cell := Vector3i(-1, -1, -1) var _active_stroke_id: String = "" var _last_local_stroke_id: String = "" var _last_edit_msec: int = 0 var _aim_hit: Dictionary = {} var _pointer_screen_position: Vector2 = Vector2.ZERO var _request_sequence: int = 0 var _canvas_sequence: int = 0 var _peer_request_times: Dictionary[int, PackedInt64Array] = {} var _peer_request_ids: Dictionary[int, PackedStringArray] = {} var _stroke_history_by_peer: Dictionary[int, Dictionary] = {} var _cell_last_stroke: Dictionary[String, String] = {} var _peer_art_entitlements: Dictionary[int, Dictionary] = {} func setup( session: NetworkSession, spawn_service: PlayerSpawnService, relationships: PlayerRelationshipStore, local_player: Player, drawing_root: Node3D, bag: PlayerBag, hotbar: PlayerHotbar, art_unlocks: PlayerArtUnlocks, ) -> void: _session = session _spawn_service = spawn_service _relationships = relationships _local_player = local_player _drawing_root = drawing_root _bag = bag _hotbar = hotbar _art_unlocks = art_unlocks _refresh_local_unlocks() if _session != null: _session.state_changed.connect(_on_session_state_changed) _session.peer_removed.connect(_on_peer_removed) if _relationships != null: _relationships.relationship_changed.connect( _on_relationship_changed ) if _bag != null and not _bag.contents_changed.is_connected( _on_local_art_entitlement_changed ): _bag.contents_changed.connect(_on_local_art_entitlement_changed) if _art_unlocks != null and not _art_unlocks.unlocks_changed.is_connected( _on_local_art_unlocks_changed ): _art_unlocks.unlocks_changed.connect(_on_local_art_unlocks_changed) _create_brush_preview() _create_placement_preview() set_process(true) _emit_hud_state("") func handle_input( event: InputEvent, can_open: bool, pointer_screen_position: Vector2 = INVALID_POINTER_SCREEN_POSITION, ) -> bool: if not _active: return false if not can_open: deactivate() return false if event.is_action_pressed("ui_cancel"): if _armed_guide_action != GuideAction.NONE: _cancel_armed_guide_action() return true if _placing_grid: _set_placement_mode(false) return true return false if event is InputEventKey: var key_event := event as InputEventKey if not key_event.pressed or key_event.echo: return false if key_event.ctrl_pressed and key_event.physical_keycode == KEY_Z: request_undo_last_stroke() return true match key_event.physical_keycode: KEY_Q: if not _placing_grid: _cycle_color(-1) return true KEY_E: if not _placing_grid: _cycle_color(1) return true KEY_R: _set_placement_mode(not _placing_grid) return true if event is InputEventMouseButton: var mouse_event := event as InputEventMouseButton # Ordinary wheel input always belongs to Hotbar selection. Brush size is # selected explicitly in the Art Kit toolbar. if mouse_event.button_index in [ MOUSE_BUTTON_WHEEL_UP, MOUSE_BUTTON_WHEEL_DOWN, ]: return false _set_pointer_from_input( mouse_event.position, pointer_screen_position, ) _update_aim() match mouse_event.button_index: MOUSE_BUTTON_LEFT: if _placing_grid: if mouse_event.pressed: if _armed_guide_action != GuideAction.NONE: _execute_armed_guide_action() elif mouse_event.shift_pressed and mouse_event.ctrl_pressed: _finalize_selected_guide() elif mouse_event.shift_pressed: _remove_selected_guide() else: _request_canvas_at_aim() return true if mouse_event.pressed: _begin_stroke() _erasing = ( mouse_event.pressed and (mouse_event.shift_pressed or _eraser_mode) ) _painting = mouse_event.pressed and not _erasing if _painting or _erasing: _submit_current_brush(_erasing, true) else: _finish_stroke() return true MOUSE_BUTTON_RIGHT: _camera_look_active = mouse_event.pressed _reset_stroke() return false if event is InputEventMouseMotion: var motion_event := event as InputEventMouseMotion if _camera_look_active: return false _set_pointer_from_input( motion_event.position, pointer_screen_position, ) _update_aim() return true return false func activate( pointer_screen_position: Vector2 = INVALID_POINTER_SCREEN_POSITION, ) -> void: if _active or not can_activate(): return _active = true _placing_grid = false _eraser_mode = false _clear_armed_guide_action(false) _refresh_local_unlocks() _rebuild_placement_preview() _reset_stroke() var initial_pointer_position: Vector2 = pointer_screen_position if not _is_valid_pointer_screen_position(initial_pointer_position): initial_pointer_position = get_viewport().get_mouse_position() _pointer_screen_position = _clamped_pointer_position( initial_pointer_position ) _prior_mouse_mode = Input.mouse_mode Input.mouse_mode = Input.MOUSE_MODE_VISIBLE _update_aim() _refresh_stencil_visibility() _emit_hud_state("marker selected") func deactivate() -> void: if not _active: return _active = false _placing_grid = false _eraser_mode = false _clear_armed_guide_action(false) _camera_look_active = false _reset_stroke() _selected_canvas_id = "" _aim_hit.clear() _hide_previews() _refresh_stencil_visibility() Input.mouse_mode = _prior_mouse_mode _emit_hud_state("") func is_active() -> bool: return _active func is_placement_mode() -> bool: return _placing_grid func can_activate() -> bool: return ( _drawing_available() and _owns_art_kit() and _art_kit_is_selected() ) func set_placement_mode(enabled: bool) -> void: _set_placement_mode(enabled) func set_color_id(color_id: StringName) -> bool: var color_index: int = _color_ids.find(color_id) if color_index < 0: return false _clear_armed_guide_action(true) _eraser_mode = false _color_index = color_index _reset_stroke() _emit_hud_state("") return true func set_eraser_mode(enabled: bool) -> void: if not _active: return _clear_armed_guide_action(true) _eraser_mode = enabled if _eraser_mode and _placing_grid: _placing_grid = false _reset_stroke() _selected_canvas_id = "" _update_aim() _refresh_stencil_visibility() _emit_hud_state("eraser selected" if _eraser_mode else "marker selected") func is_eraser_mode() -> bool: return _eraser_mode func arm_guide_action(action: int) -> bool: if ( not _active or action not in [GuideAction.HIDE, GuideAction.RESTORE, GuideAction.FINALIZE] ): return false if _armed_guide_action == action: _cancel_armed_guide_action() return false if _armed_guide_action == GuideAction.NONE: _armed_return_placement_mode = _placing_grid _armed_return_eraser_mode = _eraser_mode _armed_guide_action = action _placing_grid = true _eraser_mode = false _reset_stroke() _selected_canvas_id = "" _update_aim() _refresh_stencil_visibility() _emit_hud_state(_guide_action_status(action)) return true func get_armed_guide_action() -> int: return _armed_guide_action func cancel_armed_guide_action() -> void: _cancel_armed_guide_action() func set_brush_size(value: int) -> bool: if _art_unlocks == null or not _art_unlocks.is_brush_size_unlocked(value): return false _set_brush_size(value) return true func set_grid_size(value: int) -> bool: if _art_unlocks == null or not _art_unlocks.is_grid_size_unlocked(value): return false if _grid_size == value: return true _grid_size = value _rebuild_placement_preview() _update_previews() _emit_hud_state("") return true func get_brush_size() -> int: return _brush_size func get_grid_size() -> int: return _grid_size func get_color_id() -> StringName: return _current_color_id() func get_pointer_screen_position() -> Vector2: return _pointer_screen_position func set_unlocked_color_ids(unlocked_ids: Array[StringName]) -> void: _color_ids = SurfaceDrawingPalette.filter_unlocked_ids(unlocked_ids) _color_index = clampi(_color_index, 0, _color_ids.size() - 1) _emit_hud_state("") func _refresh_local_unlocks() -> void: if _art_unlocks == null: _color_ids = [SurfaceDrawingPalette.DEFAULT_COLOR_ID] return var previous_color: StringName = _current_color_id() _color_ids = _art_unlocks.get_unlocked_color_ids() var previous_index: int = _color_ids.find(previous_color) _color_index = previous_index if previous_index >= 0 else 0 if not _art_unlocks.is_brush_size_unlocked(_brush_size): _brush_size = PlayerArtUnlocks.BASE_BRUSH_SIZE if not _art_unlocks.is_grid_size_unlocked(_grid_size): _grid_size = PlayerArtUnlocks.BASE_GRID_SIZE func _owns_art_kit() -> bool: return _bag != null and _bag.owns_item(ArtShopStockType.ART_KIT_ITEM_ID) func _art_kit_is_selected() -> bool: return ( _hotbar != null and _hotbar.get_selected_assignment_kind() == PlayerHotbar.AssignmentKind.ITEM and _hotbar.get_selected_item_id() == ArtShopStockType.ART_KIT_ITEM_ID ) func _local_entitlement() -> Dictionary: return { "has_kit": _owns_art_kit(), "unlock_mask": ( _art_unlocks.get_unlock_mask() if _art_unlocks != null else 0 ), } func _publish_local_entitlement() -> void: if _session == null or not _session.is_gameplay_session_active(): return var entitlement: Dictionary = _local_entitlement() if _session.is_host(): _peer_art_entitlements[_session.get_local_peer_id()] = entitlement else: submit_art_entitlement.rpc_id( 1, _session.get_session_id(), bool(entitlement["has_kit"]), int(entitlement["unlock_mask"]), ) @rpc( "any_peer", "call_remote", "reliable", SurfaceDrawingProtocol.RELIABLE_CHANNEL, ) func submit_art_entitlement( session_id: String, has_kit: bool, unlock_mask: int, ) -> void: var sender_id: int = multiplayer.get_remote_sender_id() if ( _session == null or not _session.is_host() or not _session.is_authenticated_peer(sender_id) or session_id != _session.get_session_id() or unlock_mask < 0 or (unlock_mask & ~PlayerArtUnlocks.ALL_UNLOCK_MASK) != 0 ): return _peer_art_entitlements[sender_id] = { "has_kit": has_kit, "unlock_mask": unlock_mask, } func _peer_entitlement(peer_id: int) -> Dictionary: if _session != null and peer_id == _session.get_local_peer_id(): return _local_entitlement() return Dictionary(_peer_art_entitlements.get(peer_id, {})) func _peer_owns_art_kit(peer_id: int) -> bool: return bool(_peer_entitlement(peer_id).get("has_kit", false)) func _peer_can_place_grid(peer_id: int, grid_size: int) -> bool: var entitlement: Dictionary = _peer_entitlement(peer_id) return ( bool(entitlement.get("has_kit", false)) and _mask_unlocks_grid( int(entitlement.get("unlock_mask", 0)), grid_size ) ) func _peer_can_edit(peer_id: int, request: Dictionary) -> bool: var entitlement: Dictionary = _peer_entitlement(peer_id) if not bool(entitlement.get("has_kit", false)): return false var unlock_mask: int = int(entitlement.get("unlock_mask", 0)) if not _mask_unlocks_brush(unlock_mask, int(request["brush_size"])): return false for value: Variant in request["edits"]: var edit: Dictionary = value var color_id := StringName(str(edit.get("color_id", ""))) if not color_id.is_empty() and not _mask_unlocks_color( unlock_mask, color_id ): return false return true func _mask_unlocks_color(unlock_mask: int, color_id: StringName) -> bool: if color_id == SurfaceDrawingPalette.DEFAULT_COLOR_ID: return true for product_id: StringName in PlayerArtUnlocks.COLOR_PRODUCTS: if ( PlayerArtUnlocks.color_id_for_product(product_id) == color_id and _mask_owns_product(unlock_mask, product_id) ): return true return false func _mask_unlocks_brush(unlock_mask: int, brush_size: int) -> bool: if brush_size == PlayerArtUnlocks.BASE_BRUSH_SIZE: return true for product_id: StringName in PlayerArtUnlocks.BRUSH_PRODUCTS: if ( PlayerArtUnlocks.brush_size_for_product(product_id) == brush_size and _mask_owns_product(unlock_mask, product_id) ): return true return false func _mask_unlocks_grid(unlock_mask: int, grid_size: int) -> bool: if grid_size == PlayerArtUnlocks.BASE_GRID_SIZE: return true for product_id: StringName in PlayerArtUnlocks.GRID_PRODUCTS: if ( PlayerArtUnlocks.grid_size_for_product(product_id) == grid_size and _mask_owns_product(unlock_mask, product_id) ): return true return false func _mask_owns_product(unlock_mask: int, product_id: StringName) -> bool: var bit: int = PlayerArtUnlocks.get_product_bit(product_id) return bit >= 0 and (unlock_mask & (1 << bit)) != 0 func request_canvas_at_surface( origin: Vector3, normal: Vector3, tangent: Vector3, ) -> bool: if ( not can_activate() or _art_unlocks == null or not _art_unlocks.is_grid_size_unlocked(_grid_size) or normal.is_zero_approx() or tangent.is_zero_approx() ): return false var data: Dictionary = { "request_id": _new_request_id("canvas"), "session_id": _session.get_session_id(), "origin": SurfaceDrawingProtocol.vector_to_array(origin), "normal": SurfaceDrawingProtocol.vector_to_array(normal.normalized()), "tangent": SurfaceDrawingProtocol.vector_to_array(tangent.normalized()), "width": _grid_size, "height": _grid_size, "cell_size": SurfaceDrawingProtocol.CELL_SIZE, } if _session.is_host(): _handle_canvas_request(_session.get_local_peer_id(), data) else: submit_canvas_request.rpc_id(1, data) return true func request_cell_edits( canvas_id: String, edits: Array[Dictionary], stroke_id: String = "", ) -> bool: if ( not can_activate() or canvas_id.is_empty() or edits.is_empty() ): return false var resolved_stroke_id: String = stroke_id if resolved_stroke_id.is_empty(): resolved_stroke_id = _new_request_id("stroke") _last_local_stroke_id = resolved_stroke_id var data: Dictionary = { "request_id": _new_request_id("edit"), "session_id": _session.get_session_id(), "canvas_id": canvas_id, "stroke_id": resolved_stroke_id, "brush_size": _brush_size, "edits": edits, } if not SurfaceDrawingProtocol.validate_edit_request(data): return false if _session.is_host(): _handle_edit_request(_session.get_local_peer_id(), data) else: submit_edit_request.rpc_id(1, data) return true func request_guide_visibility( canvas_id: String, should_be_visible: bool, should_finalize: bool = false, ) -> bool: if not can_activate() or canvas_id.is_empty(): return false var data: Dictionary = { "request_id": _new_request_id("guide"), "session_id": _session.get_session_id(), "canvas_id": canvas_id, "guide_visible": should_be_visible and not should_finalize, "finalized": should_finalize, } if not SurfaceDrawingProtocol.validate_guide_request(data): return false if _session.is_host(): _handle_guide_request(_session.get_local_peer_id(), data) else: submit_guide_request.rpc_id(1, data) return true func request_undo_last_stroke() -> bool: if ( not can_activate() or _last_local_stroke_id.is_empty() ): _emit_hud_state("nothing to undo") return false var data: Dictionary = { "request_id": _new_request_id("undo"), "session_id": _session.get_session_id(), "stroke_id": _last_local_stroke_id, } if not SurfaceDrawingProtocol.validate_undo_request(data): return false _last_local_stroke_id = "" if _session.is_host(): _handle_undo_request(_session.get_local_peer_id(), data) else: submit_undo_request.rpc_id(1, data) _emit_hud_state("undoing last stroke...") return true func get_canvas_count() -> int: return _canvas_states.size() func get_painted_cell_count() -> int: var count: int = 0 for state: Dictionary in _canvas_states.values(): count += (state.get("cells", []) as Array).size() return count func clear_session_artwork() -> bool: if not _drawing_available() or not _session.is_host(): return false _clear_session_artwork_state() for peer_id: int in _session.get_authenticated_peer_ids(): if ( peer_id != _session.get_local_peer_id() and _session.peer_supports_capability( peer_id, SurfaceDrawingProtocol.CAPABILITY ) ): receive_session_artwork_reset.rpc_id( peer_id, _session.get_session_id() ) return true func get_canvas_ids() -> Array[String]: var result: Array[String] = [] for canvas_id: String in _canvas_states: result.append(canvas_id) result.sort() return result func get_canvas_state(canvas_id: String) -> Dictionary: return Dictionary(_canvas_states.get(canvas_id, {})).duplicate(true) func _process(_delta: float) -> void: if not _active: return if not can_activate(): deactivate() return _update_aim() if _placing_grid: return if _painting: _submit_current_brush(false) elif _erasing: _submit_current_brush(true) func _drawing_available() -> bool: return ( _session != null and _session.is_gameplay_session_active() and ( _session.is_host() or _session.supports_server_capability( SurfaceDrawingProtocol.CAPABILITY ) ) ) func _update_aim() -> void: _aim_hit = _raycast_from_pointer() var previous_canvas_id: String = _selected_canvas_id var found_canvas_id: String = "" var nearest_plane_distance: float = INF if not _aim_hit.is_empty(): var hit_position: Vector3 = _aim_hit["position"] for canvas_id: String in _canvas_nodes: var canvas: SurfaceDrawingCanvas = _canvas_nodes[canvas_id] if canvas.is_finalized(): continue var plane_distance: float = canvas.get_surface_plane_distance( hit_position ) if ( canvas.contains_world_point( hit_position, SurfaceDrawingCanvas.SURFACE_SAMPLE_DEPTH + 0.05, ) and plane_distance < nearest_plane_distance ): found_canvas_id = canvas_id nearest_plane_distance = plane_distance _selected_canvas_id = found_canvas_id if previous_canvas_id != _selected_canvas_id: _reset_stroke() _update_previews() func _raycast_from_pointer() -> Dictionary: if _local_player == null or not is_instance_valid(_local_player): return {} var camera: Camera3D = _local_player.get_active_gameplay_camera() if camera == null or not camera.current: return {} var from: Vector3 = camera.project_ray_origin(_pointer_screen_position) var direction: Vector3 = camera.project_ray_normal( _pointer_screen_position ).normalized() var to: Vector3 = from + direction * MAX_DRAW_DISTANCE var query := PhysicsRayQueryParameters3D.create( from, to, SOLID_SURFACE_MASK, ) query.collide_with_areas = false query.collide_with_bodies = true query.exclude = [_local_player.get_rid()] var hit: Dictionary = camera.get_world_3d().direct_space_state.intersect_ray(query) if hit.is_empty() or not _is_static_surface(hit.get("collider")): return {} return hit func _update_previews() -> void: if _placing_grid: _update_placement_preview() _hide_brush_preview() return if _placement_preview != null: _placement_preview.hide() _update_brush_preview() func _update_placement_preview() -> void: if _placement_preview == null or _aim_hit.is_empty(): if _placement_preview != null: _placement_preview.hide() return var placement: Dictionary = _resolved_placement() var normal: Vector3 = placement["normal"] var hit_position: Vector3 = placement["origin"] var tangent: Vector3 = placement["tangent"] var bitangent: Vector3 = normal.cross(tangent).normalized() _placement_preview.global_transform = Transform3D( Basis(tangent, bitangent, normal), hit_position + normal * GRID_PREVIEW_SURFACE_OFFSET, ) if _placement_preview_material != null: _placement_preview_material.albedo_color = ( Color(0.34, 1.0, 0.72, 0.88) if bool(placement["snapped"]) else Color(0.46, 0.91, 0.95, 0.72) ) _placement_preview.show() func _update_brush_preview() -> void: if ( _brush_preview == null or _brush_preview_multimesh == null or _aim_hit.is_empty() or _selected_canvas_id.is_empty() ): _hide_brush_preview() return var canvas: SurfaceDrawingCanvas = _canvas_nodes.get( _selected_canvas_id ) if canvas == null: _hide_brush_preview() return var center: Vector2i = canvas.cell_at_world_point(_aim_hit["position"]) var cells: Array[Vector2i] = _brush_cells(canvas, center) if cells.is_empty(): _hide_brush_preview() return var root_inverse: Transform3D = _drawing_root.global_transform.affine_inverse() for index: int in range(cells.size()): var cell: Vector2i = cells[index] var world_transform: Transform3D = canvas.get_cell_surface_transform( cell.x, cell.y, 0.94 ) _brush_preview_multimesh.set_instance_transform( index, root_inverse * world_transform ) _brush_preview_multimesh.visible_instance_count = cells.size() _brush_preview.show() func _hide_brush_preview() -> void: if _brush_preview_multimesh != null: _brush_preview_multimesh.visible_instance_count = 0 if _brush_preview != null: _brush_preview.hide() func _hide_previews() -> void: _hide_brush_preview() if _placement_preview != null: _placement_preview.hide() func _resolved_placement() -> Dictionary: var normal: Vector3 = _normalized_or( _aim_hit.get("normal", Vector3.UP), Vector3.UP ) var states: Array[Dictionary] = [] for state: Dictionary in _canvas_states.values(): states.append(state) return SurfaceDrawingPlacement.resolve( _aim_hit.get("position", Vector3.ZERO), normal, _surface_tangent(normal), states, _grid_size, ) func _request_canvas_at_aim() -> void: if _aim_hit.is_empty(): _emit_hud_state("aim at a solid surface before placing a grid") return var placement: Dictionary = _resolved_placement() var normal: Vector3 = placement["normal"] var origin: Vector3 = placement["origin"] if not _selected_canvas_id.is_empty(): var existing_state: Dictionary = _canvas_states.get( _selected_canvas_id, {} ) if not existing_state.is_empty(): if not bool(existing_state.get("guide_visible", true)): if request_guide_visibility(_selected_canvas_id, true): if not _session.is_host(): _emit_hud_state("restoring grid guide...") else: _emit_hud_state("a shared grid already covers this area") return if _overlaps_existing_canvas(origin, normal, _grid_size): _emit_hud_state("a shared grid already covers this area") return if request_canvas_at_surface(origin, normal, placement["tangent"]): _emit_hud_state( "placing snapped shared grid..." if bool(placement["snapped"]) else "placing shared grid..." ) @rpc( "any_peer", "call_remote", "reliable", SurfaceDrawingProtocol.RELIABLE_CHANNEL, ) func submit_canvas_request(data: Dictionary) -> void: var sender_id: int = multiplayer.get_remote_sender_id() if _session.is_host() and _session.is_authenticated_peer(sender_id): _handle_canvas_request(sender_id, data) func _handle_canvas_request(peer_id: int, data: Dictionary) -> void: var requested_size: int = int(data.get("width", 0)) if ( not _session.is_host() or not SurfaceDrawingProtocol.validate_canvas_request(data) or str(data["session_id"]) != _session.get_session_id() or not _accept_request(peer_id, str(data["request_id"])) or not _peer_can_place_grid(peer_id, requested_size) or _canvas_states.size() >= SurfaceDrawingProtocol.MAX_CANVASES or _active_canvas_count() >= SurfaceDrawingProtocol.MAX_ACTIVE_CANVASES or _allocated_grid_cells() + requested_size * requested_size > SurfaceDrawingProtocol.MAX_SESSION_GRID_CELLS ): return var avatar: Player = _spawn_service.get_avatar(peer_id) var requested_origin: Vector3 = SurfaceDrawingProtocol.array_to_vector( data["origin"] ) if ( avatar == null or avatar.global_position.distance_to(requested_origin) > MAX_DRAW_DISTANCE + 2.0 ): return var requested_normal: Vector3 = SurfaceDrawingProtocol.array_to_vector( data["normal"] ).normalized() var surface_hit: Dictionary = _validate_surface( requested_origin, requested_normal ) if surface_hit.is_empty(): return var normal: Vector3 = _normalized_or( surface_hit.get("normal", requested_normal), requested_normal ) if _overlaps_existing_canvas( surface_hit["position"], normal, requested_size ): return var tangent: Vector3 = SurfaceDrawingProtocol.array_to_vector( data["tangent"] ) tangent = (tangent - normal * tangent.dot(normal)).normalized() if tangent.is_zero_approx(): tangent = _surface_tangent(normal) _canvas_sequence += 1 var canvas_id: String = "%s-%d" % [ _session.get_session_id().left(16), _canvas_sequence, ] var record: PeerRegistry.PeerRecord = _session.get_peer_record(peer_id) if record == null or not record.identity_authenticated: return var state: Dictionary = { "session_id": _session.get_session_id(), "canvas_id": canvas_id, "origin": SurfaceDrawingProtocol.vector_to_array( surface_hit["position"] ), "normal": SurfaceDrawingProtocol.vector_to_array(normal), "tangent": SurfaceDrawingProtocol.vector_to_array(tangent), "width": requested_size, "height": requested_size, "cell_size": SurfaceDrawingProtocol.CELL_SIZE, "revision": 0, "guide_visible": true, "finalized": false, "layer": _canvas_sequence, "creator_fingerprint": record.identity_fingerprint, "cells": [], } _apply_canvas_state(state) _broadcast_canvas_state(state) _emit_hud_state("shared grid placed • everyone can draw here") @rpc( "any_peer", "call_remote", "reliable", SurfaceDrawingProtocol.RELIABLE_CHANNEL, ) func submit_guide_request(data: Dictionary) -> void: var sender_id: int = multiplayer.get_remote_sender_id() if _session.is_host() and _session.is_authenticated_peer(sender_id): _handle_guide_request(sender_id, data) func _handle_guide_request(peer_id: int, data: Dictionary) -> void: if ( not _session.is_host() or not SurfaceDrawingProtocol.validate_guide_request(data) or str(data["session_id"]) != _session.get_session_id() or not _accept_request(peer_id, str(data["request_id"])) or not _peer_owns_art_kit(peer_id) ): return var canvas_id: String = str(data["canvas_id"]) var state: Dictionary = _canvas_states.get(canvas_id, {}) var avatar: Player = _spawn_service.get_avatar(peer_id) if state.is_empty() or avatar == null: return var origin: Vector3 = SurfaceDrawingProtocol.array_to_vector(state["origin"]) if avatar.global_position.distance_to(origin) > MAX_DRAW_DISTANCE + 2.0: return var was_finalized: bool = bool(state.get("finalized", false)) var finalized: bool = bool(data["finalized"]) if was_finalized: return var guide_visible: bool = bool(data["guide_visible"]) and not finalized if ( bool(state.get("guide_visible", true)) == guide_visible and was_finalized == finalized ): return state["revision"] = int(state["revision"]) + 1 state["guide_visible"] = guide_visible state["finalized"] = finalized _canvas_states[canvas_id] = state var update: Dictionary = { "session_id": _session.get_session_id(), "canvas_id": canvas_id, "revision": int(state["revision"]), "guide_visible": guide_visible, "finalized": finalized, } var canvas: SurfaceDrawingCanvas = _canvas_nodes.get(canvas_id) if canvas != null: canvas.apply_guide_update(update) _broadcast_guide_update(update) _emit_hud_state( "grid finalized • artwork remains editable through new grids" if finalized else "grid guide restored" if guide_visible else "grid guide hidden" ) _emit_artwork_changed() func _submit_current_brush(erasing: bool, force: bool = false) -> void: if _selected_canvas_id.is_empty() or _aim_hit.is_empty(): return var canvas: SurfaceDrawingCanvas = _canvas_nodes.get( _selected_canvas_id ) if canvas == null: return var center: Vector2i = canvas.cell_at_world_point(_aim_hit["position"]) if center.x < 0: return var stroke_key := Vector3i( _selected_canvas_id.hash(), center.x, center.y ) var now: int = Time.get_ticks_msec() if ( not force and ( stroke_key == _last_edited_cell or now - _last_edit_msec < EDIT_INTERVAL_MSEC ) ): return var edits: Array[Dictionary] = [] for cell: Vector2i in _brush_cells(canvas, center): edits.append({ "x": cell.x, "y": cell.y, "color_id": "" if erasing else str(_current_color_id()), }) if edits.is_empty(): return _last_edited_cell = stroke_key _last_edit_msec = now if _active_stroke_id.is_empty(): _begin_stroke() request_cell_edits(_selected_canvas_id, edits, _active_stroke_id) func _brush_cells( canvas: SurfaceDrawingCanvas, center: Vector2i, ) -> Array[Vector2i]: var result: Array[Vector2i] = [] if canvas == null or center.x < 0 or center.y < 0: return result var start_offset: int = -floori(float(_brush_size - 1) * 0.5) for y_offset: int in range(start_offset, start_offset + _brush_size): for x_offset: int in range(start_offset, start_offset + _brush_size): var cell := Vector2i(center.x + x_offset, center.y + y_offset) if ( cell.x < 0 or cell.x >= canvas.grid_width or cell.y < 0 or cell.y >= canvas.grid_height ): continue result.append(cell) return result @rpc( "any_peer", "call_remote", "reliable", SurfaceDrawingProtocol.RELIABLE_CHANNEL, ) func submit_edit_request(data: Dictionary) -> void: var sender_id: int = multiplayer.get_remote_sender_id() if _session.is_host() and _session.is_authenticated_peer(sender_id): _handle_edit_request(sender_id, data) func _handle_edit_request(peer_id: int, data: Dictionary) -> void: if ( not _session.is_host() or not SurfaceDrawingProtocol.validate_edit_request(data) or str(data["session_id"]) != _session.get_session_id() or not _accept_request(peer_id, str(data["request_id"])) or not _peer_can_edit(peer_id, data) ): return var canvas_id: String = str(data["canvas_id"]) var state: Dictionary = _canvas_states.get(canvas_id, {}) var avatar: Player = _spawn_service.get_avatar(peer_id) var record: PeerRegistry.PeerRecord = _session.get_peer_record(peer_id) if ( state.is_empty() or bool(state.get("finalized", false)) or avatar == null or record == null ): return var stroke_id: String = str(data["stroke_id"]) var grid_width: int = int(state["width"]) var grid_height: int = int(state["height"]) var mutations: Dictionary[String, Dictionary] = {} for edit_value: Variant in data["edits"]: var edit: Dictionary = edit_value var x: int = int(edit["x"]) var y: int = int(edit["y"]) if x >= grid_width or y >= grid_height: continue var cell_position: Vector3 = _state_cell_position( state, x, y ) if ( avatar.global_position.distance_to(cell_position) > MAX_DRAW_DISTANCE + 2.0 or _validate_surface( cell_position, SurfaceDrawingProtocol.array_to_vector(state["normal"]), ).is_empty() ): continue var color_id := StringName(str(edit["color_id"])) var authoritative: Dictionary = { "x": x, "y": y, "color_id": str(color_id), "author_fingerprint": ( record.identity_fingerprint if not color_id.is_empty() else "" ), } _queue_cell_mutation( mutations, canvas_id, authoritative, peer_id, stroke_id, ) _queue_overlapping_finished_mutations( mutations, canvas_id, cell_position, peer_id, stroke_id, ) if mutations.is_empty(): return _publish_cell_mutations(mutations) func _queue_cell_mutation( mutations: Dictionary[String, Dictionary], canvas_id: String, edit: Dictionary, peer_id: int, stroke_id: String, ) -> void: var state: Dictionary = _canvas_states.get(canvas_id, {}) if state.is_empty(): return var x: int = int(edit["x"]) var y: int = int(edit["y"]) var cell_key: int = _cell_key_for_state(state, x, y) var canvas_mutations: Dictionary = mutations.get( canvas_id, {} ) var current: Dictionary = ( _cell_value_from_edit(canvas_mutations[cell_key]) if canvas_mutations.has(cell_key) else _state_cell(state, x, y) ) var next: Dictionary = _cell_value_from_edit(edit) if _same_cell_value(current, next): return _record_stroke_change( peer_id, stroke_id, canvas_id, x, y, current, next ) canvas_mutations[cell_key] = edit.duplicate(true) mutations[canvas_id] = canvas_mutations _cell_last_stroke[_mutation_key(canvas_id, x, y)] = stroke_id func _queue_overlapping_finished_mutations( mutations: Dictionary[String, Dictionary], target_canvas_id: String, world_position: Vector3, peer_id: int, stroke_id: String, ) -> void: var target_state: Dictionary = _canvas_states.get(target_canvas_id, {}) if target_state.is_empty(): return var target_normal: Vector3 = SurfaceDrawingProtocol.array_to_vector( target_state["normal"] ).normalized() for canvas_id: String in _canvas_states: if canvas_id == target_canvas_id: continue var state: Dictionary = _canvas_states[canvas_id] if not bool(state.get("finalized", false)): continue var normal: Vector3 = SurfaceDrawingProtocol.array_to_vector( state["normal"] ).normalized() if normal.dot(target_normal) < 0.9: continue var canvas: SurfaceDrawingCanvas = _canvas_nodes.get(canvas_id) if ( canvas == null or not canvas.contains_world_point( world_position, SurfaceDrawingCanvas.SURFACE_SAMPLE_DEPTH + 0.05, ) ): continue var center_cell: Vector2i = canvas.cell_at_world_point(world_position) if center_cell.x < 0: continue for y_offset: int in range(-1, 2): for x_offset: int in range(-1, 2): var cell := Vector2i( center_cell.x + x_offset, center_cell.y + y_offset, ) if ( cell.x < 0 or cell.x >= canvas.grid_width or cell.y < 0 or cell.y >= canvas.grid_height ): continue var relative: Vector3 = ( canvas.get_cell_world_position(cell.x, cell.y) - world_position ) if ( absf(relative.dot(canvas.get_surface_tangent())) >= SurfaceDrawingProtocol.CELL_SIZE or absf(relative.dot(canvas.get_surface_bitangent())) >= SurfaceDrawingProtocol.CELL_SIZE or _state_cell(state, cell.x, cell.y).is_empty() ): continue _queue_cell_mutation( mutations, canvas_id, { "x": cell.x, "y": cell.y, "color_id": "", "author_fingerprint": "", }, peer_id, stroke_id, ) func _record_stroke_change( peer_id: int, stroke_id: String, canvas_id: String, x: int, y: int, before: Dictionary, after: Dictionary, ) -> void: var history: Dictionary = _stroke_history_by_peer.get(peer_id, {}) if str(history.get("stroke_id", "")) != stroke_id: history = {"stroke_id": stroke_id, "changes": {}} var changes: Dictionary = history["changes"] var key: String = _mutation_key(canvas_id, x, y) var change: Dictionary = changes.get(key, {}) if change.is_empty(): change = { "canvas_id": canvas_id, "x": x, "y": y, "before": before.duplicate(true), "before_stroke": str(_cell_last_stroke.get(key, "")), } change["after"] = after.duplicate(true) changes[key] = change history["changes"] = changes _stroke_history_by_peer[peer_id] = history func _publish_cell_mutations( mutations: Dictionary[String, Dictionary], ) -> void: var canvas_ids: Array[String] = mutations.keys() canvas_ids.sort() for canvas_id: String in canvas_ids: var state: Dictionary = _canvas_states.get(canvas_id, {}) if state.is_empty(): continue var cells: Dictionary[int, Dictionary] = _cells_by_key( state["cells"], int(state["width"]) ) var canvas_mutations: Dictionary = mutations[canvas_id] var cell_keys: Array[int] = [] for cell_key_value: Variant in canvas_mutations.keys(): cell_keys.append(int(cell_key_value)) cell_keys.sort() var edits: Array[Dictionary] = [] for cell_key: int in cell_keys: var edit: Dictionary = canvas_mutations[cell_key] if str(edit["color_id"]).is_empty(): cells.erase(cell_key) else: cells[cell_key] = edit.duplicate(true) edits.append(edit.duplicate(true)) if edits.is_empty(): continue state["revision"] = int(state["revision"]) + 1 state["cells"] = _sorted_cells(cells) _canvas_states[canvas_id] = state var update: Dictionary = { "session_id": _session.get_session_id(), "canvas_id": canvas_id, "revision": int(state["revision"]), "edits": edits, } var local_canvas: SurfaceDrawingCanvas = _canvas_nodes.get(canvas_id) if local_canvas != null: local_canvas.apply_update(update) _broadcast_canvas_update(update) _emit_artwork_changed() @rpc( "any_peer", "call_remote", "reliable", SurfaceDrawingProtocol.RELIABLE_CHANNEL, ) func submit_undo_request(data: Dictionary) -> void: var sender_id: int = multiplayer.get_remote_sender_id() if _session.is_host() and _session.is_authenticated_peer(sender_id): _handle_undo_request(sender_id, data) func _handle_undo_request(peer_id: int, data: Dictionary) -> void: if ( not _session.is_host() or not SurfaceDrawingProtocol.validate_undo_request(data) or str(data["session_id"]) != _session.get_session_id() or not _accept_request(peer_id, str(data["request_id"])) or not _peer_owns_art_kit(peer_id) ): return var stroke_id: String = str(data["stroke_id"]) var history: Dictionary = _stroke_history_by_peer.get(peer_id, {}) var mutations: Dictionary[String, Dictionary] = {} var restored_count: int = 0 if str(history.get("stroke_id", "")) == stroke_id: var changes: Dictionary = history.get("changes", {}) for mutation_key: String in changes: if str(_cell_last_stroke.get(mutation_key, "")) != stroke_id: continue var change: Dictionary = changes[mutation_key] var canvas_id: String = str(change["canvas_id"]) var state: Dictionary = _canvas_states.get(canvas_id, {}) if state.is_empty(): continue var x: int = int(change["x"]) var y: int = int(change["y"]) var current: Dictionary = _state_cell(state, x, y) if not _same_cell_value(current, change.get("after", {})): continue var before: Dictionary = change.get("before", {}) var restored: Dictionary = _authoritative_edit_from_value( x, y, before ) var canvas_mutations: Dictionary = mutations.get( canvas_id, {} ) canvas_mutations[_cell_key_for_state(state, x, y)] = restored mutations[canvas_id] = canvas_mutations var previous_stroke: String = str( change.get("before_stroke", "") ) if previous_stroke.is_empty(): _cell_last_stroke.erase(mutation_key) else: _cell_last_stroke[mutation_key] = previous_stroke restored_count += 1 _stroke_history_by_peer.erase(peer_id) if not mutations.is_empty(): _publish_cell_mutations(mutations) _send_undo_result(peer_id, restored_count) func _send_undo_result(peer_id: int, restored_count: int) -> void: if peer_id == _session.get_local_peer_id(): _receive_undo_result(restored_count) else: receive_undo_result.rpc_id(peer_id, restored_count) @rpc( "authority", "call_remote", "reliable", SurfaceDrawingProtocol.RELIABLE_CHANNEL, ) func receive_undo_result(restored_count: int) -> void: _receive_undo_result(restored_count) func _receive_undo_result(restored_count: int) -> void: _emit_hud_state( "last stroke undone" if restored_count > 0 else "last stroke changed elsewhere and could not be undone" ) @rpc( "authority", "call_remote", "reliable", SurfaceDrawingProtocol.RELIABLE_CHANNEL, ) func receive_canvas_state(data: Dictionary) -> void: _apply_canvas_state(data) @rpc( "authority", "call_remote", "reliable", SurfaceDrawingProtocol.RELIABLE_CHANNEL, ) func receive_canvas_update(data: Dictionary) -> void: _apply_canvas_update(data) @rpc( "authority", "call_remote", "reliable", SurfaceDrawingProtocol.RELIABLE_CHANNEL, ) func receive_guide_update(data: Dictionary) -> void: _apply_guide_update(data) @rpc( "authority", "call_remote", "reliable", SurfaceDrawingProtocol.RELIABLE_CHANNEL, ) func receive_session_artwork_reset(session_id: String) -> void: if session_id == _session.get_session_id(): _clear_session_artwork_state() @rpc( "any_peer", "call_remote", "reliable", SurfaceDrawingProtocol.RELIABLE_CHANNEL, ) func request_canvas_snapshot(request_id: String) -> void: var sender_id: int = multiplayer.get_remote_sender_id() if ( not _session.is_host() or not _session.is_authenticated_peer(sender_id) or request_id.is_empty() or request_id.length() > SurfaceDrawingProtocol.MAX_REQUEST_ID_LENGTH or not _accept_request(sender_id, request_id) or not _session.peer_supports_capability( sender_id, SurfaceDrawingProtocol.CAPABILITY ) ): return for state: Dictionary in _canvas_states.values(): receive_canvas_state.rpc_id(sender_id, state) func _apply_canvas_state(data: Dictionary) -> void: if ( not SurfaceDrawingProtocol.validate_canvas_state(data) or str(data["session_id"]) != _session.get_session_id() ): return var canvas_id: String = str(data["canvas_id"]) var previous_state: Dictionary = _canvas_states.get(canvas_id, {}) if ( not previous_state.is_empty() and int(data["revision"]) <= int(previous_state["revision"]) ): return _canvas_states[canvas_id] = data.duplicate(true) var previous_canvas: SurfaceDrawingCanvas = _canvas_nodes.get(canvas_id) if previous_canvas != null: previous_canvas.queue_free() var canvas := SurfaceDrawingCanvas.new() canvas.name = "Drawing_%s" % canvas_id _drawing_root.add_child(canvas) if not canvas.setup(data, _relationships, SOLID_SURFACE_MASK): canvas.queue_free() return _canvas_nodes[canvas_id] = canvas _refresh_stencil_visibility() _emit_artwork_changed() if ( _active and str(data.get("creator_fingerprint", "")) == _session.get_local_identity_fingerprint() ): _emit_hud_state("shared grid placed • move and click to place another") func _apply_canvas_update(data: Dictionary) -> void: if ( not SurfaceDrawingProtocol.validate_canvas_update(data) or str(data["session_id"]) != _session.get_session_id() ): return var canvas_id: String = str(data["canvas_id"]) var state: Dictionary = _canvas_states.get(canvas_id, {}) var canvas: SurfaceDrawingCanvas = _canvas_nodes.get(canvas_id) if state.is_empty() or canvas == null or int(data["revision"]) <= int(state["revision"]): return var cells: Dictionary[int, Dictionary] = _cells_by_key( state["cells"], int(state["width"]) ) var grid_width: int = int(state["width"]) var grid_height: int = int(state["height"]) for edit_value: Variant in data["edits"]: var edit: Dictionary = edit_value var x: int = int(edit["x"]) var y: int = int(edit["y"]) if x < 0 or x >= grid_width or y < 0 or y >= grid_height: return var key: int = y * grid_width + x if str(edit["color_id"]).is_empty(): cells.erase(key) else: cells[key] = edit.duplicate(true) state["revision"] = int(data["revision"]) state["cells"] = _sorted_cells(cells) _canvas_states[canvas_id] = state canvas.apply_update(data) _emit_artwork_changed() func _apply_guide_update(data: Dictionary) -> void: if ( not SurfaceDrawingProtocol.validate_guide_update(data) or str(data["session_id"]) != _session.get_session_id() ): return var canvas_id: String = str(data["canvas_id"]) var state: Dictionary = _canvas_states.get(canvas_id, {}) var canvas: SurfaceDrawingCanvas = _canvas_nodes.get(canvas_id) if ( state.is_empty() or canvas == null or int(data["revision"]) <= int(state["revision"]) ): return state["revision"] = int(data["revision"]) state["finalized"] = bool(data["finalized"]) state["guide_visible"] = ( bool(data["guide_visible"]) and not bool(data["finalized"]) ) _canvas_states[canvas_id] = state canvas.apply_guide_update(data) if _active: _emit_hud_state( "grid finalized • new grids may overlap its artwork" if bool(data["finalized"]) else "grid guide restored" if bool(data["guide_visible"]) else "grid guide hidden • pixels now meet edge-to-edge" ) _emit_artwork_changed() func _broadcast_canvas_state(data: Dictionary) -> void: for peer_id: int in _session.get_authenticated_peer_ids(): if peer_id == _session.get_local_peer_id(): continue if _session.peer_supports_capability( peer_id, SurfaceDrawingProtocol.CAPABILITY ): receive_canvas_state.rpc_id(peer_id, data) func _broadcast_canvas_update(data: Dictionary) -> void: for peer_id: int in _session.get_authenticated_peer_ids(): if peer_id == _session.get_local_peer_id(): continue if _session.peer_supports_capability( peer_id, SurfaceDrawingProtocol.CAPABILITY ): receive_canvas_update.rpc_id(peer_id, data) func _broadcast_guide_update(data: Dictionary) -> void: for peer_id: int in _session.get_authenticated_peer_ids(): if peer_id == _session.get_local_peer_id(): continue if _session.peer_supports_capability( peer_id, SurfaceDrawingProtocol.CAPABILITY ): receive_guide_update.rpc_id(peer_id, data) func _validate_surface(origin: Vector3, normal: Vector3) -> Dictionary: if _drawing_root == null or normal.is_zero_approx(): return {} var world: World3D = _drawing_root.get_world_3d() if world == null: return {} var direction: Vector3 = normal.normalized() var query := PhysicsRayQueryParameters3D.create( origin + direction * SURFACE_VALIDATION_DEPTH, origin - direction * SURFACE_VALIDATION_DEPTH, SOLID_SURFACE_MASK, ) query.collide_with_areas = false query.collide_with_bodies = true var hit: Dictionary = world.direct_space_state.intersect_ray(query) if hit.is_empty() or not _is_static_surface(hit.get("collider")): return {} var hit_normal: Vector3 = _normalized_or( hit.get("normal", direction), direction ) if absf(hit_normal.dot(direction)) < MIN_SURFACE_NORMAL_DOT: return {} return hit func _is_static_surface(value: Variant) -> bool: return value is StaticBody3D func _surface_tangent(normal: Vector3) -> Vector3: var camera: Camera3D = ( _local_player.get_active_gameplay_camera() if _local_player != null else null ) var candidate: Vector3 = ( camera.global_basis.x if camera != null else Vector3.RIGHT ) candidate = (candidate - normal * candidate.dot(normal)).normalized() if candidate.is_zero_approx(): candidate = Vector3.UP.cross(normal).normalized() if candidate.is_zero_approx(): candidate = Vector3.RIGHT return candidate func _state_cell_position(state: Dictionary, x: int, y: int) -> Vector3: var origin: Vector3 = SurfaceDrawingProtocol.array_to_vector(state["origin"]) var normal: Vector3 = SurfaceDrawingProtocol.array_to_vector( state["normal"] ).normalized() var tangent: Vector3 = SurfaceDrawingProtocol.array_to_vector( state["tangent"] ).normalized() var bitangent: Vector3 = normal.cross(tangent).normalized() var size: float = float(state["cell_size"]) return ( origin + tangent * (float(x) + 0.5 - float(state["width"]) * 0.5) * size + bitangent * (float(y) + 0.5 - float(state["height"]) * 0.5) * size ) func _overlaps_existing_canvas( origin: Vector3, normal: Vector3, requested_size: int, ) -> bool: for state: Dictionary in _canvas_states.values(): if bool(state.get("finalized", false)): continue var existing_origin: Vector3 = SurfaceDrawingProtocol.array_to_vector( state["origin"] ) var existing_normal: Vector3 = SurfaceDrawingProtocol.array_to_vector( state["normal"] ).normalized() if absf(normal.dot(existing_normal)) < 0.85: continue var relative: Vector3 = origin - existing_origin if absf(relative.dot(existing_normal)) > SURFACE_VALIDATION_DEPTH: continue var tangent: Vector3 = SurfaceDrawingProtocol.array_to_vector( state["tangent"] ).normalized() var bitangent: Vector3 = existing_normal.cross(tangent).normalized() var width: float = ( (float(state["width"]) + float(requested_size)) * float(state["cell_size"]) * 0.5 ) var height: float = ( (float(state["height"]) + float(requested_size)) * float(state["cell_size"]) * 0.5 ) var clearance: float = float(state["cell_size"]) * 0.5 if ( absf(relative.dot(tangent)) < width - clearance and absf(relative.dot(bitangent)) < height - clearance ): return true return false func _active_canvas_count() -> int: var count: int = 0 for state: Dictionary in _canvas_states.values(): if not bool(state.get("finalized", false)): count += 1 return count func _allocated_grid_cells() -> int: var count: int = 0 for state: Dictionary in _canvas_states.values(): count += int(state.get("width", 0)) * int(state.get("height", 0)) return count func _create_brush_preview() -> void: if _drawing_root == null: return var quad := QuadMesh.new() quad.size = Vector2.ONE _brush_preview_material = ShaderMaterial.new() _brush_preview_material.shader = BrushHighlightShader quad.material = _brush_preview_material _brush_preview_multimesh = MultiMesh.new() _brush_preview_multimesh.transform_format = MultiMesh.TRANSFORM_3D _brush_preview_multimesh.mesh = quad _brush_preview_multimesh.instance_count = ( SurfaceDrawingProtocol.MAX_EDITS_PER_REQUEST ) _brush_preview_multimesh.visible_instance_count = 0 _brush_preview = MultiMeshInstance3D.new() _brush_preview.name = "MarkerBrushPreview" _brush_preview.multimesh = _brush_preview_multimesh _brush_preview.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF _brush_preview.hide() _drawing_root.add_child(_brush_preview) func _create_placement_preview() -> void: _rebuild_placement_preview() func _rebuild_placement_preview() -> void: if _drawing_root == null: return if _placement_preview != null: _placement_preview.queue_free() _placement_preview = null var mesh := ImmediateMesh.new() _placement_preview_material = StandardMaterial3D.new() _placement_preview_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED _placement_preview_material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA _placement_preview_material.cull_mode = BaseMaterial3D.CULL_DISABLED _placement_preview_material.albedo_color = Color(0.46, 0.91, 0.95, 0.72) mesh.surface_begin(Mesh.PRIMITIVE_LINES, _placement_preview_material) var half_width: float = ( float(_grid_size) * SurfaceDrawingProtocol.CELL_SIZE * 0.5 ) var half_height: float = ( float(_grid_size) * SurfaceDrawingProtocol.CELL_SIZE * 0.5 ) for x: int in range(_grid_size + 1): var horizontal: float = ( -half_width + float(x) * SurfaceDrawingProtocol.CELL_SIZE ) mesh.surface_add_vertex(Vector3(horizontal, -half_height, 0.0)) mesh.surface_add_vertex(Vector3(horizontal, half_height, 0.0)) for y: int in range(_grid_size + 1): var vertical: float = ( -half_height + float(y) * SurfaceDrawingProtocol.CELL_SIZE ) mesh.surface_add_vertex(Vector3(-half_width, vertical, 0.0)) mesh.surface_add_vertex(Vector3(half_width, vertical, 0.0)) mesh.surface_end() _placement_preview = MeshInstance3D.new() _placement_preview.name = "MarkerGridPlacementPreview" _placement_preview.mesh = mesh _placement_preview.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF _placement_preview.hide() _drawing_root.add_child(_placement_preview) func _set_placement_mode(enabled: bool) -> void: if not _active: return _clear_armed_guide_action(true) if _placing_grid == enabled: _emit_hud_state("") return _placing_grid = enabled _reset_stroke() _selected_canvas_id = "" _update_aim() _refresh_stencil_visibility() _emit_hud_state( ( "click place/restore • shift hide • ctrl shift finish • shift scroll zoom" if _placing_grid else "click draw • shift click erase • ctrl z undo • shift scroll zoom" ) ) func _execute_armed_guide_action() -> void: var action: int = _armed_guide_action match action: GuideAction.HIDE: _remove_selected_guide() GuideAction.RESTORE: _restore_selected_guide() GuideAction.FINALIZE: _finalize_selected_guide() # Guide actions are persistent modes. Keep the selected action armed so # multiple grids can be handled without returning to the toolbar after # every click; selecting the active toggle again explicitly exits it. _emit_hud_state(_guide_action_status(action)) func _cancel_armed_guide_action() -> void: if _armed_guide_action == GuideAction.NONE: return _clear_armed_guide_action(true) _emit_hud_state("") func _clear_armed_guide_action(restore_tool: bool) -> void: if _armed_guide_action == GuideAction.NONE: return _armed_guide_action = GuideAction.NONE if restore_tool: _placing_grid = _armed_return_placement_mode _eraser_mode = _armed_return_eraser_mode _armed_return_placement_mode = false _armed_return_eraser_mode = false _reset_stroke() _update_aim() _refresh_stencil_visibility() func _guide_action_verb(action: int) -> String: match action: GuideAction.HIDE: return "hide" GuideAction.RESTORE: return "show" GuideAction.FINALIZE: return "finish" return "use" func _guide_action_status(action: int) -> String: return "%s grid mode • click grids • toggle again to exit" % ( _guide_action_verb(action).capitalize() ) func _remove_selected_guide() -> void: if _selected_canvas_id.is_empty(): _emit_hud_state("aim at a placed grid before removing its guide") return var state: Dictionary = _canvas_states.get(_selected_canvas_id, {}) if state.is_empty(): return if not bool(state.get("guide_visible", true)): _emit_hud_state("this grid guide is hidden • click to restore") return if request_guide_visibility(_selected_canvas_id, false): if not _session.is_host(): _emit_hud_state("hiding grid guide...") func _restore_selected_guide() -> void: if _selected_canvas_id.is_empty(): _emit_hud_state("aim at a hidden grid before restoring its guide") return var state: Dictionary = _canvas_states.get(_selected_canvas_id, {}) if state.is_empty() or bool(state.get("finalized", false)): return if bool(state.get("guide_visible", true)): _emit_hud_state("this grid guide is already visible") return if request_guide_visibility(_selected_canvas_id, true): if not _session.is_host(): _emit_hud_state("restoring grid guide...") func _finalize_selected_guide() -> void: if _selected_canvas_id.is_empty(): _emit_hud_state("aim at an active grid before finishing it") return var state: Dictionary = _canvas_states.get(_selected_canvas_id, {}) if state.is_empty() or bool(state.get("finalized", false)): return if request_guide_visibility(_selected_canvas_id, false, true): if not _session.is_host(): _emit_hud_state("finishing grid • artwork remains shared...") func _clamped_pointer_position(value: Vector2) -> Vector2: var viewport_size: Vector2 = get_viewport().get_visible_rect().size return Vector2( clampf(value.x, POINTER_EDGE_MARGIN, viewport_size.x - POINTER_EDGE_MARGIN), clampf(value.y, POINTER_EDGE_MARGIN, viewport_size.y - POINTER_EDGE_MARGIN), ) func _set_pointer_from_input( event_position: Vector2, pointer_screen_position: Vector2, ) -> void: # GameUI receives input through a scaled SubViewport, so event_position is # expressed in that UI viewport's render coordinates. Camera3D ray methods # require coordinates in the camera's root viewport instead. GameUI supplies # that authoritative window position; direct callers may still use an event # position when they share this service's viewport. var resolved_position: Vector2 = pointer_screen_position if not _is_valid_pointer_screen_position(resolved_position): resolved_position = event_position _pointer_screen_position = _clamped_pointer_position(resolved_position) func _is_valid_pointer_screen_position(value: Vector2) -> bool: return value.x >= 0.0 and value.y >= 0.0 func _cycle_color(direction: int) -> void: if _color_ids.is_empty(): return _clear_armed_guide_action(true) _eraser_mode = false _color_index = posmod(_color_index + direction, _color_ids.size()) _reset_stroke() _emit_hud_state("") func _set_brush_size(value: int) -> void: var requested: int = clampi(value, 1, 4) if _art_unlocks != null and not _art_unlocks.is_brush_size_unlocked( requested ): return _brush_size = requested _reset_stroke() _emit_hud_state("") func _current_color_id() -> StringName: return ( _color_ids[_color_index] if not _color_ids.is_empty() else SurfaceDrawingPalette.DEFAULT_COLOR_ID ) func _current_color() -> Color: return SurfaceDrawingPalette.get_color(_current_color_id()) func _emit_hud_state(status: String) -> void: hud_state_changed.emit( _active, "place grid" if _placing_grid else "marker", SurfaceDrawingPalette.get_display_name(_current_color_id()), _current_color(), _brush_size, _grid_size, status, ) func _refresh_stencil_visibility() -> void: for canvas_id: String in _canvas_nodes: _canvas_nodes[canvas_id].set_stencil_visible(_active) func _begin_stroke() -> void: _active_stroke_id = _new_request_id("stroke") _last_edited_cell = Vector3i(-1, -1, -1) func _finish_stroke() -> void: _painting = false _erasing = false _active_stroke_id = "" _last_edited_cell = Vector3i(-1, -1, -1) func _reset_stroke() -> void: _finish_stroke() func _new_request_id(prefix: String) -> String: _request_sequence += 1 return "%s-%d-%d" % [prefix, Time.get_ticks_msec(), _request_sequence] func _cells_by_key( values: Array, grid_width: int, ) -> Dictionary[int, Dictionary]: var result: Dictionary[int, Dictionary] = {} for value: Variant in values: if typeof(value) != TYPE_DICTIONARY: continue var cell: Dictionary = value var key: int = ( int(cell.get("y", -1)) * grid_width + int(cell.get("x", -1)) ) result[key] = cell.duplicate(true) return result func _state_cell(state: Dictionary, x: int, y: int) -> Dictionary: var cells: Dictionary[int, Dictionary] = _cells_by_key( state.get("cells", []), int(state["width"]) ) return Dictionary( cells.get(_cell_key_for_state(state, x, y), {}) ).duplicate(true) func _cell_value_from_edit(edit: Dictionary) -> Dictionary: if edit.is_empty() or str(edit.get("color_id", "")).is_empty(): return {} return { "color_id": str(edit["color_id"]), "author_fingerprint": str(edit.get("author_fingerprint", "")), } func _authoritative_edit_from_value( x: int, y: int, value: Dictionary, ) -> Dictionary: return { "x": x, "y": y, "color_id": str(value.get("color_id", "")), "author_fingerprint": str(value.get("author_fingerprint", "")), } func _same_cell_value(first: Dictionary, second: Dictionary) -> bool: return ( str(first.get("color_id", "")) == str(second.get("color_id", "")) and str(first.get("author_fingerprint", "")) == str(second.get("author_fingerprint", "")) ) func _cell_key_for_state(state: Dictionary, x: int, y: int) -> int: return y * int(state["width"]) + x func _mutation_key(canvas_id: String, x: int, y: int) -> String: return "%s:%d:%d" % [canvas_id, x, y] func _sorted_cells(values: Dictionary[int, Dictionary]) -> Array[Dictionary]: var keys: Array[int] = values.keys() keys.sort() var result: Array[Dictionary] = [] for key: int in keys: result.append(values[key].duplicate(true)) return result func _normalized_or(value: Variant, fallback: Vector3) -> Vector3: var vector: Vector3 = value if typeof(value) == TYPE_VECTOR3 else fallback return fallback if vector.is_zero_approx() else vector.normalized() func _accept_request(peer_id: int, request_id: String) -> bool: var recent_ids: PackedStringArray = _peer_request_ids.get( peer_id, PackedStringArray() ) if request_id in recent_ids: return false var now: int = Time.get_ticks_msec() var request_times: PackedInt64Array = _peer_request_times.get( peer_id, PackedInt64Array() ) while ( not request_times.is_empty() and now - request_times[0] >= REQUEST_WINDOW_MSEC ): request_times.remove_at(0) if request_times.size() >= MAX_REQUESTS_PER_WINDOW: _peer_request_times[peer_id] = request_times return false request_times.append(now) recent_ids.append(request_id) while recent_ids.size() > MAX_RECENT_REQUEST_IDS: recent_ids.remove_at(0) _peer_request_times[peer_id] = request_times _peer_request_ids[peer_id] = recent_ids return true func _on_peer_removed(peer_id: int) -> void: _peer_request_times.erase(peer_id) _peer_request_ids.erase(peer_id) _stroke_history_by_peer.erase(peer_id) _peer_art_entitlements.erase(peer_id) func _on_local_art_entitlement_changed() -> void: if _active and not can_activate(): deactivate() _publish_local_entitlement() func _on_local_art_unlocks_changed(_unlock_mask: int) -> void: _refresh_local_unlocks() _rebuild_placement_preview() _publish_local_entitlement() _emit_hud_state("") func _on_relationship_changed(_fingerprint: String) -> void: for canvas: SurfaceDrawingCanvas in _canvas_nodes.values(): canvas.refresh_relationship_visibility() func _on_session_state_changed(state: NetworkSession.State) -> void: if state == NetworkSession.State.JOINED_CLIENT: _publish_local_entitlement() request_canvas_snapshot.rpc_id(1, _new_request_id("snapshot")) return if state in [ NetworkSession.State.PRIVATE_HOST, NetworkSession.State.OPEN_HOST, ]: _publish_local_entitlement() return if state not in [ NetworkSession.State.INACTIVE, NetworkSession.State.DISCONNECTING, NetworkSession.State.CONNECTION_FAILED, NetworkSession.State.SERVER_LOST, ]: return deactivate() _clear_session_artwork_state() _canvas_sequence = 0 _peer_request_times.clear() _peer_request_ids.clear() _peer_art_entitlements.clear() func _clear_session_artwork_state() -> void: _canvas_states.clear() for canvas: SurfaceDrawingCanvas in _canvas_nodes.values(): canvas.queue_free() _canvas_nodes.clear() _canvas_sequence = 0 _selected_canvas_id = "" _stroke_history_by_peer.clear() _cell_last_stroke.clear() _last_local_stroke_id = "" _hide_previews() _emit_artwork_changed() func _emit_artwork_changed() -> void: session_artwork_changed.emit( get_canvas_count(), get_painted_cell_count() )