//! WebSocket message protocol for channel presence. //! //! Shared message types used by both server and WASM client. use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::models::{AvatarRenderData, ChannelMemberInfo, ChannelMemberWithAvatar, ForcedAvatarReason, LooseProp}; /// Default function for serde that returns true (for is_same_scene field). /// Must be pub for serde derive macro to access via full path. pub fn default_is_same_scene() -> bool { true } /// WebSocket configuration sent to client on connect. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WsConfig { /// Interval for client to send ping to keep connection alive (seconds). pub ping_interval_secs: u64, } /// WebSocket close codes (custom range: 4000-4999). pub mod close_codes { /// Scene change (user navigating to different scene). pub const SCENE_CHANGE: u16 = 4000; /// Server timeout (no message received within timeout period). pub const SERVER_TIMEOUT: u16 = 4001; /// User explicitly logged out. pub const LOGOUT: u16 = 4003; } /// Reason for member disconnect. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum DisconnectReason { /// Graceful disconnect (browser close, normal WebSocket close). Graceful, /// Scene navigation (custom close code 4000). SceneChange, /// Timeout (connection lost, no ping response). Timeout, } /// Client-to-server WebSocket messages. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ClientMessage { /// Update position in the channel. UpdatePosition { /// X coordinate in scene space. x: f64, /// Y coordinate in scene space. y: f64, }, /// Update emotion by name. UpdateEmotion { /// Emotion name (e.g., "happy", "sad", "neutral"). emotion: String, }, /// Ping to keep connection alive. Ping, /// Send a chat message to the channel or directly to a user. SendChatMessage { /// Message content (max 500 chars). content: String, /// Target display name for direct whisper. None = broadcast to scene. #[serde(default)] target_display_name: Option, }, /// Drop a prop from inventory to the canvas. DropProp { /// Inventory item ID to drop. inventory_item_id: Uuid, }, /// Pick up a loose prop from the canvas. PickUpProp { /// Loose prop ID to pick up. loose_prop_id: Uuid, }, /// Request to broadcast avatar appearance to other users. SyncAvatar, /// Request to teleport to a different scene. Teleport { /// Scene ID to teleport to. scene_id: Uuid, }, /// Moderator command (only processed if sender is a moderator). ModCommand { /// Subcommand name ("summon", "avatar", "teleport", "ban", etc.). subcommand: String, /// Arguments for the subcommand. args: Vec, }, /// Request to refresh identity after registration (guest → user conversion). /// Server will fetch updated user data and broadcast to all members. RefreshIdentity, } /// Server-to-client WebSocket messages. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ServerMessage { /// Welcome message with initial state after connection. Welcome { /// This user's member info. member: ChannelMemberInfo, /// All current members with avatars. members: Vec, /// WebSocket configuration for the client. config: WsConfig, }, /// A member joined the channel. MemberJoined { /// The member that joined. member: ChannelMemberWithAvatar, }, /// A member left the channel. MemberLeft { /// User ID of the member who left. user_id: Uuid, /// Reason for disconnect. reason: DisconnectReason, }, /// A member updated their position. PositionUpdated { /// User ID of the member. user_id: Uuid, /// New X coordinate. x: f64, /// New Y coordinate. y: f64, }, /// A member changed their emotion. EmotionUpdated { /// User ID of the member. user_id: Uuid, /// Emotion name (e.g., "happy", "sad", "neutral"). emotion: String, /// Asset paths for all 9 positions of the new emotion layer. emotion_layer: [Option; 9], }, /// Pong response to client ping. Pong, /// Error message. Error { /// Error code. code: String, /// Error message. message: String, }, /// A chat message was received. ChatMessageReceived { /// Unique message ID. message_id: Uuid, /// User ID of sender. user_id: Uuid, /// Display name of sender. display_name: String, /// Message content. content: String, /// Emotion name for bubble styling (e.g., "happy", "sad", "neutral"). emotion: String, /// Sender's X position at time of message. x: f64, /// Sender's Y position at time of message. y: f64, /// Server timestamp (milliseconds since epoch). timestamp: i64, /// Whether this is a whisper (direct message). /// Default: false (broadcast message). #[serde(default)] is_whisper: bool, /// Whether sender is in the same scene as recipient. /// For whispers: true = same scene (show as bubble), false = cross-scene (show as notification). /// For broadcasts: always true. /// Default: true (same scene). #[serde(default = "self::default_is_same_scene")] is_same_scene: bool, }, /// Initial list of loose props when joining channel. LoosePropsSync { /// All current loose props in the channel. props: Vec, }, /// A prop was dropped on the canvas. PropDropped { /// The dropped prop. prop: LooseProp, }, /// A prop was picked up from the canvas. PropPickedUp { /// ID of the prop that was picked up. prop_id: Uuid, /// User ID who picked it up. picked_up_by_user_id: Uuid, }, /// A prop expired and was removed. PropExpired { /// ID of the expired prop. prop_id: Uuid, }, /// A member updated their avatar appearance. AvatarUpdated { /// User ID of the member. user_id: Uuid, /// Updated avatar render data. avatar: AvatarRenderData, }, /// Teleport approved - client should disconnect and reconnect to new scene. TeleportApproved { /// Scene ID to navigate to. scene_id: Uuid, /// Scene slug for URL. scene_slug: String, }, /// User has been summoned by a moderator - triggers teleport. Summoned { /// Scene ID to teleport to. scene_id: Uuid, /// Scene slug for URL. scene_slug: String, /// Display name of the moderator who summoned. summoned_by: String, }, /// Result of a moderator command. ModCommandResult { /// Whether the command succeeded. success: bool, /// Human-readable result message. message: String, }, /// A member's identity was updated (e.g., guest registered as user). MemberIdentityUpdated { /// User ID of the member. user_id: Uuid, /// New display name. display_name: String, /// Whether the member is still a guest. is_guest: bool, }, /// A user's avatar was forcibly changed (by moderator or scene entry). AvatarForced { /// User ID whose avatar was forced. user_id: Uuid, /// The forced avatar render data. avatar: AvatarRenderData, /// Why the avatar was forced. reason: ForcedAvatarReason, /// Display name of who forced the avatar (if mod command). forced_by: Option, }, /// A user's forced avatar was cleared (returned to their chosen avatar). AvatarCleared { /// User ID whose forced avatar was cleared. user_id: Uuid, /// The user's original avatar render data (restored). avatar: AvatarRenderData, /// Display name of who cleared the forced avatar (if mod command). cleared_by: Option, }, }