diff --git a/src-tauri/src/app.rs b/src-tauri/src/app.rs index 7a31827..4bc0c31 100644 --- a/src-tauri/src/app.rs +++ b/src-tauri/src/app.rs @@ -29,6 +29,12 @@ async fn construct_app() { }); let init_ws = tauri::async_runtime::spawn(async { + // init_ws_client calls get_access_token().await. + // During a fresh login, this token might be in the process of being saved/refreshed + // or the client initialization might be racing. + // However, construct_app is called after auth success, so tokens should be there. + // The issue might be that init_ws_client is idempotent but if called twice or early... + // Actually, init_ws_client handles creating the socket. crate::services::ws::init_ws_client().await; }); diff --git a/src-tauri/src/remotes/friends.rs b/src-tauri/src/remotes/friends.rs index 92371c1..509f5bd 100644 --- a/src-tauri/src/remotes/friends.rs +++ b/src-tauri/src/remotes/friends.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use ts_rs::TS; -use crate::{lock_r, services::auth::with_auth, state::FDOLL}; +use crate::{lock_r, remotes::dolls::DollDto, services::auth::with_auth, state::FDOLL}; #[derive(Error, Debug)] pub enum RemoteError { @@ -22,7 +22,7 @@ pub struct UserBasicDto { pub id: String, pub name: String, pub username: Option, - pub active_doll_id: Option, + pub active_doll: Option, } #[derive(Default, Serialize, Deserialize, Clone, Debug, TS)] diff --git a/src-tauri/src/services/ws.rs b/src-tauri/src/services/ws.rs index db18a03..cda0df2 100644 --- a/src-tauri/src/services/ws.rs +++ b/src-tauri/src/services/ws.rs @@ -1,4 +1,4 @@ -use rust_socketio::{ClientBuilder, Payload, RawClient}; +use rust_socketio::{ClientBuilder, Event, Payload, RawClient}; use serde_json::json; use tauri::{async_runtime, Emitter}; use tracing::{error, info}; @@ -39,9 +39,39 @@ impl WS_EVENT { pub const FRIEND_DOLL_CREATED: &str = "friend-doll-created"; pub const FRIEND_DOLL_UPDATED: &str = "friend-doll-updated"; pub const FRIEND_DOLL_DELETED: &str = "friend-doll-deleted"; + pub const FRIEND_ACTIVE_DOLL_CHANGED: &str = "friend-active-doll-changed"; pub const DOLL_CREATED: &str = "doll_created"; pub const DOLL_UPDATED: &str = "doll_updated"; pub const DOLL_DELETED: &str = "doll_deleted"; + pub const CLIENT_INITIALIZE: &str = "client-initialize"; + pub const INITIALIZED: &str = "initialized"; +} + +fn on_connected(_payload: Payload, socket: RawClient) { + info!("WebSocket connected. Sending initialization request."); + if let Err(e) = socket.emit(WS_EVENT::CLIENT_INITIALIZE, json!({})) { + error!("Failed to emit client-initialize: {:?}", e); + } +} + +fn on_initialized(payload: Payload, _socket: RawClient) { + match payload { + Payload::Text(values) => { + if let Some(first_value) = values.first() { + info!("Received initialized event: {:?}", first_value); + + // Mark WebSocket as initialized + let mut guard = lock_w!(FDOLL); + if let Some(clients) = guard.clients.as_mut() { + clients.is_ws_initialized = true; + info!("WebSocket marked as initialized and ready for business"); + } + } else { + info!("Received initialized event with empty payload"); + } + } + _ => error!("Received unexpected payload format for initialized"), + } } fn on_friend_request_received(payload: Payload, _socket: RawClient) { @@ -154,10 +184,10 @@ fn on_friend_doll_created(payload: Payload, _socket: RawClient) { info!("Received friend-doll-created event: {:?}", first_value); // Future: Trigger re-fetch or emit to frontend } else { - info!("Received friend-doll-created event with empty payload"); + info!("Received friend-doll-created event with empty payload"); } } - _ => error!("Received unexpected payload format for friend-doll-created"), + _ => error!("Received unexpected payload format for friend-doll-created"), } } @@ -167,7 +197,7 @@ fn on_friend_doll_updated(payload: Payload, _socket: RawClient) { if let Some(first_value) = values.first() { info!("Received friend-doll-updated event: {:?}", first_value); } else { - info!("Received friend-doll-updated event with empty payload"); + info!("Received friend-doll-updated event with empty payload"); } } _ => error!("Received unexpected payload format for friend-doll-updated"), @@ -180,26 +210,49 @@ fn on_friend_doll_deleted(payload: Payload, _socket: RawClient) { if let Some(first_value) = values.first() { info!("Received friend-doll-deleted event: {:?}", first_value); } else { - info!("Received friend-doll-deleted event with empty payload"); + info!("Received friend-doll-deleted event with empty payload"); } } _ => error!("Received unexpected payload format for friend-doll-deleted"), } } +fn on_friend_active_doll_changed(payload: Payload, _socket: RawClient) { + match payload { + Payload::Text(values) => { + if let Some(first_value) = values.first() { + info!( + "Received friend-active-doll-changed event: {:?}", + first_value + ); + if let Err(e) = + get_app_handle().emit(WS_EVENT::FRIEND_ACTIVE_DOLL_CHANGED, first_value) + { + error!("Failed to emit friend-active-doll-changed event: {:?}", e); + } else { + info!("Emitted friend-active-doll-changed to frontend"); + } + } else { + info!("Received friend-active-doll-changed event with empty payload"); + } + } + _ => error!("Received unexpected payload format for friend-active-doll-changed"), + } +} + fn on_doll_created(payload: Payload, _socket: RawClient) { match payload { Payload::Text(values) => { if let Some(first_value) = values.first() { info!("Received doll.created event: {:?}", first_value); - if let Err(e) = get_app_handle().emit(WS_EVENT::DOLL_CREATED, first_value) { + if let Err(e) = get_app_handle().emit(WS_EVENT::DOLL_CREATED, first_value) { error!("Failed to emit doll.created event: {:?}", e); } } else { - info!("Received doll.created event with empty payload"); + info!("Received doll.created event with empty payload"); } } - _ => error!("Received unexpected payload format for doll.created"), + _ => error!("Received unexpected payload format for doll.created"), } } @@ -212,7 +265,7 @@ fn on_doll_updated(payload: Payload, _socket: RawClient) { error!("Failed to emit doll.updated event: {:?}", e); } } else { - info!("Received doll.updated event with empty payload"); + info!("Received doll.updated event with empty payload"); } } _ => error!("Received unexpected payload format for doll.updated"), @@ -228,7 +281,7 @@ fn on_doll_deleted(payload: Payload, _socket: RawClient) { error!("Failed to emit doll.deleted event: {:?}", e); } } else { - info!("Received doll.deleted event with empty payload"); + info!("Received doll.deleted event with empty payload"); } } _ => error!("Received unexpected payload format for doll.deleted"), @@ -240,11 +293,15 @@ pub async fn report_cursor_data(cursor_position: CursorPosition) { // and if clients are actually initialized. let client_opt = { let guard = lock_r!(FDOLL); - guard - .clients - .as_ref() - .and_then(|c| c.ws_client.as_ref()) - .cloned() + if let Some(clients) = &guard.clients { + if clients.is_ws_initialized { + clients.ws_client.as_ref().cloned() + } else { + None + } + } else { + None + } }; if let Some(client) = client_opt { @@ -261,9 +318,7 @@ pub async fn report_cursor_data(cursor_position: CursorPosition) { Err(e) => error!("Failed to execute blocking task for cursor report: {}", e), } } else { - // Quietly fail if client is not ready (e.g. during startup/shutdown) - // or debug log it. - // debug!("WebSocket client not ready to report cursor data"); + // Quietly fail if client is not ready (e.g. during startup/shutdown or before initialization) } } @@ -323,9 +378,15 @@ pub async fn build_ws_client( .on(WS_EVENT::FRIEND_DOLL_CREATED, on_friend_doll_created) .on(WS_EVENT::FRIEND_DOLL_UPDATED, on_friend_doll_updated) .on(WS_EVENT::FRIEND_DOLL_DELETED, on_friend_doll_deleted) + .on( + WS_EVENT::FRIEND_ACTIVE_DOLL_CHANGED, + on_friend_active_doll_changed, + ) .on(WS_EVENT::DOLL_CREATED, on_doll_created) .on(WS_EVENT::DOLL_UPDATED, on_doll_updated) .on(WS_EVENT::DOLL_DELETED, on_doll_deleted) + .on(WS_EVENT::INITIALIZED, on_initialized) + .on(Event::Connect, on_connected) .auth(json!({ "token": token })) .connect() }) diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index 3632358..82a595d 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -26,6 +26,7 @@ pub struct OAuthFlowTracker { pub struct Clients { pub http_client: reqwest::Client, pub ws_client: Option, + pub is_ws_initialized: bool, } #[derive(Default)] @@ -78,6 +79,7 @@ pub fn init_fdoll_state(tracing_guard: Option({ raw: { x: 0, y: 0 }, @@ -24,13 +25,13 @@ type FriendCursorData = { // The exported store will only expose the position part to consumers, // but internally we manage the full data. // Actually, it's easier if we just export the positions and manage state internally. -export let friendsCursorPositions = writable>( - {}, -); +export let friendsCursorPositions = writable>({}); +export let friendsActiveDolls = writable>({}); let unlistenCursor: UnlistenFn | null = null; let unlistenFriendCursor: UnlistenFn | null = null; let unlistenFriendDisconnected: UnlistenFn | null = null; +let unlistenFriendActiveDollChanged: UnlistenFn | null = null; let isListening = false; // Internal state to track timestamps @@ -89,16 +90,19 @@ export async function initCursorTracking() { try { payload = JSON.parse(payload); } catch (e) { - console.error("[Cursor] Failed to parse friend disconnected payload:", e); + console.error( + "[Cursor] Failed to parse friend disconnected payload:", + e, + ); return; } } const data = Array.isArray(payload) ? payload[0] : payload; - + // Remove from internal state if (friendCursorState[data.userId]) { - delete friendCursorState[data.userId]; + delete friendCursorState[data.userId]; } // Update svelte store @@ -107,9 +111,75 @@ export async function initCursorTracking() { delete next[data.userId]; return next; }); - } + }, ); + // Listen to friend active doll changed events + unlistenFriendActiveDollChanged = await listen< + | string + | { + friendId: string; + doll: DollDto | null; + } + >("friend-active-doll-changed", (event) => { + let data = event.payload; + + if (typeof data === "string") { + try { + data = JSON.parse(data); + } catch (e) { + console.error( + "[Cursor] Failed to parse friend-active-doll-changed payload:", + e, + ); + return; + } + } + + // Cast to expected type after parsing + const payload = data as { friendId: string; doll: DollDto | null }; + + console.log( + "[Cursor] Received friend-active-doll-changed event:", + payload, + ); + + if (!payload.doll) { + // If doll is null, it means the friend deactivated their doll. + console.log( + `[Cursor] Removing doll for friend ${payload.friendId} due to deactivation`, + ); + + // Update the active dolls store to explicitly set this friend's doll to null + // We MUST set it to null instead of deleting it, otherwise the UI might + // fall back to the initial appData snapshot which might still have the old doll. + friendsActiveDolls.update((current) => { + const next = { ...current }; + next[payload.friendId] = null; + return next; + }); + + // Also remove from cursor positions so the sprite disappears + friendsCursorPositions.update((current) => { + const next = { ...current }; + delete next[payload.friendId]; + return next; + }); + } else { + // Update or add the new doll configuration + console.log( + `[Cursor] Updating doll for friend ${payload.friendId}:`, + payload.doll, + ); + friendsActiveDolls.update((current) => { + return { + ...current, + [payload.friendId]: payload.doll!, + }; + }); + } + }); + isListening = true; } catch (err) { console.error("[Cursor] Failed to initialize cursor tracking:", err); @@ -134,6 +204,10 @@ export function stopCursorTracking() { unlistenFriendDisconnected(); unlistenFriendDisconnected = null; } + if (unlistenFriendActiveDollChanged) { + unlistenFriendActiveDollChanged(); + unlistenFriendActiveDollChanged = null; + } isListening = false; } diff --git a/src/routes/scene/+page.svelte b/src/routes/scene/+page.svelte index 4ad3992..81223e4 100644 --- a/src/routes/scene/+page.svelte +++ b/src/routes/scene/+page.svelte @@ -2,6 +2,7 @@ import { cursorPositionOnScreen, friendsCursorPositions, + friendsActiveDolls, } from "../../events/cursor"; import { appData } from "../../events/app-data"; @@ -14,6 +15,18 @@ const friend = $appData?.friends?.find((f) => f.friend.id === userId); return friend ? friend.friend.name : userId.slice(0, 8) + "..."; } + + function getFriendDollConfig(userId: string) { + // 1. Try to get from real-time store (most up-to-date) + // Check if key exists to distinguish between "unknown" (undefined) and "no doll" (null) + if (userId in $friendsActiveDolls) { + return $friendsActiveDolls[userId]?.configuration; + } + + // 2. Fallback to initial app data (snapshot on load) + const friend = $appData?.friends?.find((f) => f.friend.id === userId); + return friend?.friend.activeDoll?.configuration; + } @@ -66,11 +79,16 @@
{#if Object.keys($friendsCursorPositions).length > 0} {#each Object.entries($friendsCursorPositions) as [userId, position]} - + {@const config = getFriendDollConfig(userId)} + {#if config} + + {/if} {/each} {/if}
diff --git a/src/routes/scene/DesktopPet.svelte b/src/routes/scene/DesktopPet.svelte index 3a1c324..1fd9701 100644 --- a/src/routes/scene/DesktopPet.svelte +++ b/src/routes/scene/DesktopPet.svelte @@ -1,10 +1,13 @@